From d078bae7477e016528562d40c0e447453466b70b Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Sat, 11 Jan 2025 20:54:54 +0000 Subject: [PATCH] feat: open schematic (.schem) todo: fix exporting .schem --- src/components/dialogs/OpenSchematic.tsx | 88 ++++++++++++++++++++---- src/components/dialogs/SaveLitematic.tsx | 6 +- src/components/dialogs/SaveSchem.tsx | 6 +- 3 files changed, 78 insertions(+), 22 deletions(-) diff --git a/src/components/dialogs/OpenSchematic.tsx b/src/components/dialogs/OpenSchematic.tsx index fe36ca7..91813e8 100644 --- a/src/components/dialogs/OpenSchematic.tsx +++ b/src/components/dialogs/OpenSchematic.tsx @@ -13,7 +13,7 @@ import { Button } from "@/components/ui/button"; import _blockData from "@/data/blocks/data.json"; const blockData: BlockData = _blockData; -interface BlockPalette extends nbt.CompoundTagLike { +interface LitematicaBlockPalette extends nbt.CompoundTagLike { Name: string; Properties?: Record; } @@ -21,13 +21,24 @@ interface BlockPalette extends nbt.CompoundTagLike { interface LitematicNBT extends nbt.ListTagLike { Regions: { Image: { - BlockStatePalette: BlockPalette[]; + BlockStatePalette: LitematicaBlockPalette[]; BlockStates: BigInt64Array; Size: { x: number; y: number; z: number }; }; }; } +interface SpongeNBT extends nbt.ListTagLike { + Schematic: { + Blocks: { + Data: Int8Array; + Palette: Record; + }; + Width: number; + Height: number; + }; +} + function OpenSchematic({ close }: DialogProps) { const { setBlocks } = useContext(CanvasContext); const { setLoading } = useContext(LoadingContext); @@ -50,17 +61,16 @@ function OpenSchematic({ close }: DialogProps) { const data = await nbt.read(bytes); if (fileExtension == "litematic") { - const litematicData = (data as nbt.NBTData).data; - const imageRegion = litematicData.Regions.Image; + const litematicData = (data as nbt.NBTData).data.Regions.Image; // todo: set version - const requiredBits = Math.max(Math.ceil(Math.log2(imageRegion.BlockStatePalette.length)), 2); + const requiredBits = Math.max(Math.ceil(Math.log2(litematicData.BlockStatePalette.length)), 2); const getPaletteIndex = (index: number): bigint => { - const originalY = Math.floor(index / imageRegion.Size.x); - const originalX = index % imageRegion.Size.x; - const reversedY = imageRegion.Size.y - 1 - originalY; - const reversedIndex = reversedY * imageRegion.Size.x + originalX; + const originalY = Math.floor(index / litematicData.Size.x); + const originalX = index % litematicData.Size.x; + const reversedY = litematicData.Size.y - 1 - originalY; + const reversedIndex = reversedY * litematicData.Size.x + originalX; // getAt() implementation - LitematicaBitArray.java const startOffset = reversedIndex * requiredBits; @@ -70,11 +80,11 @@ function OpenSchematic({ close }: DialogProps) { const mask = (1 << requiredBits) - 1; if (startArrayIndex === endArrayIndex) { - return (imageRegion.BlockStates[startArrayIndex] >> BigInt(bitOffset)) & BigInt(mask); + return (litematicData.BlockStates[startArrayIndex] >> BigInt(bitOffset)) & BigInt(mask); } else { const endOffset = 64 - bitOffset; return ( - ((imageRegion.BlockStates[startArrayIndex] >> BigInt(bitOffset)) | (imageRegion.BlockStates[endArrayIndex] << BigInt(endOffset))) & + ((litematicData.BlockStates[startArrayIndex] >> BigInt(bitOffset)) | (litematicData.BlockStates[endArrayIndex] << BigInt(endOffset))) & BigInt(mask) ); } @@ -84,10 +94,11 @@ function OpenSchematic({ close }: DialogProps) { const blocks: Block[] = []; let index = 0; - for (let y = 0; y < imageRegion.Size.y; y++) { - for (let x = 0; x < imageRegion.Size.x; x++) { + for (let y = 0; y < litematicData.Size.y; y++) { + for (let x = 0; x < litematicData.Size.x; x++) { const paletteIndex = Number(getPaletteIndex(index)); - const paletteBlock = imageRegion.BlockStatePalette.at(paletteIndex); + console.log(paletteIndex); + const paletteBlock = litematicData.BlockStatePalette[paletteIndex]; index++; if (!paletteBlock) continue; @@ -101,9 +112,14 @@ function OpenSchematic({ close }: DialogProps) { const paletteProperties = paletteBlock.Properties; const dataProperties = blockData[name].properties; + // The schematic doesn't explicitly provide the texture name, thus we have to figure it out by checking the block's properties if (paletteProperties) { + // Check if the properties in the block data exist + // If not, that means the block we're looking for has an extra word + // For example, pale_oak_log can have no properties but pale_oak_log_top does if (!dataProperties) continue; + // Check if the schematic and data properties are the same if (!Object.keys(paletteProperties).every((key) => paletteProperties[key] === dataProperties[key])) { continue; } @@ -115,6 +131,50 @@ function OpenSchematic({ close }: DialogProps) { } } + setBlocks(blocks); + } else if (fileExtension == "schem") { + const spongeData = (data as nbt.NBTData).data.Schematic; + // todo: set version + + // Add every block to the canvas + const blocks: Block[] = []; + let index = 0; + + for (let y = spongeData.Height; y > 0; y--) { + for (let x = 0; x < spongeData.Width; x++) { + const paletteValue = spongeData.Blocks.Data[index]; + const paletteBlock = Object.keys(spongeData.Blocks.Palette).find((key) => spongeData.Blocks.Palette[key] == paletteValue); + + index++; + if (!paletteBlock) continue; + + const blockIdWithProperties = paletteBlock.replace("minecraft:", ""); + if (blockIdWithProperties == "air") continue; + + // Split up block ID and properties + const [blockId, propertiesString] = blockIdWithProperties.split(/\[(.+)\]/); + const properties = propertiesString ? Object.fromEntries(propertiesString.split(",").map((pair) => pair.split("="))) : null; + + for (const name in blockData) { + if (blockData[name].id !== blockId) continue; + + const dataProperties = blockData[name].properties; + + // See .litematic function above for how this works + if (properties) { + if (!dataProperties) continue; + + if (!Object.keys(properties).every((key) => properties[key] === dataProperties[key])) { + continue; + } + } + + blocks.push({ x, y, name }); + break; + } + } + } + setBlocks(blocks); } } diff --git a/src/components/dialogs/SaveLitematic.tsx b/src/components/dialogs/SaveLitematic.tsx index cff8b2f..8ce9204 100644 --- a/src/components/dialogs/SaveLitematic.tsx +++ b/src/components/dialogs/SaveLitematic.tsx @@ -133,10 +133,8 @@ function SaveLitematic({ close }: DialogProps) { }; // Write to file - const bytes = await nbt.write(data); - const compressed = await nbt.compress(bytes, "gzip"); - - const blob = new Blob([compressed], { type: "application/x-gzip" }); + const bytes = await nbt.write(data, { compression: "gzip" }); + const blob = new Blob([bytes], { type: "application/x-gzip" }); const url = URL.createObjectURL(blob); setLoading(false); diff --git a/src/components/dialogs/SaveSchem.tsx b/src/components/dialogs/SaveSchem.tsx index 1d9220d..5683af6 100644 --- a/src/components/dialogs/SaveSchem.tsx +++ b/src/components/dialogs/SaveSchem.tsx @@ -102,10 +102,8 @@ function SaveLitematic({ close }: DialogProps) { }; // Write to file - const bytes = await nbt.write(data); - const compressed = await nbt.compress(bytes, "gzip"); - - const blob = new Blob([compressed], { type: "application/x-gzip" }); + const bytes = await nbt.write(data, { compression: "gzip" }); + const blob = new Blob([bytes], { type: "application/x-gzip" }); const url = URL.createObjectURL(blob); setLoading(false);