Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Litematica support #2544

Closed
wants to merge 1 commit into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import baritone.api.schematic.format.ISchematicFormat;
import baritone.utils.schematic.format.defaults.MCEditSchematic;
import baritone.utils.schematic.format.defaults.SpongeSchematic;
import baritone.utils.schematic.format.defaults.LitematicaSchematic;
import net.minecraft.nbt.CompressedStreamTools;
import net.minecraft.nbt.NBTTagCompound;
import org.apache.commons.io.FilenameUtils;
Expand Down Expand Up @@ -65,6 +66,24 @@ public IStaticSchematic parse(InputStream input) throws IOException {
throw new UnsupportedOperationException("Unsupported Version of a Sponge Schematic");
}
}
},
/**
* The Litematica Schematic Specification. Commonly denoted by the ".litematic" file extension.
*
*/
LITEMATIC("litematic") {

@Override
public IStaticSchematic parse(InputStream input) throws IOException {
NBTTagCompound nbt = CompressedStreamTools.readCompressed(input);
int version = nbt.getInteger("Version");
switch (version) {
case 5:
return new LitematicaSchematic(nbt);
default:
throw new UnsupportedOperationException("Unsupported Version of a Litematica Schematic");
}
}
};

private final String extension;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/

package baritone.utils.schematic.format.defaults;

import baritone.utils.schematic.StaticSchematic;
import net.minecraft.block.*;
import net.minecraft.block.properties.IProperty;
import net.minecraft.nbt.*;
import net.minecraft.util.ResourceLocation;
import net.minecraft.block.state.IBlockState;

import org.apache.commons.lang3.Validate;
import javax.annotation.Nullable;
import java.util.*;

/**
* @author Emerson
* @since 12/27/2020
*/
public final class LitematicaSchematic extends StaticSchematic {

public LitematicaSchematic(NBTTagCompound nbt) {
String regionName = (String) nbt.getCompoundTag("Regions").getKeySet().toArray()[0];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not iterate over all regions?
They are all part of the schematic and the issues would just be spammed with "Baritone not loading some litematic regions"

Copy link
Author

@EmersonDove EmersonDove Mar 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I didn't do it because it involves reading the entire schematic into a larger volume. It makes more sense to add a region argument to the command? Going to add it to my initial issues list.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to just keep the sub-regions, just like CompositeSchematic does it with it's sub-schematics.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I'll look into that one too.

this.x = Math.abs(nbt.getCompoundTag("Regions").getCompoundTag(regionName).getCompoundTag("Size").getInteger("x"));
Copy link

@maruohon maruohon Feb 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The region size needs to be handled differently. The stored sizes per axis can be negative, if the "primary corner" (region origin) is not in the minimum corner of the region. (If they are negative, then they are at max -2, there should be no -1 values created by Litematica, as that is essentially the same as positive 1.)
image

I think for Baritone you can basically just use Math.abs() and then offset the origin if the values were negative for some axes. (No idea how you handle the positioning, and if you have rotations, but if you read everything into one large block container then it doesn't matter at this level anyway.) In Litematica this "freedom" in creating the selection with the primary corner in any relative corner causes quite some mess and complexity when handling the regions with rotations and mirroring etc.

Copy link
Collaborator

@ZacSharp ZacSharp Feb 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We always use the minimum corner for the position and don't have rotations, so (once we read more than the first region) just adding size + 1 and then removing the sign from size should work fine.

this.y = Math.abs(nbt.getCompoundTag("Regions").getCompoundTag(regionName).getCompoundTag("Size").getInteger("y"));
this.z = Math.abs(nbt.getCompoundTag("Regions").getCompoundTag(regionName).getCompoundTag("Size").getInteger("z"));
this.states = new IBlockState[this.x][this.z][this.y];


NBTTagList paletteTag = nbt.getCompoundTag("Regions").getCompoundTag(regionName).getTagList("BlockStatePalette",10);
// ListNBT paletteTag = nbt.getCompound("Regions").getCompound(regionName).getList("BlockStatePalette",10);

// Create the block states array
IBlockState[] paletteBlockStates = new IBlockState[paletteTag.tagCount()];
// For every part of the array
for (int i = 0; i<paletteTag.tagCount(); i++) {
// Set the default state by getting block name
Block block = Block.REGISTRY.getObject(new ResourceLocation((((NBTTagCompound) paletteTag.get(i)).getString("Name"))));
IBlockState blockState = block.getDefaultState();
NBTTagCompound properties = ((NBTTagCompound) paletteTag.get(i)).getCompoundTag("Properties");
Object[] keys = properties.getKeySet().toArray();
Map<String, String> propertiesMap = new HashMap<>();
// Create a map for each state
for (int j = 0; j<keys.length; j++) {
propertiesMap.put((String) keys[j], (properties.getString((String) keys[j])));
}
for (int j = 0; j<keys.length; j++) {
IProperty<?> property = block.getBlockState().getProperty(keys[j].toString());
if (property != null) {
blockState = setPropertyValue(blockState, property, propertiesMap.get(keys[j]));
}
}
paletteBlockStates[i] = blockState;
}


// BlockData is stored as an NBT long[]
int paletteSize = (int) Math.floor(log2(paletteTag.tagCount()))+1;
long litematicSize = (long) this.x*this.y*this.z;

// In 1.12, the long array isn't exposed by the libraries so parsing has to be done manually
String rawBlockString = (nbt.getCompoundTag("Regions").getCompoundTag(regionName)).getTag("BlockStates").toString();
rawBlockString = rawBlockString.substring(3,rawBlockString.length()-1);
String[] rawBlockArrayString = rawBlockString.split(",");
long[] rawBlockData = new long[rawBlockArrayString.length];
for (int i = 0; i < rawBlockArrayString.length; i++) {
rawBlockData[i] = Long.parseLong(rawBlockArrayString[i].substring(0,rawBlockArrayString[i].length()-1));
}
Comment on lines +76 to +83
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I hate parsing strings that aren't meant to be parsed, may I suggest using reflection or mixin to access the data attribute? I checked the litematica and malilib sources and the latter is what is used there.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes so that's the main reason why I have this as a draft pull because that iteration is so tacky. I'll look into those sources and try to find something.



LitematicaBitArray bitArray = new LitematicaBitArray(paletteSize, litematicSize, rawBlockData);
if (paletteSize > 32) {
throw new IllegalStateException("Too many blocks in schematic to handle");
}

int[] serializedBlockStates = new int[(int) litematicSize];
for (int i = 0; i<serializedBlockStates.length; i++) {
serializedBlockStates[i] = bitArray.getAt(i);
}

int counter = 0;
for (int y = 0; y < this.y; y++) {
for (int z = 0; z < this.z; z++) {
for (int x = 0; x < this.x; x++) {
IBlockState state = paletteBlockStates[serializedBlockStates[counter]];
this.states[x][z][y] = state;
counter++;
}
}
}
}
private static double log2(int N) {
return (Math.log(N) / Math.log(2));
}

private static <T extends Comparable<T>> IBlockState setPropertyValue(IBlockState state, IProperty<T> property, String value) {
Optional<T> parsed = property.parseValue(value).toJavaUtil();
if (parsed.isPresent()) {
return state.withProperty(property, parsed.get());
} else {
throw new IllegalArgumentException("Invalid value for property " + property);
}
}

/** LitematicaBitArray class from litematica */
private static class LitematicaBitArray
{
/** The long array that is used to store the data for this BitArray. */
private final long[] longArray;
/** Number of bits a single entry takes up */
private final int bitsPerEntry;
/**
* The maximum value for a single entry. This also works as a bitmask for a single entry.
* For instance, if bitsPerEntry were 5, this value would be 31 (ie, {@code 0b00011111}).
*/
private final long maxEntryValue;
/** Number of entries in this array (<b>not</b> the length of the long array that internally backs this array) */
private final long arraySize;

public LitematicaBitArray(int bitsPerEntryIn, long arraySizeIn, @Nullable long[] longArrayIn)
{
Validate.inclusiveBetween(1L, 32L, (long) bitsPerEntryIn);
this.arraySize = arraySizeIn;
this.bitsPerEntry = bitsPerEntryIn;
this.maxEntryValue = (1L << bitsPerEntryIn) - 1L;

if (longArrayIn != null)
{
this.longArray = longArrayIn;
}
else
{
this.longArray = new long[(int) (roundUp((long) arraySizeIn * (long) bitsPerEntryIn, 64L) / 64L)];
}
}

public void setAt(long index, int value)
{
Validate.inclusiveBetween(0L, this.arraySize - 1L, (long) index);
Validate.inclusiveBetween(0L, this.maxEntryValue, (long) value);
long startOffset = index * (long) this.bitsPerEntry;
int startArrIndex = (int) (startOffset >> 6); // startOffset / 64
int endArrIndex = (int) (((index + 1L) * (long) this.bitsPerEntry - 1L) >> 6);
int startBitOffset = (int) (startOffset & 0x3F); // startOffset % 64
this.longArray[startArrIndex] = this.longArray[startArrIndex] & ~(this.maxEntryValue << startBitOffset) | ((long) value & this.maxEntryValue) << startBitOffset;

if (startArrIndex != endArrIndex)
{
int endOffset = 64 - startBitOffset;
int j1 = this.bitsPerEntry - endOffset;
this.longArray[endArrIndex] = this.longArray[endArrIndex] >>> j1 << j1 | ((long) value & this.maxEntryValue) >> endOffset;
}
}

public int getAt(long index)
{
Validate.inclusiveBetween(0L, this.arraySize - 1L, (long) index);
long startOffset = index * (long) this.bitsPerEntry;
int startArrIndex = (int) (startOffset >> 6); // startOffset / 64
int endArrIndex = (int) (((index + 1L) * (long) this.bitsPerEntry - 1L) >> 6);
int startBitOffset = (int) (startOffset & 0x3F); // startOffset % 64

if (startArrIndex == endArrIndex)
{
return (int) (this.longArray[startArrIndex] >>> startBitOffset & this.maxEntryValue);
}
else
{
int endOffset = 64 - startBitOffset;
return (int) ((this.longArray[startArrIndex] >>> startBitOffset | this.longArray[endArrIndex] << endOffset) & this.maxEntryValue);
}
}


public long size()
{
return this.arraySize;
}

public static long roundUp(long number, long interval)
{
if (interval == 0)
{
return 0;
}
else if (number == 0)
{
return interval;
}
else
{
if (number < 0)
{
interval *= -1;
}

long i = number % interval;
return i == 0 ? number : number + interval - i;
}
}
Comment on lines +195 to +215
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public static long roundUp(long number, long interval)
{
if (interval == 0)
{
return 0;
}
else if (number == 0)
{
return interval;
}
else
{
if (number < 0)
{
interval *= -1;
}
long i = number % interval;
return i == 0 ? number : number + interval - i;
}
}
public static long roundUp(double number, long interval)
{
if (number < 0){
interval *= -1;
}
return interval * (long) Math.ceil(number/interval);
}

I believe that this has exactly the same functionality, if you have any reason for wanting to use this instead please do, however I think this is nicer,

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it will throw an exception instead of returning 0 if interval is 0 and if number is 0 it returns 0 instead of number.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe that interval is ever 0, the only occurrence I could found was that it was 64.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can test it. The nested class there is straight out of litematica, so I just left the code as-is.

}
}