From b226d236d4a5507fa2aef20484fa529cdc4e0dd7 Mon Sep 17 00:00:00 2001 From: uberswe Date: Tue, 25 Aug 2026 19:12:29 +0200 Subject: [PATCH] Use Blueprinted screenshots, drop 360 view, and fix release flow - Remove the 360 rotation render: always render just the 4 isometric views, drop the render360/frameCount/saveAllFrames config options and the rotation_images upload parts - Rework the Create: Blueprinted integration for the 2.2.2 ShareProvider API: keep Blueprinted's own share button, let it render the screenshot and upload it with the schematic via onRender's Future; the save-prompt flow also routes through Blueprinted's renderer when installed, falling back to our renderer otherwise - Register the share provider in FMLClientSetupEvent instead of the mod constructor to avoid load-order issues, and require Blueprinted 2.2.2+ - Bump mod_version to 2.2.0, make the release workflow commit the released version back to the branch, and build 1.21.1 releases from main instead of the stale mc/1.21.1 branch --- .github/workflows/release.yml | 20 +- README.md | 15 +- .../createschematichelper/ConfigValues.java | 3 - .../SchematicIsometricRenderer.java | 200 ++---------------- .../SchematicUploadHandler.java | 125 +++++++---- .../createschematichelper/lang/en_us.json | 2 - gradle.properties | 2 +- neoforge/build.gradle | 2 +- .../CreateSchematicHelperNeoForge.java | 21 +- .../neoforge/NeoForgeConfig.java | 14 +- .../neoforge/compat/BlueprintedCompat.java | 102 +++++---- .../mixin/SchematicTableScreenMixin.java | 34 +-- .../templates/META-INF/neoforge.mods.toml | 2 +- 13 files changed, 200 insertions(+), 342 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f72a61b..cd1c5b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,7 @@ jobs: game-version: '1.20.1' java: '17' forge-loaders: "forge\nneoforge" - - branch: mc/1.21.1 + - branch: main game-version: '1.21.1' java: '21' runs-on: ubuntu-latest @@ -187,6 +187,24 @@ jobs: files: ${{ steps.jars.outputs.fabric_jar }} + # Keep the checked-in mod_version in sync with what was just published, so + # local/dev builds on this branch are stamped with the released version. + - name: Commit version bump + if: github.event_name == 'push' || inputs.publish + run: | + version="${{ inputs.version || github.ref_name }}" + version="${version#v}" + sed -i "s/^mod_version=.*/mod_version=${version}/" gradle.properties + if git diff --quiet gradle.properties; then + echo "mod_version already ${version}, nothing to commit" + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add gradle.properties + git commit -m "Bump mod_version to ${version}" + git push origin "HEAD:${{ matrix.branch }}" + release: needs: build runs-on: ubuntu-latest diff --git a/README.md b/README.md index 1d4067a..a10adcd 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ ### Upload - **Automatic upload** - schematics are uploaded to [createmod.com](https://createmod.com) the moment you save them in-game -- **360° preview rendering** *(NeoForge 1.21.1+ only)* - 120 isometric frames are rendered client-side and uploaded alongside the schematic for an interactive 3D rotation view on the website +- **Preview rendering** *(NeoForge 1.21.1+ only)* - 4 isometric screenshots are rendered client-side and uploaded alongside the schematic; with [Create: Blueprinted](https://modrinth.com/mod/create-blueprinted) installed, its rendered schematic image is used instead - **Progress bar** *(NeoForge 1.21.1+ only)* - a live progress bar on the action bar tracks rendering, processing, and uploading stages - **One-click sharing** - a clickable link appears in chat so you (or anyone) can view the schematic in a browser - **Claim flow** - log in on the website to claim ownership, then publish to the community @@ -40,14 +40,14 @@ This is a **client-side only** mod. It does not need to be installed on the serv ## Supported Versions -| Branch | Minecraft | Loaders | Create | Java | 360° Viewer | -|--------|-----------|---------|--------|------|-------------| -| `main` / `mc/1.21.1` | 1.21.1 | NeoForge, Fabric | 6.0.10+ | 21 | Yes | +| Branch | Minecraft | Loaders | Create | Java | Preview Images | +|--------|-----------|---------|--------|------|----------------| +| `main` | 1.21.1 | NeoForge, Fabric | 6.0.10+ | 21 | Yes | | `mc/1.20.1` | 1.20.1 | Forge, NeoForge, Fabric | 6.0.8+ | 17 | No | | `mc/1.19.2` | 1.19.2 | Forge, Fabric | 0.5.1+ | 17 | No | | `mc/1.18.2` | 1.18.2 | Forge, Fabric | 0.5.1+ | 17 | No | -> **Note:** On 1.20.1 and below, schematics are uploaded without preview images. The 360° rotation viewer and progress bar are available on NeoForge 1.21.1+ only. +> **Note:** On 1.20.1 and below, schematics are uploaded without preview images. Preview rendering and the progress bar are available on NeoForge 1.21.1+ only. ## Installation @@ -67,13 +67,10 @@ This is a **client-side only** mod. It does not need to be installed on the serv | `enabled` | `true` | Enable or disable the upload feature entirely | | `autoUpload` | `true` | Upload schematics to createmod.com when saved. If `false` and local screenshot saving is enabled, only screenshots are generated | | `baseUrl` | `https://createmod.com` | API base URL | -| `render360` | `true` | Render 360° rotation view (disable for faster uploads with only 4 featured angles) *(1.21.1+ only)* | -| `frameCount` | `120` | Number of frames in the 360° rotation view (min 4, max 720) *(1.21.1+ only)* | | `aspectRatio` | `16:9` | Aspect ratio for rendered images (e.g. `16:9`, `4:3`, `1:1`) *(1.21.1+ only)* | | `overrideWidth` | `0` | Override image width in pixels (0 = automatic based on schematic size) *(1.21.1+ only)* | | `overrideHeight` | `0` | Override image height in pixels (0 = automatic based on schematic size) *(1.21.1+ only)* | -| `saveFeaturedFrames` | `false` | Save the 4 featured perspective images locally to the schematics folder *(1.21.1+ only)* | -| `saveAllFrames` | `false` | Save all rotation frames locally to the schematics folder *(1.21.1+ only)* | +| `saveFeaturedFrames` | `false` | Save the 4 perspective screenshots locally to the schematics folder *(1.21.1+ only)* | | `imageFormat` | `jpeg` | Image format for rendered frames: `jpeg` (smaller files, recommended) or `png` (lossless) *(1.21.1+ only)* | | `backgroundImage` | *(empty)* | Absolute path to a custom background image (JPEG or PNG). Leave empty to use the default blueprint grid. *(1.21.1+ only)* | | `promptBeforeUpload` | `true` | Show a clickable chat prompt before uploading or generating screenshots. If `false`, proceeds automatically *(1.21.1+ only)* | diff --git a/common/src/main/java/com/uberswe/createschematichelper/ConfigValues.java b/common/src/main/java/com/uberswe/createschematichelper/ConfigValues.java index c72527f..1d4638d 100644 --- a/common/src/main/java/com/uberswe/createschematichelper/ConfigValues.java +++ b/common/src/main/java/com/uberswe/createschematichelper/ConfigValues.java @@ -5,13 +5,10 @@ public class ConfigValues { public static boolean autoUpload = true; public static String baseUrl = "https://createmod.com"; public static String modVersion = "1.0.0"; - public static boolean render360 = true; - public static int frameCount = 120; public static String aspectRatio = "16:9"; public static int overrideWidth = 0; public static int overrideHeight = 0; public static boolean saveFeaturedFrames = false; - public static boolean saveAllFrames = false; public static String imageFormat = "jpeg"; public static String backgroundImage = ""; public static boolean promptBeforeUpload = true; diff --git a/common/src/main/java/com/uberswe/createschematichelper/SchematicIsometricRenderer.java b/common/src/main/java/com/uberswe/createschematichelper/SchematicIsometricRenderer.java index 202e510..49f2434 100644 --- a/common/src/main/java/com/uberswe/createschematichelper/SchematicIsometricRenderer.java +++ b/common/src/main/java/com/uberswe/createschematichelper/SchematicIsometricRenderer.java @@ -65,10 +65,8 @@ public class SchematicIsometricRenderer { private static final AtomicReference> activeRender = new AtomicReference<>(); private static final float ISOMETRIC_PITCH = 35.264f; - private static final float START_ANGLE = 45f; - private static final float[] FEATURED_ANGLES = {45f, 135f, 225f, 315f}; + private static final float[] VIEW_ANGLES = {45f, 135f, 225f, 315f}; - private static final int PIXELS_PER_BLOCK = 32; private static final int MAX_FB_SIZE = 4096; private static final int MIN_FB_SIZE = 768; private static final int TARGET_WIDTH = 1200; @@ -89,25 +87,25 @@ public interface ProgressCallback { // 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); + public static CompletableFuture> renderViews(Path nbtFile) { + return renderViews(nbtFile, NOOP); } - public static CompletableFuture> render360(Path nbtFile, ProgressCallback progress) { + public static CompletableFuture> renderViews(Path nbtFile, ProgressCallback progress) { CompletableFuture previous = activeRender.getAndSet(null); if (previous != null) { LOGGER.info("Cancelling previous render"); previous.cancel(false); } - LOGGER.info("Starting render360 for {}", nbtFile); + LOGGER.info("Starting view render for {}", nbtFile); CompletableFuture> future = CompletableFuture.supplyAsync(() -> { try (InputStream is = Files.newInputStream(nbtFile)) { return NbtIo.readCompressed(is, NbtAccounter.unlimitedHeap()); } catch (Exception e) { throw new RuntimeException("Failed to read NBT file: " + nbtFile, e); } - }).thenCompose(tag -> render360(tag, progress)); + }).thenCompose(tag -> renderViews(tag, progress)); activeRender.set(future); future.whenComplete((result, ex) -> activeRender.compareAndSet(future, null)); return future; @@ -240,19 +238,7 @@ private static PreRenderState prepareOffThread(CompoundTag tag) { LOGGER.info("Render setup: fb={}x{}, scale={}, fluids={}", fbW, fbH, scale, fluidPositions.size()); - float[] angles; - if (ConfigValues.render360) { - int frameCount = Math.max(4, ConfigValues.frameCount); - float degreesPerFrame = 360f / frameCount; - angles = new float[frameCount]; - for (int i = 0; i < frameCount; i++) { - angles[i] = START_ANGLE + i * degreesPerFrame; - } - } else { - angles = FEATURED_ANGLES; - } - - return new PreRenderState(schematicLevel, fluidPositions, size, angles, scale, fbW, fbH); + return new PreRenderState(schematicLevel, fluidPositions, size, VIEW_ANGLES, scale, fbW, fbH); } private static RenderState finalizeOnRenderThread(PreRenderState pre) { @@ -410,7 +396,7 @@ private static void renderFluids(RenderState state, PoseStack poseStack) { } } - public static CompletableFuture> render360(CompoundTag tag, ProgressCallback progress) { + public static CompletableFuture> renderViews(CompoundTag tag, ProgressCallback progress) { return CompletableFuture.supplyAsync(() -> prepareOffThread(tag)) .thenCompose(pre -> { CompletableFuture> renderFuture = new CompletableFuture<>(); @@ -430,20 +416,16 @@ public static CompletableFuture> render360(CompoundTag tag, .orTimeout(2, TimeUnit.MINUTES); } - public static CompletableFuture> render360(CompoundTag tag) { - return render360(tag, NOOP); + public static CompletableFuture> renderViews(CompoundTag tag) { + return renderViews(tag, NOOP); } private static List cropAndConvert(List rawImages, ProgressCallback progress) { - Set featuredIndices = computeFeaturedIndices(rawImages.size()); - int fbW = rawImages.isEmpty() ? 1 : rawImages.get(0).getWidth(); int fbH = rawImages.isEmpty() ? 1 : rawImages.get(0).getHeight(); int unionMinX = fbW, unionMinY = fbH, unionMaxX = -1, unionMaxY = -1; - for (int idx : featuredIndices) { - if (idx >= rawImages.size()) continue; - NativeImage raw = rawImages.get(idx); + for (NativeImage raw : rawImages) { int w = raw.getWidth(); int h = raw.getHeight(); for (int y = 0; y < h; y++) { @@ -497,7 +479,6 @@ private static List cropAndConvert(List rawImages, P progress.onProgress("processing", 0, rawImages.size()); List scaledImages = new ArrayList<>(); - List featuredFlags = new ArrayList<>(); for (int i = 0; i < rawImages.size(); i++) { NativeImage raw = rawImages.get(i); try (raw) { @@ -519,7 +500,6 @@ private static List cropAndConvert(List rawImages, P g2d.dispose(); scaledImages.add(output); - featuredFlags.add(featuredIndices.contains(i)); } catch (Exception e) { LOGGER.error("Failed to process frame {}", i, e); } @@ -527,13 +507,13 @@ private static List cropAndConvert(List rawImages, P } float quality = QUALITY_HIGH; - List result = encodeFrames(scaledImages, featuredFlags, format, ext, mimeType, quality); + List result = encodeFrames(scaledImages, format, ext, mimeType, quality); if (isJpeg && totalBytes(result) > MAX_TOTAL_IMAGE_BYTES) { LOGGER.info("Frames total {}MB at quality {}, re-encoding at {}", totalBytes(result) / (1024 * 1024), quality, QUALITY_LOW); quality = QUALITY_LOW; - result = encodeFrames(scaledImages, featuredFlags, format, ext, mimeType, quality); + result = encodeFrames(scaledImages, format, ext, mimeType, quality); } return result; @@ -543,20 +523,18 @@ private static long totalBytes(List frames) { long total = 0; for (RenderedFrame f : frames) { total += f.data().length; - if (f.featured()) total += f.data().length; } return total; } - private static List encodeFrames(List images, List featuredFlags, + private static List encodeFrames(List images, String format, String ext, String mimeType, float quality) { List result = new ArrayList<>(); for (int i = 0; i < images.size(); i++) { try { byte[] imageBytes = encodeImage(images.get(i), format, quality); - boolean featured = featuredFlags.get(i); - String filename = String.format(featured ? "frame_%03d_featured.%s" : "frame_%03d.%s", i, ext); - result.add(new RenderedFrame(filename, imageBytes, featured, mimeType)); + String filename = String.format("view_%02d.%s", i, ext); + result.add(new RenderedFrame(filename, imageBytes, mimeType)); } catch (Exception e) { LOGGER.error("Failed to encode frame {}", i, e); } @@ -564,20 +542,6 @@ private static List encodeFrames(List images, List return result; } - private static Set computeFeaturedIndices(int frameCount) { - if (frameCount <= FEATURED_ANGLES.length) { - Set all = new java.util.HashSet<>(); - for (int i = 0; i < frameCount; i++) all.add(i); - return all; - } - float degreesPerFrame = 360f / frameCount; - Set indices = new java.util.HashSet<>(); - for (float angle : FEATURED_ANGLES) { - indices.add(Math.round((angle - START_ANGLE) / degreesPerFrame)); - } - return indices; - } - private static int[] parseAspectRatio(String ratio) { if (ratio != null && ratio.contains(":")) { String[] parts = ratio.split(":"); @@ -590,79 +554,6 @@ private static int[] parseAspectRatio(String ratio) { return new int[]{16, 9}; } - private static NativeImage generateBlueprintBackground(int w, int h) { - NativeImage bg = new NativeImage(w, h, false); - - float cx = w / 2f; - float cy = h / 2f; - float maxDist = (float) Math.sqrt(cx * cx + cy * cy); - - int smallGrid = Math.max(4, w / 50); - int largeGrid = smallGrid * 5; - - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - float dx = x - cx; - float dy = y - cy; - float dist = (float) Math.sqrt(dx * dx + dy * dy) / maxDist; - float brightness = 1.0f - dist * 0.35f; - - int r = clamp8(Math.round(68 * brightness)); - int g = clamp8(Math.round(108 * brightness)); - int b = clamp8(Math.round(140 * brightness)); - - boolean onLargeGrid = (x % largeGrid == 0) || (y % largeGrid == 0); - boolean onSmallGrid = (x % smallGrid == 0) || (y % smallGrid == 0); - - if (onLargeGrid) { - r = clamp8(r + 45); - g = clamp8(g + 45); - b = clamp8(b + 45); - } else if (onSmallGrid) { - r = clamp8(r + 22); - g = clamp8(g + 22); - b = clamp8(b + 22); - } - - bg.setPixelRGBA(x, y, 0xFF000000 | (b << 16) | (g << 8) | r); - } - } - return bg; - } - - private static NativeImage compositeRegionOnBackground(NativeImage background, NativeImage source, - int srcX, int srcY, int cropW, int cropH, int padX, int padY, int bgW, int bgH) { - NativeImage result = new NativeImage(bgW, bgH, false); - for (int y = 0; y < bgH; y++) { - for (int x = 0; x < bgW; x++) { - result.setPixelRGBA(x, y, background.getPixelRGBA(x, y)); - } - } - - for (int y = 0; y < cropH; y++) { - for (int x = 0; x < cropW; x++) { - int pixel = source.getPixelRGBA(srcX + x, srcY + y); - int a = (pixel >> 24) & 0xFF; - if (a == 0) continue; - - int dx = padX + x; - int dy = padY + y; - if (a == 255) { - result.setPixelRGBA(dx, dy, pixel); - } else { - int bgPixel = result.getPixelRGBA(dx, dy); - int sr = pixel & 0xFF, sg = (pixel >> 8) & 0xFF, sb = (pixel >> 16) & 0xFF; - int dr = bgPixel & 0xFF, dg = (bgPixel >> 8) & 0xFF, db = (bgPixel >> 16) & 0xFF; - int or = sr + dr * (255 - a) / 255; - int og = sg + dg * (255 - a) / 255; - int ob = sb + db * (255 - a) / 255; - result.setPixelRGBA(dx, dy, 0xFF000000 | (ob << 16) | (og << 8) | or); - } - } - } - return result; - } - private static int clamp8(int v) { return Math.max(0, Math.min(255, v)); } @@ -740,63 +631,6 @@ private static BufferedImage nativeToBuffered(NativeImage source) { return result; } - private static BufferedImage extractCropToBuffered(NativeImage source, int srcX, int srcY, int w, int h) { - BufferedImage result = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - int srcW = source.getWidth(); - int srcH = source.getHeight(); - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - int sx = srcX + x; - int sy = srcY + y; - if (sx < 0 || sx >= srcW || sy < 0 || sy >= srcH) continue; - int pixel = source.getPixelRGBA(sx, sy); - int r = pixel & 0xFF; - int g = (pixel >> 8) & 0xFF; - int b = (pixel >> 16) & 0xFF; - int a = (pixel >> 24) & 0xFF; - result.setRGB(x, y, (a << 24) | (r << 16) | (g << 8) | b); - } - } - return result; - } - - private static BufferedImage toScaledBufferedImage(NativeImage image, boolean isJpeg) { - int w = image.getWidth(); - int h = image.getHeight(); - int imageType = isJpeg ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; - BufferedImage buffered = new BufferedImage(w, h, imageType); - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - int pixel = image.getPixelRGBA(x, y); - int r = pixel & 0xFF; - int g = (pixel >> 8) & 0xFF; - int b = (pixel >> 16) & 0xFF; - if (isJpeg) { - buffered.setRGB(x, y, (r << 16) | (g << 8) | b); - } else { - int a = (pixel >> 24) & 0xFF; - buffered.setRGB(x, y, (a << 24) | (r << 16) | (g << 8) | b); - } - } - } - - int targetW = TARGET_WIDTH; - int targetH = Math.round((float) TARGET_WIDTH * h / w); - if (targetW != w || targetH != h) { - BufferedImage scaled = new BufferedImage(targetW, targetH, imageType); - java.awt.Graphics2D g2d = scaled.createGraphics(); - g2d.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION, - java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2d.setRenderingHint(java.awt.RenderingHints.KEY_RENDERING, - java.awt.RenderingHints.VALUE_RENDER_QUALITY); - g2d.drawImage(buffered, 0, 0, targetW, targetH, null); - g2d.dispose(); - buffered = scaled; - } - - return buffered; - } - private static byte[] encodeImage(BufferedImage buffered, String format, float quality) throws Exception { if (!format.equals("jpeg")) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -815,5 +649,5 @@ private static byte[] encodeImage(BufferedImage buffered, String format, float q return baos.toByteArray(); } - public record RenderedFrame(String filename, byte[] data, boolean featured, String mimeType) {} + public record RenderedFrame(String filename, byte[] data, String mimeType) {} } diff --git a/common/src/main/java/com/uberswe/createschematichelper/SchematicUploadHandler.java b/common/src/main/java/com/uberswe/createschematichelper/SchematicUploadHandler.java index 8c40caf..3ecef89 100644 --- a/common/src/main/java/com/uberswe/createschematichelper/SchematicUploadHandler.java +++ b/common/src/main/java/com/uberswe/createschematichelper/SchematicUploadHandler.java @@ -14,6 +14,7 @@ import java.io.ByteArrayOutputStream; import java.net.URI; +import java.net.URL; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; @@ -31,10 +32,27 @@ public class SchematicUploadHandler { private static final int PROGRESS_BAR_LENGTH = 20; private static volatile Path pendingPath; + /** + * When Create: Blueprinted is installed, uploads are routed through its render + * pipeline instead of our own isometric renderer: the delegate kicks off + * Blueprinted's screenshot render, which hands the finished image back to + * {@link #uploadWithImage} via our ShareProvider. + */ + @FunctionalInterface + public interface BlueprintedShareDelegate { + void share(String schematicFileName); + } + + private static volatile BlueprintedShareDelegate blueprintedShare; + + public static void setBlueprintedShareDelegate(BlueprintedShareDelegate delegate) { + blueprintedShare = delegate; + } + public static void onSchematicSaved(Path filePath) { if (!ConfigValues.enabled) return; - boolean wantsSave = ConfigValues.saveFeaturedFrames || ConfigValues.saveAllFrames; + boolean wantsSave = ConfigValues.saveFeaturedFrames; if (ConfigValues.autoUpload) { if (ConfigValues.promptBeforeUpload) { @@ -94,7 +112,7 @@ private static void renderAsync(Path filePath) { .withStyle(ChatFormatting.GRAY)); sendProgressBar("Rendering", 0, 1); - SchematicIsometricRenderer.render360(filePath, (stage, current, total) -> { + SchematicIsometricRenderer.renderViews(filePath, (stage, current, total) -> { switch (stage) { case "rendering" -> sendProgressBar("Rendering", current, total); case "processing" -> sendProgressBar("Processing", current, total); @@ -120,14 +138,14 @@ 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. + * Entry point for our Create: Blueprinted ShareProvider. Blueprinted has already + * rendered its screenshot of the schematic; this uploads the schematic file with + * that image attached. Runs quietly on success — Blueprinted announces the + * resulting link itself — but reports specific failures in chat. * - * @return the destination base URL, or null if the schematic file could not be found + * @return a future completing with the uploaded schematic's URL, or exceptionally on failure */ - public static java.net.URL shareSchematic(String schematicName) { + public static CompletableFuture uploadWithImage(String schematicName, byte[] imageBytes) { Path schematicsDir = Minecraft.getInstance().gameDirectory.toPath().resolve("schematics"); Path filePath = schematicsDir.resolve(schematicName); if (!Files.exists(filePath) && !schematicName.endsWith(".nbt")) { @@ -137,26 +155,51 @@ public static java.net.URL shareSchematic(String schematicName) { LOGGER.error("Cannot share schematic, file not found: {}", schematicName); sendChatMessage(Component.translatable("createschematichelper.share.missing_file", schematicName) .withStyle(ChatFormatting.YELLOW)); - return null; + return CompletableFuture.failedFuture( + new IllegalArgumentException("Schematic file not found: " + schematicName)); } - uploadAsync(filePath); - - try { - return URI.create(ConfigValues.baseUrl).toURL(); - } catch (Exception e) { - return null; - } + Path resolvedPath = filePath; + List frames = imageBytes != null && imageBytes.length > 0 + ? List.of(new RenderedFrame("preview.png", imageBytes, "image/png")) + : List.of(); + return CompletableFuture.supplyAsync(() -> { + sendChatMessage(Component.translatable("createschematichelper.upload.private_note") + .withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC)); + URL url; + try { + url = upload(resolvedPath, frames, false); + } catch (Exception e) { + LOGGER.error("Failed to upload schematic", e); + throw new RuntimeException("Failed to upload schematic", e); + } + if (url == null) { + // The specific reason (too large, duplicate, server error) was already + // reported in chat by upload() + throw new RuntimeException("Schematic upload was rejected"); + } + return url; + }); } private static void uploadAsync(Path filePath) { + BlueprintedShareDelegate delegate = blueprintedShare; + if (delegate != null) { + try { + delegate.share(filePath.getFileName().toString()); + return; + } catch (Throwable t) { + LOGGER.error("Blueprinted share pipeline failed, falling back to built-in renderer", t); + } + } + 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) -> { + SchematicIsometricRenderer.renderViews(filePath, (stage, current, total) -> { switch (stage) { case "rendering" -> sendProgressBar("Rendering", current, total); case "processing" -> sendProgressBar("Processing", current, total); @@ -166,7 +209,7 @@ private static void uploadAsync(Path filePath) { try { saveFramesLocally(filePath, frames); sendProgressBar("Uploading", 0, 0); - upload(filePath, frames); + upload(filePath, frames, true); } catch (Exception e) { LOGGER.error("Failed to upload schematic", e); sendChatMessage(Component.translatable("createschematichelper.upload.failed") @@ -178,7 +221,7 @@ private static void uploadAsync(Path filePath) { CompletableFuture.runAsync(() -> { try { sendProgressBar("Uploading", 0, 0); - upload(filePath, List.of()); + upload(filePath, List.of(), true); } catch (Exception e) { LOGGER.error("Failed to upload schematic", e); sendChatMessage(Component.translatable("createschematichelper.upload.failed") @@ -189,12 +232,12 @@ private static void uploadAsync(Path filePath) { }); } - private static void upload(Path filePath, List frames) throws Exception { + private static URL upload(Path filePath, List frames, boolean announceSuccess) throws Exception { long fileSize = Files.size(filePath); if (fileSize > MAX_FILE_SIZE) { sendChatMessage(Component.translatable("createschematichelper.upload.too_large") .withStyle(ChatFormatting.RED)); - return; + return null; } String fileName = filePath.getFileName().toString(); @@ -205,7 +248,7 @@ private static void upload(Path filePath, List frames) throws Exc LOGGER.error("Schematic file is empty: {}", filePath); sendChatMessage(Component.translatable("createschematichelper.upload.failed") .withStyle(ChatFormatting.YELLOW)); - return; + return null; } HttpResponse response = sendUploadRequest(fileName, fileBytes, frames); @@ -217,7 +260,7 @@ private static void upload(Path filePath, List frames) throws Exc LOGGER.info("Retry response: HTTP {} — {}", response.statusCode(), response.body()); } - handleResponse(response, ConfigValues.baseUrl, frames.size()); + return handleResponse(response, ConfigValues.baseUrl, announceSuccess); } private static HttpResponse sendUploadRequest(String fileName, byte[] fileBytes, List frames) throws Exception { @@ -248,7 +291,7 @@ private static HttpResponse sendUploadRequest(String fileName, byte[] fi return client.send(request, HttpResponse.BodyHandlers.ofString()); } - private static void handleResponse(HttpResponse response, String baseUrl, int imageCount) { + private static URL handleResponse(HttpResponse response, String baseUrl, boolean announceSuccess) { int status = response.statusCode(); String body = response.body(); @@ -259,15 +302,18 @@ private static void handleResponse(HttpResponse response, String baseUrl String token = json.get("token").getAsString(); String url = baseUrl + "/u/" + token; - MutableComponent prefix = Component.translatable("createschematichelper.upload.success") - .withStyle(ChatFormatting.GREEN); - MutableComponent link = Component.literal(url) - .withStyle(style -> style - .withColor(ChatFormatting.AQUA) - .withUnderlined(true) - .withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url))); + if (announceSuccess) { + MutableComponent prefix = Component.translatable("createschematichelper.upload.success") + .withStyle(ChatFormatting.GREEN); + MutableComponent link = Component.literal(url) + .withStyle(style -> style + .withColor(ChatFormatting.AQUA) + .withUnderlined(true) + .withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url))); - sendChatMessage(prefix.append(link)); + sendChatMessage(prefix.append(link)); + } + return URI.create(url).toURL(); } else if (status == 409) { sendChatMessage(Component.translatable("createschematichelper.upload.already_exists") .withStyle(ChatFormatting.YELLOW)); @@ -282,6 +328,7 @@ private static void handleResponse(HttpResponse response, String baseUrl sendChatMessage(Component.translatable("createschematichelper.upload.failed") .withStyle(ChatFormatting.YELLOW)); } + return null; } private static byte[] buildMultipartBody(String boundary, String fileName, byte[] fileBytes, List frames) throws Exception { @@ -292,13 +339,7 @@ private static byte[] buildMultipartBody(String boundary, String fileName, byte[ writePart(baos, boundary, crlf, "file", safeFileName, "application/octet-stream", fileBytes); for (RenderedFrame frame : frames) { - if (frame.featured()) { - writePart(baos, boundary, crlf, "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()); - } + writePart(baos, boundary, crlf, "images", frame.filename(), frame.mimeType(), frame.data()); } baos.write(("--" + boundary + "--" + crlf).getBytes(StandardCharsets.UTF_8)); @@ -316,7 +357,7 @@ private static void writePart(ByteArrayOutputStream baos, String boundary, Strin } private static void saveFramesLocally(Path schematicPath, List frames) { - if (!ConfigValues.saveFeaturedFrames && !ConfigValues.saveAllFrames) return; + if (!ConfigValues.saveFeaturedFrames) return; try { String baseName = schematicPath.getFileName().toString().replaceFirst("\\.nbt$", ""); @@ -324,9 +365,7 @@ private static void saveFramesLocally(Path schematicPath, List fr Files.createDirectories(dir); for (RenderedFrame frame : frames) { - if (ConfigValues.saveAllFrames || (ConfigValues.saveFeaturedFrames && frame.featured())) { - Files.write(dir.resolve(frame.filename()), frame.data()); - } + Files.write(dir.resolve(frame.filename()), frame.data()); } LOGGER.info("Saved rendered frames to {}", dir); } catch (Exception e) { 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 bd524bc..7bd75b9 100644 --- a/common/src/main/resources/assets/createschematichelper/lang/en_us.json +++ b/common/src/main/resources/assets/createschematichelper/lang/en_us.json @@ -17,8 +17,6 @@ "createschematichelper.render.failed": "Failed to generate screenshots", "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.", diff --git a/gradle.properties b/gradle.properties index 605b3ee..a6d9ac2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,7 +35,7 @@ java_version=21 mod_id=createschematichelper mod_name=CreateSchematicHelper mod_license=MIT -mod_version=2.0.1 +mod_version=2.2.0 mod_group_id=com.uberswe mod_authors=uberswe, Jamalam mod_description=Upload and download Create mod schematics to and from createmod.com. diff --git a/neoforge/build.gradle b/neoforge/build.gradle index 51fe0ac..4593eaa 100644 --- a/neoforge/build.gradle +++ b/neoforge/build.gradle @@ -44,7 +44,7 @@ dependencies { } 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") + compileOnly("maven.modrinth:create-blueprinted:2.2.2+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 d70aa7d..cf4e4ad 100644 --- a/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/CreateSchematicHelperNeoForge.java +++ b/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/CreateSchematicHelperNeoForge.java @@ -10,6 +10,7 @@ import net.neoforged.fml.common.Mod; import net.neoforged.fml.config.ModConfig; import net.neoforged.fml.event.config.ModConfigEvent; +import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; import net.neoforged.neoforge.client.event.RegisterClientCommandsEvent; import net.neoforged.neoforge.common.NeoForge; import org.slf4j.Logger; @@ -41,13 +42,18 @@ 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); + // Registered in client setup rather than the mod constructor so Blueprinted has + // definitely finished constructing before we touch its registry, regardless of + // mod load order (see the load-order concern raised by the Blueprinted dev). + modEventBus.addListener((FMLClientSetupEvent event) -> event.enqueueWork(() -> { + 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)"); } @@ -64,13 +70,10 @@ private static void syncConfig() { ConfigValues.enabled = NeoForgeConfig.ENABLED.get(); ConfigValues.autoUpload = NeoForgeConfig.AUTO_UPLOAD.get(); ConfigValues.baseUrl = NeoForgeConfig.BASE_URL.get(); - ConfigValues.render360 = NeoForgeConfig.RENDER_360.get(); - ConfigValues.frameCount = NeoForgeConfig.FRAME_COUNT.get(); ConfigValues.aspectRatio = NeoForgeConfig.ASPECT_RATIO.get(); ConfigValues.overrideWidth = NeoForgeConfig.OVERRIDE_WIDTH.get(); ConfigValues.overrideHeight = NeoForgeConfig.OVERRIDE_HEIGHT.get(); ConfigValues.saveFeaturedFrames = NeoForgeConfig.SAVE_FEATURED_FRAMES.get(); - ConfigValues.saveAllFrames = NeoForgeConfig.SAVE_ALL_FRAMES.get(); ConfigValues.imageFormat = NeoForgeConfig.IMAGE_FORMAT.get(); ConfigValues.backgroundImage = NeoForgeConfig.BACKGROUND_IMAGE.get(); ConfigValues.promptBeforeUpload = NeoForgeConfig.PROMPT_BEFORE_UPLOAD.get(); diff --git a/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/NeoForgeConfig.java b/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/NeoForgeConfig.java index 01c4731..1540859 100644 --- a/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/NeoForgeConfig.java +++ b/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/NeoForgeConfig.java @@ -17,14 +17,6 @@ public class NeoForgeConfig { .comment("Base URL for the createmod.com API") .define("baseUrl", "https://createmod.com"); - public static final ModConfigSpec.BooleanValue RENDER_360 = BUILDER - .comment("Render 360° rotation view (disable for faster uploads with only 4 featured angles)") - .define("render360", true); - - public static final ModConfigSpec.IntValue FRAME_COUNT = BUILDER - .comment("Number of frames in the 360° rotation view (default 120, min 4)") - .defineInRange("frameCount", 120, 4, 720); - public static final ModConfigSpec.ConfigValue ASPECT_RATIO = BUILDER .comment("Aspect ratio for rendered images (e.g. '16:9', '4:3', '1:1')") .define("aspectRatio", "16:9"); @@ -38,13 +30,9 @@ public class NeoForgeConfig { .defineInRange("overrideHeight", 0, 0, 4096); public static final ModConfigSpec.BooleanValue SAVE_FEATURED_FRAMES = BUILDER - .comment("Save the 4 featured perspective images locally to the schematics folder") + .comment("Save the 4 perspective screenshots locally to the schematics folder") .define("saveFeaturedFrames", false); - public static final ModConfigSpec.BooleanValue SAVE_ALL_FRAMES = BUILDER - .comment("Save all rotation frames locally to the schematics folder") - .define("saveAllFrames", false); - public static final ModConfigSpec.ConfigValue IMAGE_FORMAT = BUILDER .comment("Image format for rendered frames: 'jpeg' (smaller files, recommended) or 'png' (lossless)") .define("imageFormat", "jpeg", o -> o instanceof String s && (s.equals("jpeg") || s.equals("png"))); 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 index 473303f..620fb32 100644 --- a/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/compat/BlueprintedCompat.java +++ b/neoforge/src/main/java/com/uberswe/createschematichelper/neoforge/compat/BlueprintedCompat.java @@ -6,34 +6,58 @@ import com.uberswe.createschematichelper.SchematicUploadHandler; import net.createmod.catnip.gui.element.ScreenElement; import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; 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.handler.SchematicImageHandler; import net.swzo.create_blueprinted.render.SchematicRenderSettings; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import java.net.URL; +import java.util.List; +import java.util.concurrent.Future; /** - * Integration with Create: Blueprinted's ShareProvider API. This class references + * Integration with Create: Blueprinted's ShareProvider API (2.2.2+). This class references * Blueprinted types directly and must only be classloaded when create_blueprinted is present. + * + * Blueprinted owns the render: its share pipeline produces the screenshot and hands the + * image bytes to {@link CreateModComShareProvider#onRender}, which uploads the schematic + * file together with that image and completes with the resulting createmod.com URL. + * Blueprinted then shows the clickable link in chat itself. */ public final class BlueprintedCompat { private static final Logger LOGGER = LogUtils.getLogger(); + private static final CreateModComShareProvider PROVIDER = new CreateModComShareProvider(); private BlueprintedCompat() {} public static void register() { - ShareProviderRegistry.register(new CreateModComShareProvider()); + ShareProviderRegistry.register(PROVIDER); + SchematicUploadHandler.setBlueprintedShareDelegate(BlueprintedCompat::share); LOGGER.info("Registered CreateMod.com share provider with Create: Blueprinted"); } + /** + * Routes the save-prompt upload through Blueprinted's render pipeline with our + * provider pinned, regardless of which provider the player has set active for + * the schematic table's share button. + */ + private static void share(String schematicFileName) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) { + throw new IllegalStateException("Cannot share a schematic without a player"); + } + new SchematicImageHandler(PROVIDER.id(), schematicFileName, + mc.player.createCommandSourceStack(), + SchematicRenderSettings.builder(), PROVIDER).share(); + } + /** * A 15px button matching Blueprinted's export/share/refresh column, so our mode * toggle can sit directly below them with the same look. @@ -43,49 +67,20 @@ public static IconButton createSmallButton(int x, int y, ScreenElement 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}). + * Toggles Blueprinted's export and share buttons 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. */ public static void setShareButtonsVisible(Iterable widgets, boolean visible) { for (Object widget : widgets) { if (widget instanceof ExportButton button) { button.visible = button.active = visible; + } else if (widget instanceof ShareButton 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() { @@ -105,11 +100,32 @@ public String destinationUrl() { } @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); + public Future onRender(ResourceLocation handlerId, String schematicName, + SchematicRenderSettings renderSettings, byte[] imageByteArray) { + return SchematicUploadHandler.uploadWithImage(schematicName, imageByteArray); + } + + @Override + public List extras() { + return List.of( + Component.translatable("text.createschematichelper.share_note1") + .withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC), + Component.translatable("text.createschematichelper.share_note2") + .withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC), + Component.translatable("text.createschematichelper.share_note3") + .withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC)); + } + + @Override + public boolean includeSchematicData() { + return true; + } + + @Override + public int timeout() { + // Blueprinted blocks a background thread on the upload future for this long; + // large schematics on slow connections need more than the 30s default + return 300; } } } 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 8fda9eb..94ab40c 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,6 @@ 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; @@ -35,10 +34,8 @@ import org.spongepowered.asm.mixin.injection.ModifyConstant; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -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. +// theirs and can toggle the export/share buttons it adds. @Mixin(value = SchematicTableScreen.class, priority = 1500) public abstract class SchematicTableScreenMixin extends AbstractSimiContainerScreen { @Unique @@ -75,8 +72,6 @@ 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); @@ -110,33 +105,9 @@ public SchematicTableScreenMixin(SchematicTableMenu container, Inventory inv, Co 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"), @@ -176,9 +147,6 @@ public SchematicTableScreenMixin(SchematicTableMenu container, Inventory inv, Co 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; diff --git a/neoforge/src/main/templates/META-INF/neoforge.mods.toml b/neoforge/src/main/templates/META-INF/neoforge.mods.toml index 551744f..918eb13 100644 --- a/neoforge/src/main/templates/META-INF/neoforge.mods.toml +++ b/neoforge/src/main/templates/META-INF/neoforge.mods.toml @@ -39,6 +39,6 @@ side = "CLIENT" [[dependencies."${mod_id}"]] modId = "create_blueprinted" type = "optional" -versionRange = "[2.0,)" +versionRange = "[2.2.2,)" ordering = "AFTER" side = "CLIENT"