feat: open schematic (.schem)

todo: fix exporting .schem
This commit is contained in:
trafficlunar 2025-01-11 20:54:54 +00:00
parent b89111d3c3
commit d078bae747
3 changed files with 78 additions and 22 deletions

View file

@ -13,7 +13,7 @@ import { Button } from "@/components/ui/button";
import _blockData from "@/data/blocks/data.json"; import _blockData from "@/data/blocks/data.json";
const blockData: BlockData = _blockData; const blockData: BlockData = _blockData;
interface BlockPalette extends nbt.CompoundTagLike { interface LitematicaBlockPalette extends nbt.CompoundTagLike {
Name: string; Name: string;
Properties?: Record<string, string>; Properties?: Record<string, string>;
} }
@ -21,13 +21,24 @@ interface BlockPalette extends nbt.CompoundTagLike {
interface LitematicNBT extends nbt.ListTagLike { interface LitematicNBT extends nbt.ListTagLike {
Regions: { Regions: {
Image: { Image: {
BlockStatePalette: BlockPalette[]; BlockStatePalette: LitematicaBlockPalette[];
BlockStates: BigInt64Array; BlockStates: BigInt64Array;
Size: { x: number; y: number; z: number }; Size: { x: number; y: number; z: number };
}; };
}; };
} }
interface SpongeNBT extends nbt.ListTagLike {
Schematic: {
Blocks: {
Data: Int8Array;
Palette: Record<string, number>;
};
Width: number;
Height: number;
};
}
function OpenSchematic({ close }: DialogProps) { function OpenSchematic({ close }: DialogProps) {
const { setBlocks } = useContext(CanvasContext); const { setBlocks } = useContext(CanvasContext);
const { setLoading } = useContext(LoadingContext); const { setLoading } = useContext(LoadingContext);
@ -50,17 +61,16 @@ function OpenSchematic({ close }: DialogProps) {
const data = await nbt.read(bytes); const data = await nbt.read(bytes);
if (fileExtension == "litematic") { if (fileExtension == "litematic") {
const litematicData = (data as nbt.NBTData<LitematicNBT>).data; const litematicData = (data as nbt.NBTData<LitematicNBT>).data.Regions.Image;
const imageRegion = litematicData.Regions.Image;
// todo: set version // 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 getPaletteIndex = (index: number): bigint => {
const originalY = Math.floor(index / imageRegion.Size.x); const originalY = Math.floor(index / litematicData.Size.x);
const originalX = index % imageRegion.Size.x; const originalX = index % litematicData.Size.x;
const reversedY = imageRegion.Size.y - 1 - originalY; const reversedY = litematicData.Size.y - 1 - originalY;
const reversedIndex = reversedY * imageRegion.Size.x + originalX; const reversedIndex = reversedY * litematicData.Size.x + originalX;
// getAt() implementation - LitematicaBitArray.java // getAt() implementation - LitematicaBitArray.java
const startOffset = reversedIndex * requiredBits; const startOffset = reversedIndex * requiredBits;
@ -70,11 +80,11 @@ function OpenSchematic({ close }: DialogProps) {
const mask = (1 << requiredBits) - 1; const mask = (1 << requiredBits) - 1;
if (startArrayIndex === endArrayIndex) { if (startArrayIndex === endArrayIndex) {
return (imageRegion.BlockStates[startArrayIndex] >> BigInt(bitOffset)) & BigInt(mask); return (litematicData.BlockStates[startArrayIndex] >> BigInt(bitOffset)) & BigInt(mask);
} else { } else {
const endOffset = 64 - bitOffset; const endOffset = 64 - bitOffset;
return ( return (
((imageRegion.BlockStates[startArrayIndex] >> BigInt(bitOffset)) | (imageRegion.BlockStates[endArrayIndex] << BigInt(endOffset))) & ((litematicData.BlockStates[startArrayIndex] >> BigInt(bitOffset)) | (litematicData.BlockStates[endArrayIndex] << BigInt(endOffset))) &
BigInt(mask) BigInt(mask)
); );
} }
@ -84,10 +94,11 @@ function OpenSchematic({ close }: DialogProps) {
const blocks: Block[] = []; const blocks: Block[] = [];
let index = 0; let index = 0;
for (let y = 0; y < imageRegion.Size.y; y++) { for (let y = 0; y < litematicData.Size.y; y++) {
for (let x = 0; x < imageRegion.Size.x; x++) { for (let x = 0; x < litematicData.Size.x; x++) {
const paletteIndex = Number(getPaletteIndex(index)); const paletteIndex = Number(getPaletteIndex(index));
const paletteBlock = imageRegion.BlockStatePalette.at(paletteIndex); console.log(paletteIndex);
const paletteBlock = litematicData.BlockStatePalette[paletteIndex];
index++; index++;
if (!paletteBlock) continue; if (!paletteBlock) continue;
@ -101,9 +112,14 @@ function OpenSchematic({ close }: DialogProps) {
const paletteProperties = paletteBlock.Properties; const paletteProperties = paletteBlock.Properties;
const dataProperties = blockData[name].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) { 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; if (!dataProperties) continue;
// Check if the schematic and data properties are the same
if (!Object.keys(paletteProperties).every((key) => paletteProperties[key] === dataProperties[key])) { if (!Object.keys(paletteProperties).every((key) => paletteProperties[key] === dataProperties[key])) {
continue; continue;
} }
@ -115,6 +131,50 @@ function OpenSchematic({ close }: DialogProps) {
} }
} }
setBlocks(blocks);
} else if (fileExtension == "schem") {
const spongeData = (data as nbt.NBTData<SpongeNBT>).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); setBlocks(blocks);
} }
} }

View file

@ -133,10 +133,8 @@ function SaveLitematic({ close }: DialogProps) {
}; };
// Write to file // Write to file
const bytes = await nbt.write(data); const bytes = await nbt.write(data, { compression: "gzip" });
const compressed = await nbt.compress(bytes, "gzip"); const blob = new Blob([bytes], { type: "application/x-gzip" });
const blob = new Blob([compressed], { type: "application/x-gzip" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
setLoading(false); setLoading(false);

View file

@ -102,10 +102,8 @@ function SaveLitematic({ close }: DialogProps) {
}; };
// Write to file // Write to file
const bytes = await nbt.write(data); const bytes = await nbt.write(data, { compression: "gzip" });
const compressed = await nbt.compress(bytes, "gzip"); const blob = new Blob([bytes], { type: "application/x-gzip" });
const blob = new Blob([compressed], { type: "application/x-gzip" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
setLoading(false); setLoading(false);