Created
December 31, 2021 20:36
-
-
Save CutestNekoAqua/d0325fb4bec3fe0edb1b6c2a4af625e0 to your computer and use it in GitHub Desktop.
Schematic Loading in Minestom
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package me.hugo.kweebecparty.schem; | |
import net.minestom.server.coordinate.Point; | |
import org.jetbrains.annotations.NotNull; | |
public interface ISchematic { | |
ErrorMessage read(); | |
ErrorMessage write(@NotNull Region region); | |
ErrorMessage build(@NotNull Point position, Runnable blocksCompleted); | |
enum ErrorMessage { | |
NoSuchFile, | |
NotLoaded, | |
NBTName, | |
NBTWidth, | |
NBTHeight, | |
NBTLength, | |
NBTMaxPalette, | |
NBTPalette, | |
PaletteNotEqualsMaxPalette, | |
PaletteGetInt, | |
NBTBlockData, | |
NBTEntities, | |
BadRead, | |
BadWrite, | |
VarIntSize, | |
NoBlocks, | |
BadMaterials, | |
Instance, | |
BlockBatch, | |
None | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package me.hugo.kweebecparty.schem; | |
/** | |
* Code based on https://github.com/sejtam-dev/MineSchem | |
* Licensed under Apache v2 | |
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package me.hugo.kweebecparty.schem; | |
import net.minestom.server.MinecraftServer; | |
import net.minestom.server.coordinate.Point; | |
import net.minestom.server.coordinate.Pos; | |
import net.minestom.server.instance.Instance; | |
import net.minestom.server.instance.block.Block; | |
import org.jetbrains.annotations.NotNull; | |
import java.util.*; | |
public class Region implements Iterable<Region.RegionBlock>, Cloneable { | |
private Point pos1; | |
private Point pos2; | |
private Instance instance; | |
public Region(@NotNull Point position1, @NotNull Point position2) { | |
this(position1, position2, new ArrayList<>(MinecraftServer.getInstanceManager().getInstances()).get(0)); | |
} | |
public Region(@NotNull Point position1, @NotNull Point position2, @NotNull Instance instance) { | |
this.pos1 = new Pos(Math.min(position1.x(), position2.x()), Math.min(position1.y(), position2.y()), Math.min(position1.z(), position2.z())); | |
this.pos2 = new Pos(Math.max(position1.x(), position2.x()), Math.max(position1.y(), position2.y()), Math.max(position1.z(), position2.z())); | |
this.instance = instance; | |
} | |
// Width | |
public double getSizeX() { | |
return (this.pos2.x() - this.pos1.x()) + 1; | |
} | |
// Height | |
public double getSizeY() { | |
return (this.pos2.y() - this.pos1.y()) + 1; | |
} | |
// Length | |
public double getSizeZ() { | |
return (this.pos2.z() - this.pos1.z()) + 1; | |
} | |
public double getLowerX() { | |
return this.pos1.x(); | |
} | |
public double getLowerY() { | |
return this.pos1.y(); | |
} | |
public double getLowerZ() { | |
return this.pos1.z(); | |
} | |
public Point getLower() { | |
return new Pos(this.pos1.x(), this.pos1.y(), this.pos1.z()); | |
} | |
public double getUpperX() { | |
return this.pos2.x(); | |
} | |
public double getUpperY() { | |
return this.pos2.y(); | |
} | |
public double getUpperZ() { | |
return this.pos2.z(); | |
} | |
public Point getUpper() { | |
return new Pos(this.pos2.x(), this.pos2.y(), this.pos2.z()); | |
} | |
public short[] cornerBlocksStateId() { | |
short[] corners = new short[8]; | |
corners[0] = instance.getBlock((int) this.pos1.x(), (int) this.pos1.y(), (int) this.pos1.z()).stateId(); | |
corners[1] = instance.getBlock((int) this.pos1.x(), (int) this.pos1.y(), (int) this.pos2.z()).stateId(); | |
corners[2] = instance.getBlock((int) this.pos1.x(), (int) this.pos2.y(), (int) this.pos1.z()).stateId(); | |
corners[3] = instance.getBlock((int) this.pos1.x(), (int) this.pos2.y(), (int) this.pos2.z()).stateId(); | |
corners[4] = instance.getBlock((int) this.pos2.x(), (int) this.pos1.y(), (int) this.pos1.z()).stateId(); | |
corners[5] = instance.getBlock((int) this.pos2.x(), (int) this.pos1.y(), (int) this.pos2.z()).stateId(); | |
corners[6] = instance.getBlock((int) this.pos2.x(), (int) this.pos2.y(), (int) this.pos1.z()).stateId(); | |
corners[7] = instance.getBlock((int) this.pos2.x(), (int) this.pos2.y(), (int) this.pos2.z()).stateId(); | |
return corners; | |
} | |
public Point[] cornerBlocksPosition() { | |
Point[] corners = new Point[8]; | |
corners[0] = new Pos(this.pos1.x(), this.pos1.y(), this.pos1.z()); | |
corners[1] = new Pos(this.pos1.x(), this.pos1.y(), this.pos2.z()); | |
corners[2] = new Pos(this.pos1.x(), this.pos2.y(), this.pos1.z()); | |
corners[3] = new Pos(this.pos1.x(), this.pos2.y(), this.pos2.z()); | |
corners[4] = new Pos(this.pos2.x(), this.pos1.y(), this.pos1.z()); | |
corners[5] = new Pos(this.pos2.x(), this.pos1.y(), this.pos2.z()); | |
corners[6] = new Pos(this.pos2.x(), this.pos2.y(), this.pos1.z()); | |
corners[7] = new Pos(this.pos2.x(), this.pos2.y(), this.pos2.z()); | |
return corners; | |
} | |
public Iterator<RegionBlock> iterator() { | |
return new RegionIterator((int) this.pos1.x(), (int) this.pos1.y(), (int) pos1.z(), (int) this.pos2.x(), (int) pos2.y(), (int) pos2.z(), this.instance); | |
} | |
public List<Short> getStateIds() { | |
Iterator<RegionBlock> iterator = this.iterator(); | |
List<Short> list = new ArrayList<>(); | |
while (iterator.hasNext()) | |
list.add(iterator.next().getStateId()); | |
return list; | |
} | |
public Map<Point, Short> getStateIdsMap() { | |
Iterator<RegionBlock> iterator = this.iterator(); | |
Map<Point, Short> map = new HashMap<>(); | |
while (iterator.hasNext()) { | |
RegionBlock block = iterator.next(); | |
map.put(block.getPosition(), block.getStateId()); | |
} | |
return map; | |
} | |
public List<Block> getBlocks() { | |
Iterator<RegionBlock> iterator = this.iterator(); | |
List<Block> list = new ArrayList<>(); | |
while (iterator.hasNext()) | |
list.add(Block.fromStateId(iterator.next().getStateId())); | |
return list; | |
} | |
public Map<Point, Block> getBlocksMap() { | |
Iterator<RegionBlock> iterator = this.iterator(); | |
Map<Point, Block> map = new HashMap<>(); | |
while (iterator.hasNext()) { | |
RegionBlock block = iterator.next(); | |
map.put(block.getPosition(), Block.fromStateId(block.getStateId())); | |
} | |
return map; | |
} | |
public Instance getInstance() { | |
return this.instance; | |
} | |
public void setInstance(@NotNull Instance instance) { | |
this.instance = instance; | |
} | |
public String toString() { | |
return "{Pos1:" + pos1.toString() + ", Pos2:" + pos2.toString() + "}"; | |
} | |
public class RegionIterator implements Iterator<RegionBlock> { | |
private Instance instance; | |
private int baseX, baseY, baseZ; | |
private int x, y, z; | |
private int sizeX, sizeY, sizeZ; | |
public RegionIterator(int x1, int y1, int z1, int x2, int y2, int z2, @NotNull Instance instance) { | |
this.instance = instance; | |
this.baseX = x1; | |
this.baseY = y1; | |
this.baseZ = z1; | |
this.sizeX = Math.abs(x2 - x1) + 1; | |
this.sizeY = Math.abs(y2 - y1) + 1; | |
this.sizeZ = Math.abs(z2 - z1) + 1; | |
this.x = this.y = this.z = 0; | |
} | |
public boolean hasNext() { | |
return this.x < this.sizeX && this.y < this.sizeY && this.z < this.sizeZ; | |
} | |
public RegionBlock next() { | |
Point blockPosition = new Pos(this.baseX + this.x, this.baseY + this.y, this.baseZ + this.z); | |
short state = this.instance.getBlock(blockPosition).stateId(); | |
RegionBlock regionBlock = new RegionBlock(blockPosition, state); | |
if (++x >= this.sizeX) { | |
this.x = 0; | |
if (++this.z >= this.sizeZ) { | |
this.z = 0; | |
++this.y; | |
} | |
} | |
return regionBlock; | |
} | |
public void remove() {} | |
} | |
public static class RegionBlock { | |
private Point blockPosition; | |
private short stateId; | |
public RegionBlock(@NotNull Point blockPosition, short stateId) { | |
this.blockPosition = blockPosition; | |
this.stateId = stateId; | |
} | |
public Point getPosition() { | |
return this.blockPosition; | |
} | |
public short getStateId() { | |
return this.stateId; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package me.hugo.kweebecparty.schem; | |
import net.minestom.server.coordinate.Point; | |
import net.minestom.server.instance.Instance; | |
import org.jetbrains.annotations.NotNull; | |
import org.jglrxavpok.hephaistos.nbt.NBTCompound; | |
import org.jglrxavpok.hephaistos.nbt.NBTException; | |
import org.jglrxavpok.hephaistos.nbt.NBTReader; | |
import java.io.File; | |
import java.io.IOException; | |
public class Schematic implements ISchematic { | |
public static final String MAIN_FOLDER = "structures/"; | |
private File schematicFile; | |
private Instance instance; | |
private ISchematic schematic; | |
public Schematic(@NotNull String schematicName, @NotNull Instance instance) { | |
this(new File(MAIN_FOLDER + schematicName + ".schem"), instance); | |
} | |
public Schematic(@NotNull File schematicFile, @NotNull Instance instance) { | |
if(schematicFile.exists()) | |
this.schematicFile = schematicFile; | |
else | |
this.schematicFile = new File(schematicFile.getPath() + "atic"); | |
this.instance = instance; | |
} | |
public ErrorMessage read() { | |
// Check is file exists | |
if(!this.schematicFile.exists()) | |
return ErrorMessage.NoSuchFile; | |
// Check is file | |
if(!this.schematicFile.isFile()) | |
return ErrorMessage.NoSuchFile; | |
try (NBTReader reader = new NBTReader(this.schematicFile)) { | |
// Get Main NBT | |
this.schematic = new SpongeSchematic(this.schematicFile, this.instance); | |
} catch (IOException ex) { | |
return ErrorMessage.BadRead; | |
} | |
return this.schematic.read(); | |
} | |
public ErrorMessage write(@NotNull Region region) { | |
if(this.schematic == null) | |
this.schematic = new SpongeSchematic(this.schematicFile, this.instance); | |
region.setInstance(this.instance); | |
return this.schematic.write(region); | |
} | |
public ErrorMessage build(@NotNull Point position, Runnable blocksCompleted) { | |
if(this.schematic == null) | |
this.schematic = new SpongeSchematic(this.schematicFile, this.instance); | |
return this.schematic.build(position, blocksCompleted); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package me.hugo.kweebecparty.schem; | |
import it.unimi.dsi.fastutil.longs.Long2ObjectMap; | |
import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; | |
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; | |
import net.minestom.server.instance.Chunk; | |
import net.minestom.server.instance.Instance; | |
import net.minestom.server.instance.InstanceContainer; | |
import net.minestom.server.instance.batch.Batch; | |
import net.minestom.server.instance.batch.BatchOption; | |
import net.minestom.server.instance.batch.ChunkBatch; | |
import net.minestom.server.instance.block.Block; | |
import net.minestom.server.utils.chunk.ChunkUtils; | |
import org.jetbrains.annotations.ApiStatus; | |
import org.jetbrains.annotations.NotNull; | |
import org.jetbrains.annotations.Nullable; | |
import java.util.concurrent.CountDownLatch; | |
import java.util.concurrent.atomic.AtomicInteger; | |
public class SchemBlockBatch implements Batch<Runnable> { | |
// In the form of <Chunk Index, Batch> | |
public final Long2ObjectMap<ChunkBatch> chunkBatchesMap = new Long2ObjectOpenHashMap<>(); | |
// Available for other implementations to handle. | |
protected final CountDownLatch readyLatch; | |
private final BatchOption options; | |
private volatile BatchOption inverseOption = new BatchOption(); | |
public SchemBlockBatch() { | |
this(new BatchOption()); | |
} | |
public SchemBlockBatch(BatchOption options) { | |
this(options, true); | |
} | |
private SchemBlockBatch(BatchOption options, boolean ready) { | |
this.readyLatch = new CountDownLatch(ready ? 0 : 1); | |
this.options = options; | |
} | |
@Override | |
public void setBlock(int x, int y, int z, @NotNull Block block) { | |
final int chunkX = ChunkUtils.getChunkCoordinate(x); | |
final int chunkZ = ChunkUtils.getChunkCoordinate(z); | |
final long chunkIndex = ChunkUtils.getChunkIndex(chunkX, chunkZ); | |
final ChunkBatch chunkBatch; | |
synchronized (chunkBatchesMap) { | |
chunkBatch = chunkBatchesMap.computeIfAbsent(chunkIndex, i -> new ChunkBatch(this.options)); | |
} | |
final int relativeX = x - (chunkX * Chunk.CHUNK_SIZE_X); | |
final int relativeZ = z - (chunkZ * Chunk.CHUNK_SIZE_Z); | |
chunkBatch.setBlock(relativeX, y, relativeZ, block); | |
} | |
@Override | |
public void clear() { | |
synchronized (chunkBatchesMap) { | |
this.chunkBatchesMap.clear(); | |
} | |
} | |
@Override | |
public boolean isReady() { | |
return this.readyLatch.getCount() == 0; | |
} | |
@Override | |
public void awaitReady() { | |
try { | |
this.readyLatch.await(); | |
} catch (InterruptedException e) { | |
throw new RuntimeException("#awaitReady interrupted!", e); | |
} | |
} | |
/** | |
* Applies this batch to the given instance. | |
* | |
* @param instance The instance in which the batch should be applied | |
* @param callback The callback to be executed when the batch is applied | |
* @return The inverse of this batch, if inverse is enabled in the {@link BatchOption} | |
*/ | |
@Override | |
public SchemBlockBatch apply(@NotNull Instance instance, @Nullable Runnable callback) { | |
return apply(instance, callback, true); | |
} | |
/** | |
* Applies this batch to the given instance, and execute the callback immediately when the | |
* blocks have been applied, in an unknown thread. | |
* | |
* @param instance The instance in which the batch should be applied | |
* @param callback The callback to be executed when the batch is applied | |
* @return The inverse of this batch, if inverse is enabled in the {@link BatchOption} | |
*/ | |
public SchemBlockBatch unsafeApply(@NotNull Instance instance, @Nullable Runnable callback) { | |
return apply(instance, callback, false); | |
} | |
/** | |
* Applies this batch to the given instance, and execute the callback depending on safeCallback. | |
* | |
* @param instance The instance in which the batch should be applied | |
* @param callback The callback to be executed when the batch is applied | |
* @param safeCallback If true, the callback will be executed in the next instance update. | |
* Otherwise it will be executed immediately upon completion | |
* @return The inverse of this batch, if inverse is enabled in the {@link BatchOption} | |
*/ | |
protected SchemBlockBatch apply(@NotNull Instance instance, @Nullable Runnable callback, boolean safeCallback) { | |
if (!this.options.isUnsafeApply()) this.awaitReady(); | |
final SchemBlockBatch inverse = this.options.shouldCalculateInverse() ? new SchemBlockBatch(inverseOption) : null; | |
synchronized (chunkBatchesMap) { | |
AtomicInteger counter = new AtomicInteger(); | |
for (var entry : Long2ObjectMaps.fastIterable(chunkBatchesMap)) { | |
final long chunkIndex = entry.getLongKey(); | |
final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex); | |
final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex); | |
final ChunkBatch batch = entry.getValue(); | |
ChunkBatch chunkInverse = batch.apply(instance, chunkX, chunkZ, c -> { | |
final boolean isLast = counter.incrementAndGet() == chunkBatchesMap.size(); | |
// Execute the callback if this was the last chunk to process | |
if (isLast) { | |
if (inverse != null) inverse.readyLatch.countDown(); | |
if (instance instanceof InstanceContainer) { | |
// FIXME: put method in Instance instead | |
((InstanceContainer) instance).refreshLastBlockChangeTime(); | |
} | |
if (callback != null) { | |
if (safeCallback) { | |
instance.scheduleNextTick(inst -> callback.run()); | |
} else { | |
callback.run(); | |
} | |
} | |
} | |
}); | |
if (inverse != null) inverse.chunkBatchesMap.put(chunkIndex, chunkInverse); | |
} | |
} | |
return inverse; | |
} | |
@ApiStatus.Experimental | |
public @NotNull BatchOption getInverseOption() { | |
return inverseOption; | |
} | |
@ApiStatus.Experimental | |
public void setInverseOption(@NotNull BatchOption inverseOption) { | |
this.inverseOption = inverseOption; | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package me.hugo.kweebecparty.schem; | |
import kotlin.Pair; | |
import me.hugo.kweebecparty.light.LightEngine; | |
import net.minestom.server.coordinate.Point; | |
import net.minestom.server.coordinate.Pos; | |
import net.minestom.server.entity.*; | |
import net.minestom.server.instance.Chunk; | |
import net.minestom.server.instance.Instance; | |
import net.minestom.server.instance.Section; | |
import net.minestom.server.instance.batch.AbsoluteBlockBatch; | |
import net.minestom.server.instance.block.Block; | |
import org.jetbrains.annotations.NotNull; | |
import org.jetbrains.annotations.Nullable; | |
import org.jglrxavpok.hephaistos.collections.ImmutableByteArray; | |
import org.jglrxavpok.hephaistos.nbt.*; | |
import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound; | |
import java.io.ByteArrayOutputStream; | |
import java.io.File; | |
import java.io.IOException; | |
import java.util.*; | |
public class SpongeSchematic implements ISchematic { | |
private File schematicFile; | |
private Instance instance; | |
private AbsoluteBlockBatch blockBatch; | |
private Integer version = 2; | |
// Sizes | |
private Short width; | |
private Short height; | |
private Short length; | |
private int[] offset; | |
// Blocks | |
private Integer maxPalette; | |
private Map<String, Integer> palette = new HashMap<>(); | |
private ImmutableByteArray blocksData; | |
private List<Region.RegionBlock> regionBlocks = new ArrayList<>(); | |
private boolean isLoaded = false; | |
public SpongeSchematic(@NotNull File schematicFile, @NotNull Instance instance) { | |
this.schematicFile = schematicFile; | |
this.instance = instance; | |
} | |
public ErrorMessage read() { | |
if(this.isLoaded) | |
return ErrorMessage.None; | |
// Check is file exists | |
if(!this.schematicFile.exists()) | |
return ErrorMessage.NoSuchFile; | |
// Check is file | |
if(!this.schematicFile.isFile()) | |
return ErrorMessage.NoSuchFile; | |
try (NBTReader reader = new NBTReader(this.schematicFile)) { | |
// Get Main NBT Name | |
Pair<String, NBT> pair = reader.readNamed(); | |
// Get Main NBT | |
NBTCompound nbtTag = (NBTCompound) pair.getSecond(); | |
// Is SpongeSchematic | |
if(!pair.getFirst().equals("Schematic")) | |
return ErrorMessage.NBTName; | |
this.version = nbtTag.getInt("Version"); | |
if(this.version == null) | |
this.version = 2; | |
// TODO: Read and Check Data Version | |
ErrorMessage errorMessage = readSizes(nbtTag); | |
if(errorMessage != ErrorMessage.None) | |
return errorMessage; | |
errorMessage = readBlockPalette(nbtTag); | |
if(errorMessage != ErrorMessage.None) | |
return errorMessage; | |
// TODO: Read block data | |
errorMessage = readBlocks(); | |
if(errorMessage != ErrorMessage.None) | |
return errorMessage; | |
if(this.version != 2) | |
return ErrorMessage.None; | |
// TODO: Read Biome Palette | |
// TODO: Read BiomeData | |
} catch (IOException | NBTException ex) { | |
return ErrorMessage.BadRead; | |
} | |
this.isLoaded = true; | |
return ErrorMessage.None; | |
} | |
private ErrorMessage readSizes(@NotNull NBTCompound nbtTag) { | |
// Get Width | |
this.width = nbtTag.getShort("Width"); | |
if(this.width == null) | |
return ErrorMessage.NBTWidth; | |
// Get Height | |
this.height = nbtTag.getShort("Height"); | |
if(this.height == null) | |
return ErrorMessage.NBTHeight; | |
// Get Length | |
this.length = nbtTag.getShort("Length"); | |
if(this.length == null) | |
return ErrorMessage.NBTLength; | |
// Get offset | |
this.offset = nbtTag.getIntArray("Offset").copyArray(); | |
if(this.offset == null || this.offset.length != 3) | |
this.offset = new int[] {0, 0, 0}; | |
return ErrorMessage.None; | |
} | |
private ErrorMessage readBlockPalette(@NotNull NBTCompound nbtTag) { | |
// Get Max Palette | |
this.maxPalette = nbtTag.getInt("PaletteMax"); | |
if(this.maxPalette == null) | |
return ErrorMessage.NBTMaxPalette; | |
// Get Palette | |
NBTCompound nbtPalette = (NBTCompound) nbtTag.get("Palette"); | |
if(nbtPalette == null) | |
return ErrorMessage.NBTPalette; | |
// Is Palette same size | |
List<String> keys = nbtPalette.getKeys().stream().toList(); | |
if(keys.size() != maxPalette) | |
return ErrorMessage.PaletteNotEqualsMaxPalette; | |
// Create map from nbtPalette | |
for(String key : keys) { | |
Integer value = nbtPalette.getInt(key); | |
if(value == null) | |
return ErrorMessage.PaletteGetInt; | |
this.palette.put(key, value); | |
} | |
// Sort Palette map by values | |
this.palette = this.palette.entrySet().stream() | |
.sorted(Comparator.comparing(Map.Entry::getValue)) | |
.collect(LinkedHashMap::new,(map, entry) -> map.put(entry.getKey(), entry.getValue()), LinkedHashMap::putAll); | |
// Get block data | |
this.blocksData = nbtTag.getByteArray("BlockData"); | |
if(this.blocksData == null || this.blocksData.getSize() == 0) | |
return ErrorMessage.NBTBlockData; | |
return ErrorMessage.None; | |
} | |
// https://github.com/EngineHub/WorldEdit/blob/303f5a76b2df70d63480f2126c9ef4b228eb3c59/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SpongeSchematicReader.java#L261-L297 | |
private ErrorMessage readBlocks() { | |
int index = 0; | |
int i = 0; | |
int value; | |
int varintLength; | |
List<String> paletteKeys = new ArrayList<>(palette.keySet()); | |
while (i < this.blocksData.getSize()) { | |
value = 0; | |
varintLength = 0; | |
while (true) { | |
value |= (this.blocksData.get(i) & 127) << (varintLength++ * 7); | |
if (varintLength > 5) { | |
return ErrorMessage.VarIntSize; | |
} | |
if ((this.blocksData.get(i) & 128) != 128) { | |
i++; | |
break; | |
} | |
i++; | |
} | |
// Offset is not needed | |
int x = (index % (width * length)) % width | |
//- offset[0] | |
; | |
int y = index / (width * length) | |
//- offset[1] | |
; | |
int z = (index % (width * length)) / width | |
//- offset[2] | |
; | |
String block = paletteKeys.get(value); | |
short stateId = getStateId(block); | |
this.regionBlocks.add(new Region.RegionBlock( | |
new Pos(x, y, z), | |
stateId | |
)); | |
index++; | |
} | |
return ErrorMessage.None; | |
} | |
public ErrorMessage write(@NotNull Region region) { | |
MutableNBTCompound nbtTag = new MutableNBTCompound(); | |
// Set Version | |
nbtTag.setInt("Version", this.version); | |
// TODO: Write Data entities | |
ErrorMessage errorMessage = writeSizes(nbtTag, region); | |
if(errorMessage != ErrorMessage.None) | |
return errorMessage; | |
errorMessage = writeBlockPalette(nbtTag, region); | |
if(errorMessage != ErrorMessage.None) | |
return errorMessage; | |
// TODO: Write Block entities | |
// TODO: Write entities | |
// TODO: Write Biome Palette | |
// TODO: Write BiomeData | |
try(NBTWriter writer = new NBTWriter(this.schematicFile)) { | |
writer.writeNamed("Schematic", nbtTag.toCompound()); | |
} catch (IOException ex) { | |
return ErrorMessage.BadWrite; | |
} | |
return ErrorMessage.None; | |
} | |
private ErrorMessage writeSizes(@NotNull MutableNBTCompound nbtTag, @NotNull Region region) { | |
// Set Width | |
nbtTag.setShort("Width", (short)region.getSizeX()); | |
// Set Height | |
nbtTag.setShort("Height", (short)region.getSizeY()); | |
// Set Length | |
nbtTag.setShort("Length", (short)region.getSizeZ()); | |
// Set offset | |
Point lower = region.getLower(); | |
nbtTag.setIntArray("Offset", new int[] { | |
(int) lower.x(), | |
(int) lower.y(), | |
(int) lower.z() | |
}); | |
return ErrorMessage.None; | |
} | |
private ErrorMessage writeBlockPalette(@NotNull MutableNBTCompound nbtTag, @NotNull Region region) { | |
// Generate Palette | |
int paletteMax = 0; | |
Map<String, Integer> palette = new HashMap<>(); | |
ByteArrayOutputStream buffer = new ByteArrayOutputStream((int) (region.getSizeX() * region.getSizeY() * region.getSizeZ())); | |
Iterator<Region.RegionBlock> regionIterator = region.iterator(); | |
while(regionIterator.hasNext()) { | |
Region.RegionBlock regionBlock = regionIterator.next(); | |
short stateId = this.instance.getBlock(regionBlock.getPosition()).stateId(); | |
String name = getNameFromStateId(stateId); | |
int blockId; | |
if (palette.containsKey(name)) { | |
blockId = palette.get(name); | |
} else { | |
blockId = paletteMax; | |
palette.put(name, blockId); | |
paletteMax++; | |
} | |
while ((blockId & -128) != 0) { | |
buffer.write(blockId & 127 | 128); | |
blockId >>>= 7; | |
} | |
buffer.write(blockId); | |
} | |
// Set PaletteMax | |
nbtTag.setInt("PaletteMax", paletteMax); | |
// Palette items to NBTTag | |
MutableNBTCompound paletteItems = new MutableNBTCompound(); | |
palette.forEach(paletteItems::setInt); | |
// Set Palette | |
nbtTag.set("Palette", paletteItems.toCompound()); | |
// Set Block Data | |
nbtTag.setByteArray("BlockData", buffer.toByteArray()); | |
return ErrorMessage.None; | |
} | |
public ErrorMessage build(@NotNull Point position, Runnable blocksCompleted) { | |
if(!this.isLoaded) | |
return ErrorMessage.NotLoaded; | |
if(this.instance == null) | |
return ErrorMessage.Instance; | |
if(this.regionBlocks == null || this.regionBlocks.size() == 0) | |
return ErrorMessage.NoBlocks; | |
this.blockBatch = new AbsoluteBlockBatch(); | |
for (Region.RegionBlock regionBlock : this.regionBlocks) { | |
Point blockPosition = regionBlock.getPosition(); | |
short stateId = regionBlock.getStateId(); | |
this.blockBatch.setBlock((int) (blockPosition.x() + (int)position.x()), (int) (blockPosition.y() + (int)position.y()), (int) (blockPosition.z() + (int)position.z()), Block.fromStateId(stateId)); | |
} | |
//Light | |
this.blockBatch.apply(this.instance, () -> { | |
new LightEngine().recalculateInstance(instance); | |
blocksCompleted.run(); | |
}); | |
return ErrorMessage.None; | |
} | |
private Block getBlock(@NotNull String input) { | |
String fullName = input.split("\\[")[0]; | |
//String name = fullName.split(":")[1]; | |
return Block.fromNamespaceId(fullName); | |
} | |
private short getStateId(@NotNull String input) { | |
Block block = getBlock(input); | |
String states = input.replaceAll(block.name(), ""); | |
if(states.startsWith("[")) { | |
String[] stateArray = states.substring(1, states.length() - 1).split(","); | |
List<String> keys = new ArrayList<>(); | |
List<String> values = new ArrayList<>(); | |
for (String s : stateArray) { | |
String[] x = s.split("="); | |
keys.add(x[0]); | |
values.add(x[1]); | |
} | |
HashMap<String, String> hashMap = new HashMap<>(); | |
for (int i = 0; i < keys.size(); i++) { | |
hashMap.put(keys.get(i), values.get(i)); | |
} | |
return block.withProperties(hashMap).stateId(); | |
} else return block.stateId(); | |
} | |
@Nullable | |
private String getNameFromStateId(short stateId) { | |
Block block = Block.fromStateId(stateId); | |
StringBuilder blockName = new StringBuilder(block.name()); | |
List<Block> alternatives = block.possibleStates().stream().toList(); | |
for(Block blockAlternative : alternatives) { | |
if(blockAlternative.stateId() != stateId) | |
continue; | |
List<String> properties = blockAlternative.properties().values().stream().toList(); | |
if(properties.size() == 0) | |
return blockName.toString(); | |
blockName.append("["); | |
for(int i = 0; i < properties.size(); i++) { | |
blockName.append(properties.get(i)); | |
if(!(i + 1 == properties.size())) | |
blockName.append(","); | |
} | |
blockName.append("]"); | |
return blockName.toString(); | |
} | |
return null; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment