Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions buildSrc/src/main/groovy/multiloader-common.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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<BlockEntityType<?>> FAILED_BE_TYPES = ConcurrentHashMap.newKeySet();

public static CompletableFuture<List<RenderedFrame>> render360(Path nbtFile) {
return render360(nbtFile, NOOP);
}
Expand Down Expand Up @@ -100,6 +118,9 @@ private record RenderState(
RenderTarget renderTarget,
SchematicRenderer renderer,
SuperRenderTypeBuffer buffers,
SchematicLevel schematicLevel,
List<BlockPos> fluidPositions,
PoseAppliedVertexConsumer fluidConsumer,
float[] angles,
Vec3i size,
float scale,
Expand All @@ -109,20 +130,52 @@ private record RenderState(

private record PreRenderState(
SchematicLevel schematicLevel,
List<BlockPos> fluidPositions,
Vec3i size,
float[] angles,
float scale,
int fbW,
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<BlockPos> 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();

if (mc.level == null) {
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());
Expand Down Expand Up @@ -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<BlockPos> 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) {
Expand All @@ -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) {
Expand All @@ -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<NativeImage> images,
Expand Down Expand Up @@ -265,6 +327,8 @@ private static void renderBatch(RenderState state, int startIndex, List<NativeIm
);

state.renderer.render(poseStack, state.buffers);
renderFluids(state, poseStack);
renderBlockEntities(state, poseStack);
state.buffers.draw();
poseStack.popPose();

Expand All @@ -291,6 +355,61 @@ private static void renderBatch(RenderState state, int startIndex, List<NativeIm
}
}

// Belts, tank fluid, funnel flaps and similar Create visuals only exist as block-entity
// renderers; the batched block pass never draws them. The Flywheel fast paths turn
// themselves off for non-client levels, so these all take their vanilla render path here.
private static void renderBlockEntities(RenderState state, PoseStack poseStack) {
BlockEntityRenderDispatcher dispatcher = state.mc.getBlockEntityRenderDispatcher();
for (BlockEntity be : state.schematicLevel.getRenderedBlockEntities()) {
if (FAILED_BE_TYPES.contains(be.getType())) {
continue;
}
BlockEntityRenderer<BlockEntity> 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<List<RenderedFrame>> render360(CompoundTag tag, ProgressCallback progress) {
return CompletableFuture.supplyAsync(() -> prepareOffThread(tag))
.thenCompose(pre -> {
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) -> {
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down
Loading
Loading