From e5a1ed4c8e24d3acfaed3f5e89c90fb0a3e06e8e Mon Sep 17 00:00:00 2001 From: uberswe Date: Mon, 3 Aug 2026 22:30:30 +0200 Subject: [PATCH] Integrate with Create: Blueprinted and fix multiblock rendering Blueprinted integration: - Register a CreateMod.com ShareProvider (optional dep, guarded classloading) - Replace Blueprinted's share button with our own matching SmallIconButton that runs the full 360-degree upload pipeline and carries a tooltip with a private-posting disclaimer - Move the local/download mode toggle into Blueprinted's button column as a matching 15px button, anchored to topPos for even 17px spacing - Hide Blueprinted's export/share buttons in download mode - State-aware table title: "Download a schematic" when idle in download mode, Create's own uploading/finished text otherwise (re-centered) - Add privacy note chat message when uploads start Renderer fixes: - Rebase Create multiblock NBT (Controller/LastKnownPos) onto template coordinates so fluid tanks and item vaults keep their connectivity and render correctly instead of as broken 1x1 segments - Add a block entity render pass so belts, tank fluid, funnel flaps and other BER-only visuals appear in renders Co-Authored-By: Claude Fable 5 --- .../src/main/groovy/multiloader-common.gradle | 11 ++ .../PoseAppliedVertexConsumer.java | 68 +++++++++ .../SchematicIsometricRenderer.java | 129 +++++++++++++++++- .../SchematicUploadHandler.java | 37 ++++- .../createschematichelper/lang/en_us.json | 9 +- neoforge/build.gradle | 2 + .../CreateSchematicHelperNeoForge.java | 8 ++ .../neoforge/ScaledIcon.java | 26 ++++ .../neoforge/compat/BlueprintedCompat.java | 115 ++++++++++++++++ .../mixin/SchematicTableScreenMixin.java | 72 ++++++++-- .../templates/META-INF/neoforge.mods.toml | 7 + 11 files changed, 470 insertions(+), 14 deletions(-) create mode 100644 common/src/main/java/com/uberswe/createschematichelper/PoseAppliedVertexConsumer.java create mode 100644 neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/ScaledIcon.java create mode 100644 neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/compat/BlueprintedCompat.java diff --git a/buildSrc/src/main/groovy/multiloader-common.gradle b/buildSrc/src/main/groovy/multiloader-common.gradle index 41213e9..c579a03 100644 --- a/buildSrc/src/main/groovy/multiloader-common.gradle +++ b/buildSrc/src/main/groovy/multiloader-common.gradle @@ -38,6 +38,17 @@ repositories { name = "BlameJared Maven" url = "https://maven.blamejared.com" } + exclusiveContent { + forRepository { + maven { + name = "Modrinth" + url = "https://api.modrinth.com/maven" + } + } + filter { + includeGroup "maven.modrinth" + } + } } // Expose source directories to loader subprojects diff --git a/common/src/main/java/com/uberswe/createschematichelper/PoseAppliedVertexConsumer.java b/common/src/main/java/com/uberswe/createschematichelper/PoseAppliedVertexConsumer.java new file mode 100644 index 0000000..4f6db5b --- /dev/null +++ b/common/src/main/java/com/uberswe/createschematichelper/PoseAppliedVertexConsumer.java @@ -0,0 +1,68 @@ +package com.uberswe.createschematichelper; + +import com.mojang.blaze3d.vertex.VertexConsumer; +import org.jetbrains.annotations.NotNull; +import org.joml.Matrix3f; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +/** + * Bakes a pose into vertices before delegating, so geometry emitted in chunk-local + * coordinates (e.g. {@code BlockRenderDispatcher.renderLiquid}) lands in the schematic's + * transformed space. Adapted from Create: Blueprinted by salem-5/swzo (MIT). + */ +final class PoseAppliedVertexConsumer implements VertexConsumer { + + private VertexConsumer delegate; + private final Matrix4f pose = new Matrix4f(); + private final Matrix3f normal = new Matrix3f(); + private float offX, offY, offZ; + private final Vector3f scratch = new Vector3f(); + + void prepare(VertexConsumer delegate, Matrix4f pose, Matrix3f normal, float offX, float offY, float offZ) { + this.delegate = delegate; + this.pose.set(pose); + this.normal.set(normal); + this.offX = offX; + this.offY = offY; + this.offZ = offZ; + } + + @Override + public @NotNull VertexConsumer addVertex(float x, float y, float z) { + pose.transformPosition(x + offX, y + offY, z + offZ, scratch); + delegate.addVertex(scratch.x(), scratch.y(), scratch.z()); + return this; + } + + @Override + public @NotNull VertexConsumer setNormal(float x, float y, float z) { + normal.transform(x, y, z, scratch); + delegate.setNormal(scratch.x(), scratch.y(), scratch.z()); + return this; + } + + @Override + public @NotNull VertexConsumer setColor(int red, int green, int blue, int alpha) { + delegate.setColor(red, green, blue, alpha); + return this; + } + + @Override + public @NotNull VertexConsumer setUv(float u, float v) { + delegate.setUv(u, v); + return this; + } + + @Override + public @NotNull VertexConsumer setUv1(int u, int v) { + delegate.setUv1(u, v); + return this; + } + + @Override + public @NotNull VertexConsumer setUv2(int u, int v) { + delegate.setUv2(u, v); + return this; + } +} diff --git a/common/src/main/java/com/uberswe/createschematichelper/SchematicIsometricRenderer.java b/common/src/main/java/com/uberswe/createschematichelper/SchematicIsometricRenderer.java index 31cadc7..202e510 100644 --- a/common/src/main/java/com/uberswe/createschematichelper/SchematicIsometricRenderer.java +++ b/common/src/main/java/com/uberswe/createschematichelper/SchematicIsometricRenderer.java @@ -11,16 +11,29 @@ import net.createmod.catnip.levelWrappers.SchematicLevel; import net.createmod.catnip.render.SuperRenderTypeBuffer; import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.ItemBlockRenderTypes; +import net.minecraft.client.renderer.LightTexture; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.block.BlockRenderDispatcher; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.texture.OverlayTexture; import net.minecraft.core.BlockPos; import net.minecraft.core.Vec3i; import net.minecraft.core.registries.Registries; import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; import net.minecraft.nbt.NbtAccounter; import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.Tag; import net.minecraft.util.RandomSource; import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; import org.jetbrains.annotations.NotNull; @@ -40,8 +53,10 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -71,6 +86,9 @@ public interface ProgressCallback { private static final ProgressCallback NOOP = (stage, current, total) -> {}; + // Block entity types whose renderer already threw once; logged once and skipped thereafter + private static final Set> FAILED_BE_TYPES = ConcurrentHashMap.newKeySet(); + public static CompletableFuture> render360(Path nbtFile) { return render360(nbtFile, NOOP); } @@ -100,6 +118,9 @@ private record RenderState( RenderTarget renderTarget, SchematicRenderer renderer, SuperRenderTypeBuffer buffers, + SchematicLevel schematicLevel, + List fluidPositions, + PoseAppliedVertexConsumer fluidConsumer, float[] angles, Vec3i size, float scale, @@ -109,6 +130,7 @@ private record RenderState( private record PreRenderState( SchematicLevel schematicLevel, + List fluidPositions, Vec3i size, float[] angles, float scale, @@ -116,6 +138,35 @@ private record PreRenderState( int fbH ) {} + // Create's multiblocks (fluid tanks, item vaults) save their controller reference and + // "LastKnownPos" as absolute world coordinates. Placed at the origin those no longer match + // the block's position, so on load every segment discards its controller and renders as a + // broken standalone block — and reformation only happens on a ticking server level. + // Rebasing both tags onto template-local coordinates preserves the built connectivity. + private static void remapMultiblockNbt(CompoundTag tag) { + if (!tag.contains("blocks", Tag.TAG_LIST)) { + return; + } + ListTag blocks = tag.getList("blocks", Tag.TAG_COMPOUND); + for (int i = 0; i < blocks.size(); i++) { + CompoundTag entry = blocks.getCompound(i); + if (!entry.contains("nbt", Tag.TAG_COMPOUND) || !entry.contains("pos", Tag.TAG_LIST)) { + continue; + } + CompoundTag nbt = entry.getCompound("nbt"); + Optional lastKnown = NbtUtils.readBlockPos(nbt, "LastKnownPos"); + if (lastKnown.isEmpty()) { + continue; + } + ListTag posTag = entry.getList("pos", Tag.TAG_INT); + BlockPos pos = new BlockPos(posTag.getInt(0), posTag.getInt(1), posTag.getInt(2)); + BlockPos delta = pos.subtract(lastKnown.get()); + NbtUtils.readBlockPos(nbt, "Controller").ifPresent(controller -> + nbt.put("Controller", NbtUtils.writeBlockPos(controller.offset(delta)))); + nbt.put("LastKnownPos", NbtUtils.writeBlockPos(pos)); + } + } + private static PreRenderState prepareOffThread(CompoundTag tag) { Minecraft mc = Minecraft.getInstance(); @@ -123,6 +174,8 @@ private static PreRenderState prepareOffThread(CompoundTag tag) { throw new IllegalStateException("No active world"); } + remapMultiblockNbt(tag); + StructureTemplate template = new StructureTemplate(); template.load(mc.level.holderLookup(Registries.BLOCK), tag); LOGGER.info("Template loaded: size={}", template.getSize()); @@ -177,7 +230,15 @@ private static PreRenderState prepareOffThread(CompoundTag tag) { StructurePlaceSettings settings = new StructurePlaceSettings(); template.placeInWorld(schematicLevel, BlockPos.ZERO, BlockPos.ZERO, settings, RandomSource.create(), Block.UPDATE_CLIENTS); - LOGGER.info("Render setup: fb={}x{}, scale={}", fbW, fbH, scale); + List fluidPositions = new ArrayList<>(); + for (var entry : schematicLevel.getBlockMap().entrySet()) { + BlockState state = entry.getValue(); + if (!state.isAir() && !state.getFluidState().isEmpty()) { + fluidPositions.add(entry.getKey().immutable()); + } + } + + LOGGER.info("Render setup: fb={}x{}, scale={}, fluids={}", fbW, fbH, scale, fluidPositions.size()); float[] angles; if (ConfigValues.render360) { @@ -191,7 +252,7 @@ private static PreRenderState prepareOffThread(CompoundTag tag) { angles = FEATURED_ANGLES; } - return new PreRenderState(schematicLevel, size, angles, scale, fbW, fbH); + return new PreRenderState(schematicLevel, fluidPositions, size, angles, scale, fbW, fbH); } private static RenderState finalizeOnRenderThread(PreRenderState pre) { @@ -217,7 +278,8 @@ private static RenderState finalizeOnRenderThread(PreRenderState pre) { @Override public void draw(@NotNull RenderType type) { mcBuffers.endBatch(type); } }; - return new RenderState(mc, renderTarget, renderer, buffers, pre.angles, pre.size, pre.scale, pre.fbW, pre.fbH); + return new RenderState(mc, renderTarget, renderer, buffers, pre.schematicLevel, pre.fluidPositions, + new PoseAppliedVertexConsumer(), pre.angles, pre.size, pre.scale, pre.fbW, pre.fbH); } private static void renderBatch(RenderState state, int startIndex, List images, @@ -265,6 +327,8 @@ private static void renderBatch(RenderState state, int startIndex, List renderer = dispatcher.getRenderer(be); + if (renderer == null) { + continue; + } + BlockPos pos = be.getBlockPos(); + poseStack.pushPose(); + poseStack.translate(pos.getX(), pos.getY(), pos.getZ()); + try { + renderer.render(be, 0f, poseStack, state.buffers, + LightTexture.FULL_BRIGHT, OverlayTexture.NO_OVERLAY); + } catch (Exception e) { + if (FAILED_BE_TYPES.add(be.getType())) { + LOGGER.warn("Skipping block entity renderer for {} in schematic renders", + BlockEntityType.getKey(be.getType()), e); + } + } + poseStack.popPose(); + } + } + + // Create's SchematicRenderer skips fluids entirely, so water/lava/waterlogged blocks are + // rendered here via the vanilla liquid renderer. renderLiquid emits chunk-local vertices, + // so the current pose plus the chunk origin is baked in through the consumer. + private static void renderFluids(RenderState state, PoseStack poseStack) { + if (state.fluidPositions.isEmpty()) { + return; + } + BlockRenderDispatcher dispatcher = state.mc.getBlockRenderer(); + Matrix4f pose = poseStack.last().pose(); + org.joml.Matrix3f normal = poseStack.last().normal(); + + for (BlockPos pos : state.fluidPositions) { + BlockState blockState = state.schematicLevel.getBlockState(pos); + FluidState fluid = blockState.getFluidState(); + if (fluid.isEmpty()) { + continue; + } + RenderType layer = ItemBlockRenderTypes.getRenderLayer(fluid); + state.fluidConsumer.prepare(state.buffers.getBuffer(layer), pose, normal, + pos.getX() - (pos.getX() & 15), + pos.getY() - (pos.getY() & 15), + pos.getZ() - (pos.getZ() & 15)); + dispatcher.renderLiquid(pos, state.schematicLevel, state.fluidConsumer, blockState, fluid); + } + } + public static CompletableFuture> render360(CompoundTag tag, ProgressCallback progress) { return CompletableFuture.supplyAsync(() -> prepareOffThread(tag)) .thenCompose(pre -> { @@ -605,7 +724,9 @@ private static BufferedImage generateBlueprintBackgroundBuffered(int w, int h) { private static BufferedImage nativeToBuffered(NativeImage source) { int w = source.getWidth(); int h = source.getHeight(); - BufferedImage result = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + // Premultiplied alpha: bicubic downscaling of a straight-alpha image blends the black + // RGB of transparent pixels into edges, producing dark halos around the schematic. + BufferedImage result = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { int pixel = source.getPixelRGBA(x, y); diff --git a/common/src/main/java/com/uberswe/createschematichelper/SchematicUploadHandler.java b/common/src/main/java/com/uberswe/createschematichelper/SchematicUploadHandler.java index e73e91e..8c40caf 100644 --- a/common/src/main/java/com/uberswe/createschematichelper/SchematicUploadHandler.java +++ b/common/src/main/java/com/uberswe/createschematichelper/SchematicUploadHandler.java @@ -119,9 +119,41 @@ private static void renderAsync(Path filePath) { }); } + /** + * Entry point for Create: Blueprinted's share button. Runs the same pipeline as the + * chat upload link: a full 360° render of the schematic followed by the async upload. + * Blueprinted invokes its ShareProvider on the main client thread, so nothing here + * may block — progress and the resulting link are reported through chat messages. + * + * @return the destination base URL, or null if the schematic file could not be found + */ + public static java.net.URL shareSchematic(String schematicName) { + Path schematicsDir = Minecraft.getInstance().gameDirectory.toPath().resolve("schematics"); + Path filePath = schematicsDir.resolve(schematicName); + if (!Files.exists(filePath) && !schematicName.endsWith(".nbt")) { + filePath = schematicsDir.resolve(schematicName + ".nbt"); + } + if (!Files.exists(filePath)) { + LOGGER.error("Cannot share schematic, file not found: {}", schematicName); + sendChatMessage(Component.translatable("createschematichelper.share.missing_file", schematicName) + .withStyle(ChatFormatting.YELLOW)); + return null; + } + + uploadAsync(filePath); + + try { + return URI.create(ConfigValues.baseUrl).toURL(); + } catch (Exception e) { + return null; + } + } + private static void uploadAsync(Path filePath) { sendChatMessage(Component.translatable("createschematichelper.upload.uploading") .withStyle(ChatFormatting.GRAY)); + sendChatMessage(Component.translatable("createschematichelper.upload.private_note") + .withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC)); sendProgressBar("Rendering", 0, 1); SchematicIsometricRenderer.render360(filePath, (stage, current, total) -> { @@ -263,7 +295,10 @@ private static byte[] buildMultipartBody(String boundary, String fileName, byte[ if (frame.featured()) { writePart(baos, boundary, crlf, "images", frame.filename(), frame.mimeType(), frame.data()); } - writePart(baos, boundary, crlf, "rotation_images", frame.filename(), frame.mimeType(), frame.data()); + // A single frame is not a rotation sequence + if (frames.size() > 1) { + writePart(baos, boundary, crlf, "rotation_images", frame.filename(), frame.mimeType(), frame.data()); + } } baos.write(("--" + boundary + "--" + crlf).getBytes(StandardCharsets.UTF_8)); diff --git a/common/src/main/resources/assets/createschematichelper/lang/en_us.json b/common/src/main/resources/assets/createschematichelper/lang/en_us.json index f775c8c..bd524bc 100644 --- a/common/src/main/resources/assets/createschematichelper/lang/en_us.json +++ b/common/src/main/resources/assets/createschematichelper/lang/en_us.json @@ -2,6 +2,7 @@ "createschematichelper.upload.rendering": "Rendering schematic previews, this might take a few seconds...", "createschematichelper.upload.success": "Schematic uploaded! View at: ", "createschematichelper.upload.uploading": "Uploading schematic to createmod.com, this might take 30 seconds or so", + "createschematichelper.upload.private_note": "Your schematic will be posted privately. You can then share the private link or publish your schematic on the site.", "createschematichelper.upload.error": "Upload failed: %s", "createschematichelper.upload.failed": "Schematic could not be automatically uploaded", "createschematichelper.upload.already_exists": "This schematic has already been uploaded", @@ -14,7 +15,13 @@ "createschematichelper.confirm.render": "Generate", "createschematichelper.render.success": "Screenshots saved to schematics folder", "createschematichelper.render.failed": "Failed to generate screenshots", - "text.createschematichelper.processing": "Processing...", + "createschematichelper.share.missing_file": "Could not find schematic file \"%s\" to upload", + "text.createschematichelper.download_title": "Download a schematic", + "text.createschematichelper.share_title": "Share Schematic", + "text.createschematichelper.share_upload": "Upload schematic to CreateMod.com", + "text.createschematichelper.share_note1": "Your schematic will be posted privately.", + "text.createschematichelper.share_note2": "You can then share the private link or", + "text.createschematichelper.share_note3": "publish your schematic on the site.", "text.createschematichelper.url_field_hint": "Code or CreateMod.com URL", "text.createschematichelper.download_schematic": "Download a schematic from CreateMod.com", "text.createschematichelper.choose_local_schematic": "Choose a local schematic" diff --git a/neoforge/build.gradle b/neoforge/build.gradle index f95106d..51fe0ac 100644 --- a/neoforge/build.gradle +++ b/neoforge/build.gradle @@ -43,6 +43,8 @@ dependencies { transitive = false } compileOnly("net.createmod.ponder:ponder-neoforge:1.0.81+mc${minecraft_version}") + // Optional integration: registers a ShareProvider so Blueprinted's share button uploads to createmod.com + compileOnly("maven.modrinth:create-blueprinted:2.0+mc1.21.1-neoforge") } // Generate mod metadata from templates diff --git a/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/CreateSchematicHelperNeoForge.java b/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/CreateSchematicHelperNeoForge.java index 959b0bd..d70aa7d 100644 --- a/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/CreateSchematicHelperNeoForge.java +++ b/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/CreateSchematicHelperNeoForge.java @@ -41,6 +41,14 @@ public CreateSchematicHelperNeoForge(IEventBus modEventBus, ModContainer modCont ModList.get().getModContainerById("createschematichelper") .ifPresent(mc -> ConfigValues.modVersion = mc.getModInfo().getVersion().toString()); + if (ModList.get().isLoaded("create_blueprinted")) { + try { + com.uberswe.createschematichelper.neoforge.compat.BlueprintedCompat.register(); + } catch (Throwable t) { + LOGGER.warn("Create: Blueprinted is present but registering the share provider failed", t); + } + } + LOGGER.info("CreateSchematicHelper loaded (NeoForge)"); } diff --git a/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/ScaledIcon.java b/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/ScaledIcon.java new file mode 100644 index 0000000..cd69588 --- /dev/null +++ b/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/ScaledIcon.java @@ -0,0 +1,26 @@ +package com.uberswe.createschematichelper.neoforge; + +import net.createmod.catnip.gui.element.ScreenElement; +import net.minecraft.client.gui.GuiGraphics; + +/** + * Renders a standard 16x16 icon scaled down to 13x13 so it fits the icon area of + * Create: Blueprinted's 15px SmallIconButton (which draws its icon at +1,+1). + */ +public class ScaledIcon implements ScreenElement { + private static final float SCALE = 13f / 16f; + private final ScreenElement inner; + + public ScaledIcon(ScreenElement inner) { + this.inner = inner; + } + + @Override + public void render(GuiGraphics graphics, int x, int y) { + graphics.pose().pushPose(); + graphics.pose().translate(x, y, 0); + graphics.pose().scale(SCALE, SCALE, 1); + inner.render(graphics, 0, 0); + graphics.pose().popPose(); + } +} diff --git a/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/compat/BlueprintedCompat.java b/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/compat/BlueprintedCompat.java new file mode 100644 index 0000000..473303f --- /dev/null +++ b/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/compat/BlueprintedCompat.java @@ -0,0 +1,115 @@ +package com.uberswe.createschematichelper.neoforge.compat; + +import com.mojang.logging.LogUtils; +import com.simibubi.create.foundation.gui.widget.IconButton; +import com.uberswe.createschematichelper.ConfigValues; +import com.uberswe.createschematichelper.SchematicUploadHandler; +import net.createmod.catnip.gui.element.ScreenElement; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.swzo.create_blueprinted.api.ShareProvider; +import net.swzo.create_blueprinted.api.ShareProviderRegistry; +import net.swzo.create_blueprinted.gui.CBGuiTextures; +import net.swzo.create_blueprinted.gui.ExportButton; +import net.swzo.create_blueprinted.gui.ShareButton; +import net.swzo.create_blueprinted.gui.SmallIconButton; +import net.swzo.create_blueprinted.render.SchematicRenderSettings; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.net.URL; + +/** + * Integration with Create: Blueprinted's ShareProvider API. This class references + * Blueprinted types directly and must only be classloaded when create_blueprinted is present. + */ +public final class BlueprintedCompat { + private static final Logger LOGGER = LogUtils.getLogger(); + + private BlueprintedCompat() {} + + public static void register() { + ShareProviderRegistry.register(new CreateModComShareProvider()); + LOGGER.info("Registered CreateMod.com share provider with Create: Blueprinted"); + } + + /** + * A 15px button matching Blueprinted's export/share/refresh column, so our mode + * toggle can sit directly below them with the same look. + */ + public static IconButton createSmallButton(int x, int y, ScreenElement icon) { + return new SmallIconButton(x, y, icon); + } + + /** + * Toggles Blueprinted's export button alongside our local/download mode switch. + * Their replacement refresh button is not handled here — it is assigned to Create's + * refreshButton field, which the mixin already toggles. Their share button is + * removed and replaced with our own (see {@link #findShareButton}). + */ + public static void setShareButtonsVisible(Iterable widgets, boolean visible) { + for (Object widget : widgets) { + if (widget instanceof ExportButton button) { + button.visible = button.active = visible; + } + } + } + + /** Locates Blueprinted's ShareButton among the screen's widgets, if present. */ + public static @Nullable IconButton findShareButton(Iterable widgets) { + for (Object widget : widgets) { + if (widget instanceof ShareButton button) { + return button; + } + } + return null; + } + + /** + * Our replacement for Blueprinted's share button: same icon and slot, but with our + * own tooltip and a click handler that runs the full 360° upload pipeline directly, + * skipping Blueprinted's single-image render entirely. + */ + public static IconButton createShareButton(int x, int y, Runnable onClick) { + IconButton button = new SmallIconButton(x, y, CBGuiTextures.SHARE_ICON); + button.withCallback(onClick); + button.getToolTip().add(Component.translatable("text.createschematichelper.share_title")); + button.getToolTip().add(Component.translatable("text.createschematichelper.share_upload") + .withStyle(ChatFormatting.GRAY)); + button.getToolTip().add(Component.translatable("text.createschematichelper.share_note1") + .withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC)); + button.getToolTip().add(Component.translatable("text.createschematichelper.share_note2") + .withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC)); + button.getToolTip().add(Component.translatable("text.createschematichelper.share_note3") + .withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC)); + return button; + } + + private static final class CreateModComShareProvider implements ShareProvider { + @Override + public ResourceLocation id() { + return ResourceLocation.fromNamespaceAndPath("createschematichelper", "createmod_com"); + } + + @Override + public Component destinationName() { + return Component.literal("CreateMod.com"); + } + + @Override + public String destinationUrl() { + String url = ConfigValues.baseUrl; + return url != null && url.startsWith("https://") && url.length() < MAX_URL_CHAR_LENGTH + ? url : "https://createmod.com"; + } + + @Override + public @Nullable URL onRender(ResourceLocation handlerId, String schematicName, + SchematicRenderSettings renderSettings, byte[] imageByteArray) { + // Blueprinted's single rendered image is ignored: sharing runs the same full + // pipeline as the chat upload link (360° rotation render + featured frames). + return SchematicUploadHandler.shareSchematic(schematicName); + } + } +} diff --git a/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/mixin/SchematicTableScreenMixin.java b/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/mixin/SchematicTableScreenMixin.java index 8327d60..8fda9eb 100644 --- a/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/mixin/SchematicTableScreenMixin.java +++ b/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/mixin/SchematicTableScreenMixin.java @@ -13,7 +13,11 @@ import com.simibubi.create.foundation.gui.widget.Label; import com.simibubi.create.foundation.gui.widget.ScrollInput; import com.uberswe.createschematichelper.SchematicDownloadHandler; +import com.uberswe.createschematichelper.SchematicUploadHandler; import com.uberswe.createschematichelper.neoforge.DownloadIcon; +import com.uberswe.createschematichelper.neoforge.ScaledIcon; +import com.uberswe.createschematichelper.neoforge.compat.BlueprintedCompat; +import net.createmod.catnip.gui.element.ScreenElement; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.EditBox; @@ -21,6 +25,7 @@ import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.player.Inventory; +import net.neoforged.fml.ModList; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; @@ -30,18 +35,27 @@ import org.spongepowered.asm.mixin.injection.ModifyConstant; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -@Mixin(SchematicTableScreen.class) +import java.util.List; + +// Higher priority than Blueprinted's mixin (default 1000) so our init tail runs after +// theirs and can find and replace the ShareButton it adds. +@Mixin(value = SchematicTableScreen.class, priority = 1500) public abstract class SchematicTableScreenMixin extends AbstractSimiContainerScreen { @Unique private static final ResourceLocation createschematichelper$TABLE_TEXTURE = ResourceLocation.fromNamespaceAndPath("createschematichelper", "textures/gui/schematic_table.png"); @Unique private static final Component createschematichelper$URL_FIELD_HINT = Component.translatable("text.createschematichelper.url_field_hint"); @Unique - private static final Component createschematichelper$PROCESSING_TITLE = Component.translatable("text.createschematichelper.processing"); + private static final Component createschematichelper$DOWNLOAD_TITLE = Component.translatable("text.createschematichelper.download_title"); @Unique private static final Component createschematichelper$DOWNLOAD_TOOLTIP = Component.translatable("text.createschematichelper.download_schematic"); @Unique private static final Component createschematichelper$LOCAL_TOOLTIP = Component.translatable("text.createschematichelper.choose_local_schematic"); + // Create: Blueprinted stacks 15px buttons at x+205: export (y+1), share (y+18) and its + // replacement refresh button (y+35). Our mode toggle continues that column at y+52, + // built as a matching SmallIconButton via BlueprintedCompat. + @Unique + private static final boolean createschematichelper$BLUEPRINTED = ModList.get().isLoaded("create_blueprinted"); @Shadow private float lastChasingProgress; @@ -61,6 +75,8 @@ public abstract class SchematicTableScreenMixin extends AbstractSimiContainerScr private EditBox createschematichelper$urlField; @Unique private IconButton createschematichelper$modeButton; + @Unique + private IconButton createschematichelper$shareButton; public SchematicTableScreenMixin(SchematicTableMenu container, Inventory inv, Component title) { super(container, inv, title); @@ -86,13 +102,41 @@ public SchematicTableScreenMixin(SchematicTableMenu container, Inventory inv, Co }); this.addRenderableWidget(this.createschematichelper$urlField); - this.createschematichelper$modeButton = new IconButton(x + 208, y + 11, AllIcons.I_OPEN_FOLDER); + // Blueprinted anchors its column to topPos; Create's local y is topPos + 2, + // so the column positions must use topPos or the gaps come out uneven. + this.createschematichelper$modeButton = createschematichelper$BLUEPRINTED + ? BlueprintedCompat.createSmallButton(this.leftPos + 205, this.topPos + 52, new ScaledIcon(AllIcons.I_OPEN_FOLDER)) + : new IconButton(x + 208, y + 11, AllIcons.I_OPEN_FOLDER); this.createschematichelper$modeButton.withCallback(this::createschematichelper$toggleMode); this.addRenderableWidget(this.createschematichelper$modeButton); + this.createschematichelper$shareButton = null; + if (createschematichelper$BLUEPRINTED) { + IconButton blueprintedShare = BlueprintedCompat.findShareButton(this.renderables); + if (blueprintedShare != null) { + this.removeWidget(blueprintedShare); + this.createschematichelper$shareButton = BlueprintedCompat.createShareButton( + this.leftPos + 205, this.topPos + 18, this::createschematichelper$shareSelected); + this.addRenderableWidget(this.createschematichelper$shareButton); + } + } + this.createschematichelper$toggleMode(); } + @Unique + private void createschematichelper$shareSelected() { + if (this.schematicsArea == null) { + return; + } + List available = CreateClient.SCHEMATIC_SENDER.getAvailableSchematics(); + int index = this.schematicsArea.getState(); + if (index < 0 || index >= available.size()) { + return; + } + SchematicUploadHandler.shareSchematic(available.get(index).getString()); + } + @Inject( method = "lambda$init$0", at = @At("HEAD"), @@ -129,11 +173,18 @@ public SchematicTableScreenMixin(SchematicTableMenu container, Inventory inv, Co if (this.schematicsArea != null) { this.schematicsArea.visible = this.schematicsArea.active = localMode; } + if (createschematichelper$BLUEPRINTED) { + BlueprintedCompat.setShareButtonsVisible(this.renderables, localMode); + } + if (this.createschematichelper$shareButton != null) { + this.createschematichelper$shareButton.visible = this.createschematichelper$shareButton.active = localMode; + } this.createschematichelper$urlField.visible = this.createschematichelper$urlField.active = !localMode; this.createschematichelper$modeButton.setToolTip(localMode ? createschematichelper$DOWNLOAD_TOOLTIP : createschematichelper$LOCAL_TOOLTIP); - this.createschematichelper$modeButton.setIcon(localMode ? new DownloadIcon() : AllIcons.I_OPEN_FOLDER); + ScreenElement icon = localMode ? new DownloadIcon() : AllIcons.I_OPEN_FOLDER; + this.createschematichelper$modeButton.setIcon(createschematichelper$BLUEPRINTED ? new ScaledIcon(icon) : icon); } @ModifyConstant( @@ -141,7 +192,8 @@ public SchematicTableScreenMixin(SchematicTableMenu container, Inventory inv, Co constant = @Constant(intValue = 206) ) private int createschematichelper$patchRefreshButtonX(int x) { - return x + 2; + // Blueprinted removes and re-adds the refresh button itself, so leave Create's original alone. + return createschematichelper$BLUEPRINTED ? x : x + 2; } @ModifyConstant( @@ -149,7 +201,7 @@ public SchematicTableScreenMixin(SchematicTableMenu container, Inventory inv, Co constant = @Constant(intValue = 21, ordinal = 2) ) private int createschematichelper$patchRefreshButtonY(int y) { - return 32; + return createschematichelper$BLUEPRINTED ? y : 32; } @WrapOperation( @@ -175,8 +227,12 @@ public SchematicTableScreenMixin(SchematicTableMenu container, Inventory inv, Co ) ) private int createschematichelper$patchTitle(GuiGraphics instance, Font font, Component text, int x, int y, int color, boolean shadow, Operation original) { - if (this.createschematichelper$urlField.isVisible()) { - return original.call(instance, font, createschematichelper$PROCESSING_TITLE, x, y, color, shadow); + // Only replace the idle title; Create's own "Uploading..."/"Finished" states + // (text != this.title) stay visible in download mode too. + if (this.createschematichelper$urlField.isVisible() && text == this.title) { + // x was centered for the original text's width; re-center for ours + int adjustedX = x + (font.width(text) - font.width(createschematichelper$DOWNLOAD_TITLE)) / 2; + return original.call(instance, font, createschematichelper$DOWNLOAD_TITLE, adjustedX, y, color, shadow); } return original.call(instance, font, text, x, y, color, shadow); } diff --git a/neoforge/src/main/templates/META-INF/neoforge.mods.toml b/neoforge/src/main/templates/META-INF/neoforge.mods.toml index 7ee86e6..551744f 100644 --- a/neoforge/src/main/templates/META-INF/neoforge.mods.toml +++ b/neoforge/src/main/templates/META-INF/neoforge.mods.toml @@ -35,3 +35,10 @@ type = "required" versionRange = "[0.5.1,)" ordering = "AFTER" side = "CLIENT" + +[[dependencies."${mod_id}"]] +modId = "create_blueprinted" +type = "optional" +versionRange = "[2.0,)" +ordering = "AFTER" +side = "CLIENT"