feat: open schematic (.schem)
todo: fix exporting .schem
This commit is contained in:
parent
b89111d3c3
commit
d078bae747
3 changed files with 78 additions and 22 deletions
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue