Skip to content
This repository has been archived by the owner on Apr 14, 2024. It is now read-only.

Commit

Permalink
Convert to Java
Browse files Browse the repository at this point in the history
Also improved converter massively (no more OOM)
  • Loading branch information
emortaldev committed Dec 21, 2022
1 parent ecda397 commit 723cb52
Show file tree
Hide file tree
Showing 15 changed files with 446 additions and 367 deletions.
28 changes: 10 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
TNT is an experimental world format for Minestom

## Cool stuff
- Very small file size (~80kb in TNT vs ~13mb in Anvil for my lobby)
- Very fast loading times (23ms for my lobby - idk what Anvil is)
- [Converts from Anvil automatically](#anvil-conversion)
- [Could be loaded from databases, like Slime worlds](#tnt-sources)
- Stores block nbt (like sign text)
- Stores cached light from anvil (useful because Minestom doesn't have a light engine yet)
- Designed for small worlds (global palette)
- Very fast loading times (23ms for my lobby - idk what Anvil is)
- Very small file size (~80kb in TNT vs ~13mb in Anvil for my lobby)
- [Converts from Anvil automatically](#anvil-conversion)
- [Could be loaded from databases, like Slime worlds](#tnt-sources)
- Stores block nbt (like sign text)
- Stores cached light from anvil (useful because Minestom doesn't have a light engine yet)

Unfortunately does not save entities (yet) as Minestom does not have entity (de)serialisation.

Expand All @@ -19,18 +20,9 @@ Also does not have world saving yet
Creating a Minestom instance

```java
// In Kotlin
val instance = MinecraftServer.getInstanceManager().createInstanceContainer()
val tntLoader = TNTLoader(instance, FileTNTSource(Path.of("path/to/world.tnt")))
// Shorthand version
val tntLoader = TNTLoader(instance, "path/to/world.tnt")

instance.chunkLoader = tntLoader

// In Java
InstanceContainer instance = MinecraftServer.getInstanceManager().createInstanceContainer();
TNTLoader tntLoader = new TNTLoader(instance, FileTNTSource(Path.of("path/to/world.tnt")));
// Shorthand version
TNTLoader tntLoader = new TNTLoader(instance, new FileTNTSource(Path.of("path/to/world.tnt")));
// or
TNTLoader tntLoader = new TNTLoader(instance, "path/to/world.tnt")

instance.setChunkLoader(tntLoader);
Expand All @@ -56,4 +48,4 @@ TNT worlds can be loaded and saved wherever you want (however only `FileTNTSourc

For example, you could make it read from Redis, MongoDB, MySQL or any sort of datastore.

You can do this by overriding `TNTSource` and creating your own source.
You can do this by extending `TNTSource` and creating your own source.
14 changes: 4 additions & 10 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@

import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
id("org.jetbrains.kotlin.jvm") version "1.7.10"
kotlin("plugin.serialization") version "1.7.10"
id("com.github.johnrengelman.shadow") version "7.1.2"

`maven-publish`
Expand All @@ -23,8 +20,7 @@ dependencies {

compileOnly("com.github.Minestom:Minestom:d596992c0e")

compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2")
implementation("com.github.luben:zstd-jni:1.5.2-3")
implementation("com.github.luben:zstd-jni:1.5.2-5")
}

tasks {
Expand All @@ -43,11 +39,9 @@ tasks {
build { dependsOn(shadowJar) }
}

val compileKotlin: KotlinCompile by tasks
compileKotlin.kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString()

compileKotlin.kotlinOptions {
freeCompilerArgs = listOf("-Xinline-classes")
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

publishing {
Expand Down
3 changes: 2 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
kotlin.code.style=official
name=TNT
mainClass=TNTExtension
group=dev.emortal.tnt
group=dev.emortal.tnt
version=1.0.0
176 changes: 176 additions & 0 deletions src/main/java/dev/emortal/tnt/TNT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package dev.emortal.tnt;

import com.github.luben.zstd.Zstd;
import dev.emortal.tnt.source.TNTSource;
import net.minestom.server.MinecraftServer;
import net.minestom.server.instance.*;
import net.minestom.server.instance.block.Block;
import net.minestom.server.utils.binary.BinaryWriter;
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;

public class TNT {

private static Logger LOGGER = LoggerFactory.getLogger("TNT");

private static byte[] convertChunk(Chunk chunk) throws IOException {
BinaryWriter writer = new BinaryWriter();

writer.writeInt(chunk.getChunkX());
writer.writeInt(chunk.getChunkZ());
writer.writeByte((byte) chunk.getMinSection());
writer.writeByte((byte) chunk.getMaxSection());
// LOGGER.info("Chunk {} {} min max {} {}", chunk.getChunkX(), chunk.getChunkZ(), chunk.getMinSection(), chunk.getMaxSection());

int sectionIter = 0;
for (Section section : chunk.getSections()) {
int airSkip = 0;
boolean needsEnding = false;

for (int x = 0; x < Chunk.CHUNK_SIZE_X; x++) {
for (int y = 0; y < Chunk.CHUNK_SECTION_SIZE; y++) {
for (int z = 0; z < Chunk.CHUNK_SIZE_Z; z++) {
Block block = chunk.getBlock(x, y + ((sectionIter + chunk.getMinSection()) * Chunk.CHUNK_SECTION_SIZE), z);

if (block.compare(Block.AIR)) {
airSkip++;
if (airSkip == 1) {
writer.writeShort((short )0);
// LOGGER.info("Wrote short 0");
needsEnding = true;
}

continue;
}
if (airSkip > 0) {
writer.writeInt(airSkip);
// LOGGER.info("Wrote int {}", airSkip);
needsEnding = false;
}

airSkip = 0;

writer.writeShort(block.stateId());
// LOGGER.info("Wrote short {}", block.stateId());

NBTCompound nbt = block.nbt();
writer.writeBoolean(block.hasNbt());
// LOGGER.info("Wrote bool {}", block.hasNbt());
if (nbt != null) {
writer.writeNBT("blockNBT", nbt);
}
}
}
}

// Air skip sometimes isn't written, maybe there is a cleaner way?
if (needsEnding) {
writer.writeInt(airSkip);
}

writer.writeByteArray(section.getBlockLight());
writer.writeByteArray(section.getSkyLight());

sectionIter++;
}

byte[] bytes = writer.toByteArray();
// byte[] compressed = Zstd.compress(bytes);

// source.save(compressed);

writer.close();
writer.flush();

return bytes;
}

public static void convertAnvilToTNT(Path anvilPath, TNTSource source) throws IOException, InterruptedException {
InstanceManager im = MinecraftServer.getInstanceManager();

Set<Path> mcas = Files.list(anvilPath.resolve("region")).collect(Collectors.toSet());

InstanceContainer convertInstance = im.createInstanceContainer();
AnvilLoader loader = new AnvilLoader(anvilPath);
convertInstance.setChunkLoader(loader);

CountDownLatch cdl = new CountDownLatch(mcas.size() * 32 * 32);

ArrayList<byte[]> convertedChunks = new ArrayList<>();

for (Path mca : mcas) {
String[] args = mca.getFileName().toString().split("\\.");
int rX = Integer.parseInt(args[1]);
int rZ = Integer.parseInt(args[2]);

// LOGGER.info("Found mca x{} z{}", rX, rZ);

for (int x = rX * 32; x < rX * 32 + 32; x++) {
for (int z = rZ * 32; z < rZ * 32 + 32; z++) {
convertInstance.loadChunk(x, z).thenAccept(chunk -> {

// Ignore chunks that contain no blocks
boolean containsBlocks = false;
for (Section section : chunk.getSections()) {
if (section.blockPalette().count() > 0) {
containsBlocks = true;
break;
}
}
if (!containsBlocks) {
cdl.countDown();
return;
}

// LOGGER.info("Using chunk {} {}", chunk.getChunkX(), chunk.getChunkZ());


byte[] converted = new byte[0];
try {
converted = convertChunk(chunk);
} catch (IOException e) {
e.printStackTrace();
}
convertedChunks.add(converted);
converted = null;

// We're now done with this chunk
convertInstance.unloadChunk(chunk);

cdl.countDown();
});
}
}
}

cdl.await();

BinaryWriter writer = new BinaryWriter();

writer.writeInt(convertedChunks.size());
for (byte[] chunk : convertedChunks) {
writer.writeBytes(chunk);
}

// LOGGER.info("Wrote {} chunks", convertedChunks.size());

byte[] bytes = writer.toByteArray();
byte[] compressed = Zstd.compress(bytes);

source.save(compressed);

writer.close();
writer.flush();
}

}
19 changes: 19 additions & 0 deletions src/main/java/dev/emortal/tnt/TNTChunk.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.emortal.tnt;

import net.minestom.server.instance.Section;
import net.minestom.server.instance.batch.ChunkBatch;

import java.util.Arrays;

public class TNTChunk {

public ChunkBatch batch;
public Section[] sections;
public TNTChunk(ChunkBatch batch, int maxSection, int minSection) {
this.sections = new Section[maxSection - minSection];
Arrays.setAll(sections, (a) -> new Section());

this.batch = batch;
}

}
15 changes: 15 additions & 0 deletions src/main/java/dev/emortal/tnt/TNTExtension.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package dev.emortal.tnt;

import net.minestom.server.extensions.Extension;

class TNTExtension extends Extension {
@Override
public void initialize() {

}

@Override
public void terminate() {

}
}
Loading

0 comments on commit 723cb52

Please sign in to comment.