Add the Block Data API and Block Generator.

This commit is contained in:
Articdive 2021-05-22 21:56:01 +02:00
parent a749f07a3f
commit dba8b65c03
No known key found for this signature in database
GPG Key ID: B069585F0F7D90DE
20 changed files with 16201 additions and 2691 deletions

9
.gitignore vendored
View File

@ -32,7 +32,7 @@ hs_err_pid*
### Gradle template
.gradle
/build/
**/build/
# Ignore Gradle GUI config
gradle-app.setting
@ -45,6 +45,7 @@ gradle-app.setting
# IDEA files
.idea/
**/out/
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties
@ -56,5 +57,7 @@ gradle-app.setting
/.mixin.out/
# When compiling we get a docs folder
/docs
.mixin.out/
**/docs
# The data for generation is included in this folder
code-generators/data/

View File

@ -5,6 +5,8 @@ plugins {
id 'maven-publish'
id 'org.jetbrains.kotlin.jvm' version '1.5.0'
id 'checkstyle'
// Used to download our data
id 'de.undercouch.download' version '4.1.1'
}
group 'net.minestom.server'
@ -217,6 +219,85 @@ configurations.all {
exclude group: "org.checkerframework", module: "checker-qual"
}
// UPDATE: Update the data to the required version
task downloadData(type: Download) {
String mainURL = "https://raw.githubusercontent.com/Minestom/MinestomDataGenerator/master/Minestom-data/1.16.5/1_16_5_";
src([
// attributes.json
"${mainURL}attributes.json",
// biomes.json
"${mainURL}biomes.json",
// block_entities
"${mainURL}block_entities.json",
// block_properties
"${mainURL}block_properties.json",
// blocks.json
"${mainURL}blocks.json",
// custom_statistics.json
"${mainURL}custom_statistics.json",
// dimension_types.json
"${mainURL}dimension_types.json",
// enchantments.json
"${mainURL}enchantments.json",
// fluids.json
"${mainURL}fluids.json",
// items.json
"${mainURL}items.json",
// map_colors.json
"${mainURL}map_colors.json",
// particles.json
"${mainURL}particles.json",
// potion_effects.json
"${mainURL}potion_effects.json",
// potions.json
"${mainURL}potions.json",
// sounds.json
"${mainURL}sounds.json",
// villager_professions.json
"${mainURL}villager_professions.json",
// villager_types.json
"${mainURL}villager_types.json"
])
dest new File(processResources.destinationDir, "/minecraft_data/")
overwrite true
}
task downloadTags(type: Download) {
String mainURL = "https://raw.githubusercontent.com/Minestom/MinestomDataGenerator/master/Minestom-data/1.16.5/1_16_5_tags/1_16_5_";
src([
// block_tags.json
"${mainURL}block_tags.json",
// entity_type_tags.json
"${mainURL}entity_type_tags.json",
// fluid_tags.json
"${mainURL}fluid_tags.json",
// item_tags.json
"${mainURL}item_tags.json",
])
dest new File(processResources.destinationDir, "/minecraft_data/tags/")
overwrite true
}
task downloadLootTables(type: Download) {
String mainURL = "https://raw.githubusercontent.com/Minestom/MinestomDataGenerator/master/Minestom-data/1.16.5/1_16_5_loot_tables/1_16_5_";
src([
// block_loot_tables.json.json
"${mainURL}block_loot_tables.json",
// chest_loot_tables.json
"${mainURL}chest_loot_tables.json",
// entity_loot_tables.json
"${mainURL}entity_loot_tables.json",
// gameplay_loot_tables.json
"${mainURL}gameplay_loot_tables.json",
])
dest new File(processResources.destinationDir, "/minecraft_data/loot_tables/")
overwrite true
}
processResources {
dependsOn(downloadData, downloadTags, downloadLootTables)
}
publishing {
publications {
mavenJava(MavenPublication) {

View File

@ -0,0 +1,123 @@
plugins {
id 'java'
id 'application'
// Used to download our data
id 'de.undercouch.download'
}
group 'net.minestom.server'
version '1.0'
sourceCompatibility = 1.11
def mcVersion = "1.16.5"
def mcVersionUnderscored = mcVersion.replace('.', '_')
def dataDest = new File(project.projectDir, "/data/")
def dataDestTags = new File(project.projectDir, "/data/tags/")
def dataDestLootTable = new File(project.projectDir, "/data/loot_tables/")
application {
mainClass.set("net.minestom.codegen.Generators")
}
dependencies {
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'org.jetbrains:annotations:20.1.0'
implementation 'com.squareup:javapoet:1.13.0'
// Logging
implementation 'org.apache.logging.log4j:log4j-core:2.14.0'
// SLF4J is the base logger for most libraries, therefore we can hook it into log4j2.
implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.14.0'
// Kotlin stuff
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${project.kotlinVersion}"
implementation "org.jetbrains.kotlin:kotlin-reflect:${project.kotlinVersion}"
}
// UPDATE: Update the data to the required version and run the code generator.
task downloadData(type: Download) {
String mainURL = "https://raw.githubusercontent.com/Minestom/MinestomDataGenerator/master/Minestom-data/$mcVersion/${mcVersionUnderscored}_"
src([
// attributes.json
"${mainURL}attributes.json",
// biomes.json
"${mainURL}biomes.json",
// block_entities
"${mainURL}block_entities.json",
// block_properties
"${mainURL}block_properties.json",
// blocks.json
"${mainURL}blocks.json",
// custom_statistics.json
"${mainURL}custom_statistics.json",
// dimension_types.json
"${mainURL}dimension_types.json",
// enchantments.json
"${mainURL}enchantments.json",
// fluids.json
"${mainURL}fluids.json",
// items.json
"${mainURL}items.json",
// map_colors.json
"${mainURL}map_colors.json",
// particles.json
"${mainURL}particles.json",
// potion_effects.json
"${mainURL}potion_effects.json",
// potions.json
"${mainURL}potions.json",
// sounds.json
"${mainURL}sounds.json",
// villager_professions.json
"${mainURL}villager_professions.json",
// villager_types.json
"${mainURL}villager_types.json"
])
dest dataDest
overwrite true
dependsOn("downloadLootTables")
dependsOn("downloadTags")
}
task downloadTags(type: Download) {
String mainURL = "https://raw.githubusercontent.com/Minestom/MinestomDataGenerator/master/Minestom-data/$mcVersion/${mcVersionUnderscored}_tags/${mcVersionUnderscored}_"
src([
// block_tags.json
"${mainURL}block_tags.json",
// entity_type_tags.json
"${mainURL}entity_type_tags.json",
// fluid_tags.json
"${mainURL}fluid_tags.json",
// item_tags.json
"${mainURL}item_tags.json",
])
dest dataDestTags
overwrite true
}
task downloadLootTables(type: Download) {
String mainURL = "https://raw.githubusercontent.com/Minestom/MinestomDataGenerator/master/Minestom-data/$mcVersion/${mcVersionUnderscored}_loot_tables/${mcVersionUnderscored}_"
src([
// block_loot_tables.json.json
"${mainURL}block_loot_tables.json",
// chest_loot_tables.json
"${mainURL}chest_loot_tables.json",
// entity_loot_tables.json
"${mainURL}entity_loot_tables.json",
// gameplay_loot_tables.json
"${mainURL}gameplay_loot_tables.json",
])
dest dataDestLootTable
overwrite true
}
run {
// Update version
setArgs(
List.of(
mcVersion,
dataDest.getAbsolutePath(),
project.rootProject.projectDir.getPath() + "${File.separatorChar}src${File.separatorChar}autogenerated${File.separatorChar}java")
)
dependsOn(downloadData)
}

View File

@ -0,0 +1,28 @@
package net.minestom.codegen;
import net.minestom.codegen.blocks.BlockGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
public class Generators {
private static final Logger LOGGER = LoggerFactory.getLogger(Generators.class);
public static void main(String[] args) {
if (args.length < 3) {
LOGGER.error("Usage: <MC version> <source folder> <target folder>");
return;
}
String targetVersion = args[0];
File inputFolder = new File(args[1]);
File outputFolder = new File(args[2]);
// Generate blocks
new BlockGenerator(
new File(inputFolder, targetVersion.replaceAll("\\.", "_") + "_blocks.json"),
new File(inputFolder, targetVersion.replaceAll("\\.", "_") + "_block_properties.json"),
outputFolder
).generate();
LOGGER.info("Finished generating code");
}
}

View File

@ -0,0 +1,26 @@
package net.minestom.codegen;
import com.squareup.javapoet.JavaFile;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.List;
public abstract class MinestomCodeGenerator {
private static final Logger LOGGER = LoggerFactory.getLogger(MinestomCodeGenerator.class);
public abstract void generate();
protected void writeFiles(@NotNull List<JavaFile> fileList, File outputFolder) {
for (JavaFile javaFile : fileList) {
try {
javaFile.writeTo(outputFolder);
} catch (IOException e) {
LOGGER.error("An error occured while writing source code to the file system.", e);
}
}
}
}

View File

@ -0,0 +1,291 @@
package net.minestom.codegen.blocks;
import com.google.gson.*;
import com.google.gson.stream.JsonReader;
import com.squareup.javapoet.*;
import net.minestom.codegen.MinestomCodeGenerator;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.lang.model.element.Modifier;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public final class BlockGenerator extends MinestomCodeGenerator {
private static final Logger LOGGER = LoggerFactory.getLogger(BlockGenerator.class);
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
private final File blocksFile;
private final File blockPropertyFile;
private final File outputFolder;
public BlockGenerator(@NotNull File blocksFile, @NotNull File blockPropertyFile, @NotNull File outputFolder) {
this.blocksFile = blocksFile;
this.blockPropertyFile = blockPropertyFile;
this.outputFolder = outputFolder;
}
@Override
public void generate() {
if (!blocksFile.exists()) {
LOGGER.error("Failed to find blocks.json.");
LOGGER.error("Stopped code generation for blocks.");
return;
}
if (!blockPropertyFile.exists()) {
LOGGER.error("Failed to find block_properties.json.");
LOGGER.error("Stopped code generation for block properties.");
return;
}
if (!outputFolder.exists() && !outputFolder.mkdirs()) {
LOGGER.error("Output folder for code generation does not exist and could not be created.");
return;
}
if (!outputFolder.exists() && !outputFolder.mkdirs()) {
LOGGER.error("Output folder for code generation does not exist and could not be created.");
return;
}
// Important classes we use alot
ClassName namespaceIDCN = ClassName.get("net.minestom.server.utils", "NamespaceID");
ClassName blockPropertyCN = ClassName.get("net.minestom.server.instance.block", "BlockProperty");
ClassName blockCN = ClassName.get("net.minestom.server.instance.block", "Block");
ClassName blockImplCN = ClassName.get("net.minestom.server.instance.block", "BlockImpl");
JsonArray blockProperties;
try {
blockProperties = GSON.fromJson(new JsonReader(new FileReader(blockPropertyFile)), JsonArray.class);
} catch (FileNotFoundException e) {
LOGGER.error("Failed to find block_properties.json.");
LOGGER.error("Stopped code generation for block entities.");
return;
}
ClassName blockPropertiesCN = ClassName.get("net.minestom.server.instance.block", "BlockProperties");
// Particle
TypeSpec.Builder blockPropertiesClass = TypeSpec.classBuilder(blockPropertiesCN)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
// Add @SuppressWarnings("unused")
.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class).addMember("value", "$S", "unused").build())
.addJavadoc("AUTOGENERATED by " + getClass().getSimpleName());
// Add private constructor
blockPropertiesClass.addMethod(
MethodSpec.constructorBuilder()
.addModifiers(Modifier.PRIVATE)
.build()
);
// This stores the classes of the field names, e.g. WATERLOGGED --> Boolean
Map<String, Class<?>> propertyClassMap = new HashMap<>();
Map<String, String> propertyKeyMap = new HashMap<>();
Map<String, String[]> propertyValueMap = new HashMap<>();
// Use data
for (JsonElement e : blockProperties) {
JsonObject blockProperty = e.getAsJsonObject();
String propertyName = blockProperty.get("name").getAsString();
JsonArray blockValues = blockProperty.get("values").getAsJsonArray();
JsonPrimitive firstElement = blockValues.get(0).getAsJsonPrimitive();
Class<?> type;
StringBuilder values = new StringBuilder();
if (firstElement.isBoolean()) {
type = Boolean.class;
values = new StringBuilder("true, false");
} else if (firstElement.isNumber()) {
type = Integer.class;
for (JsonElement blockValue : blockValues) {
int i = blockValue.getAsInt();
values.append(i).append(", ");
}
// Delete final ', '
values.delete(values.lastIndexOf(","), values.length());
} else {
type = String.class;
for (JsonElement blockValue : blockValues) {
String s = blockValue.getAsString();
values.append("\"").append(s).append("\", ");
}
// Delete final ', '
values.delete(values.lastIndexOf(","), values.length());
}
String propertyKey = blockProperty.get("key").getAsString();
propertyKeyMap.put(propertyName, propertyKey);
propertyClassMap.put(propertyName, type);
propertyValueMap.put(propertyName, values.toString().split(", "));
// Adds the field to the main class
// e.g. BlockProperty<Boolean> WATERLOGGED = new BlockProperty<Boolean>("waterlogged", true, false)
blockPropertiesClass.addField(
FieldSpec.builder(
ParameterizedTypeName.get(blockPropertyCN, TypeName.get(type)),
propertyName
).initializer(
"new $T<>($S, $L)",
blockPropertyCN,
propertyKey,
values
).addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL).build()
);
}
JsonArray blocks;
try {
blocks = GSON.fromJson(new JsonReader(new FileReader(blocksFile)), JsonArray.class);
} catch (FileNotFoundException e) {
LOGGER.error("Failed to find blocks.json.");
LOGGER.error("Stopped code generation for blocks.");
return;
}
ClassName blocksCN = ClassName.get("net.minestom.server.instance.block", "BlockConstants");
// BlockConstants class
TypeSpec.Builder blockConstantsClass = TypeSpec.interfaceBuilder(blocksCN)
// Add @SuppressWarnings("unused")
.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class).addMember("value", "$S", "unused").build())
.addJavadoc("AUTOGENERATED by " + getClass().getSimpleName());
// Use data
for (JsonElement b : blocks) {
JsonObject block = b.getAsJsonObject();
String blockName = block.get("name").getAsString();
// Handle the properties
// Create a subclass for each Block and reference the main BlockProperty.
JsonArray properties = block.get("properties").getAsJsonArray();
if (properties.size() != 0) {
// Create a subclass called "blockName"
// e.g. subclass AIR in BlockProperties
TypeSpec.Builder subClass = TypeSpec.classBuilder(blockPropertiesCN.nestedClass(blockName))
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.addJavadoc(
"Represents the $L {@link $T $L} that {@link $T#$N} can have:\n",
properties.size(),
blockPropertyCN,
properties.size() > 1 ? "properties" : "property",
blocksCN,
blockName
).addJavadoc("<ul>\n");
// Store a list of values for the getProperties() method.
StringBuilder values = new StringBuilder();
// Go through all properties the block has.
for (JsonElement property : properties) {
String propertyName = property.getAsString();
// Add a static field that delegates to the BlockProperties static definition
FieldSpec.Builder field = FieldSpec.builder(
ParameterizedTypeName.get(blockPropertyCN, TypeName.get(propertyClassMap.get(propertyName))),
propertyName)
.initializer("$T.$N", blockPropertiesCN, propertyName)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.addJavadoc("Definition: \"$L\" = [", propertyKeyMap.get(propertyName));
// Results in "key" = ["value1", "value2"...]
String[] propertyVals = propertyValueMap.get(propertyName);
for (int i = 0; i < propertyVals.length; i++) {
if (i == propertyVals.length - 1) {
field.addJavadoc("$L]", propertyVals[i].toLowerCase());
} else {
field.addJavadoc("$L, ", propertyVals[i].toLowerCase());
}
}
// Add field to subclass
subClass.addField(field.build());
values.append(propertyName).append(", ");
subClass.addJavadoc("<li>{@link $T#$N}</li>\n", blockPropertiesCN, propertyName);
}
subClass.addJavadoc("</ul>");
// Delete final ', '
values.delete(values.lastIndexOf(","), values.length());
// Add a static method to get all the properties
subClass.addMethod(
MethodSpec.methodBuilder("getProperties")
.returns(
// List<BlockProperty<?>>
ParameterizedTypeName.get(
ClassName.get(List.class),
// BlockProperty<?>
ParameterizedTypeName.get(blockPropertyCN, TypeVariableName.get("?"))
)
)
.addStatement(
"return $T.of($L)",
// If it has multiple properties --> Arrays.asList() else Collections.singletonList()
ClassName.get(List.class),
values
)
.addModifiers(Modifier.STATIC)
.build()
);
blockPropertiesClass.addType(subClass.build());
}
JsonArray states = block.getAsJsonArray("states");
// Now handle the fields in Blocks.
// If we don't have properties
if (properties.size() == 0) {
// This is a block like Stone that only has 1 BlockState.
blockConstantsClass.addField(
FieldSpec.builder(blockCN, blockName)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.initializer(
// Blocks.STONE = new BlockImpl(NamespaceID.from("minecraft:stone"), 1, Collections.emptyList())
"$T.create($T.from($S), (short) $L, (short) $L, (short) $L, (short) $L, $T.emptyList())",
blockImplCN,
namespaceIDCN,
block.get("id").getAsString(),
// Block id
block.get("numericalID").getAsShort(),
// First state id
states.get(0).getAsJsonObject().get("id").getAsShort(),
// Last state id
states.get(states.size() - 1).getAsJsonObject().get("id").getAsShort(),
// Default state id
block.get("defaultBlockState").getAsShort(),
ClassName.get(Collections.class)
)
.build()
);
} else {
// This is a block that has multiple properties.
blockConstantsClass.addField(
FieldSpec.builder(blockCN, blockName)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.initializer(
// Blocks.GRASS_BLOCK = new BlockImpl(NamespaceID.from("minecraft:grass_block"), 9, 8, varargsProperty)
"$T.create($T.from($S), (short) $L, (short) $L, (short) $L, (short) $L, $T.$N.getProperties())",
blockImplCN,
namespaceIDCN,
block.get("id").getAsString(),
// Block id
block.get("numericalID").getAsShort(),
// First id
block.getAsJsonArray("states").get(0).getAsJsonObject().get("id").getAsShort(),
// Last state id
states.get(states.size() - 1).getAsJsonObject().get("id").getAsShort(),
// DefaultBlockStateId
block.get("defaultBlockState").getAsShort(),
blockPropertiesCN,
blockName
)
.build()
);
}
}
writeFiles(
List.of(
JavaFile.builder("net.minestom.server.instance.block", blockPropertiesClass.build())
.indent(" ")
.skipJavaLangImports(true)
.build(),
JavaFile.builder("net.minestom.server.instance.block", blockConstantsClass.build())
.indent(" ")
.skipJavaLangImports(true)
.build()
),
outputFolder
);
}
}

View File

@ -1,2 +1,2 @@
rootProject.name = 'Minestom'
include 'code-generators'

View File

@ -1,5 +0,0 @@
package net.minestom.server.instance.block;
final class BlockArray {
static final Block[] blocks = new Block[Short.MAX_VALUE];
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,9 @@
// AUTOGENERATED by net.minestom.codegen.RegistriesGenerator
package net.minestom.server.registry;
import java.util.HashMap;
import net.kyori.adventure.key.Key;
import net.minestom.server.entity.EntityType;
import net.minestom.server.fluids.Fluid;
import net.minestom.server.instance.block.Block;
import net.minestom.server.item.Enchantment;
import net.minestom.server.item.Material;
import net.minestom.server.particle.Particle;
@ -17,15 +15,12 @@ import net.minestom.server.utils.NamespaceID;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
/**
* AUTOGENERATED
*/
public final class Registries {
/**
* Should only be used for internal code, please use the get* methods.
*/
@Deprecated
public static final HashMap<NamespaceID, Block> blocks = new HashMap<>();
/**
* Should only be used for internal code, please use the get* methods.
@ -81,30 +76,6 @@ public final class Registries {
@Deprecated
public static final HashMap<NamespaceID, Fluid> fluids = new HashMap<>();
/**
* Returns the corresponding Block matching the given id. Returns 'AIR' if none match.
*/
@NotNull
public static Block getBlock(String id) {
return getBlock(NamespaceID.from(id));
}
/**
* Returns the corresponding Block matching the given id. Returns 'AIR' if none match.
*/
@NotNull
public static Block getBlock(NamespaceID id) {
return blocks.getOrDefault(id, Block.AIR);
}
/**
* Returns the corresponding Block matching the given key. Returns 'AIR' if none match.
*/
@NotNull
public static Block getBlock(Key key) {
return getBlock(NamespaceID.from(key));
}
/**
* Returns the corresponding Material matching the given id. Returns 'AIR' if none match.
*/

View File

@ -17,7 +17,6 @@ import net.minestom.server.gamedata.loottables.LootTableManager;
import net.minestom.server.gamedata.tags.TagManager;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.InstanceManager;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockManager;
import net.minestom.server.instance.block.CustomBlock;
import net.minestom.server.instance.block.rule.BlockPlacementRule;
@ -69,6 +68,7 @@ public final class MinecraftServer {
public final static Logger LOGGER = LoggerFactory.getLogger(MinecraftServer.class);
public static final String VERSION_NAME = "1.16.5";
public static final String VERSION_NAME_UNDERSCORED = VERSION_NAME.replace('.', '_');
public static final int PROTOCOL_VERSION = 754;
// Threads
@ -157,7 +157,6 @@ public final class MinecraftServer {
// without this line, registry types that are not loaded explicitly will have an internal empty registry in Registries
// That can happen with PotionType for instance, if no code tries to access a PotionType field
// TODO: automate (probably with code generation)
Block.values();
Material.values();
PotionType.values();
PotionEffect.values();

View File

@ -0,0 +1,92 @@
package net.minestom.server.instance.block;
import it.unimi.dsi.fastutil.shorts.Short2ObjectAVLTreeMap;
import it.unimi.dsi.fastutil.shorts.Short2ObjectSortedMap;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.key.Keyed;
import net.minestom.server.tag.Tag;
import net.minestom.server.tag.TagReadable;
import net.minestom.server.utils.NamespaceID;
import net.minestom.server.utils.math.IntRange;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.IntStream;
public interface Block extends Keyed, TagReadable, BlockConstants {
Registry REGISTRY = new Registry();
<T> @NotNull Block withProperty(@NotNull BlockProperty<T> property, @NotNull T value);
<T> @NotNull Block withTag(@NotNull Tag<T> tag, @Nullable T value);
@NotNull Block getDefaultBlock();
@NotNull NamespaceID getNamespaceId();
@Override
default @NotNull Key key() {
return getNamespaceId();
}
default @NotNull String getName() {
return getNamespaceId().asString();
}
@NotNull Map<String, String> createPropertiesMap();
int getBlockId();
short getStateId();
@NotNull BlockData getData();
/**
* Migrated to {@link #getData()}.{@link BlockData#isSolid()} method.
*
* @return True if the Block is solid.
*/
@Deprecated(
forRemoval = true
)
default boolean isSolid() {
return getData().isSolid();
}
class Registry {
private final Map<NamespaceID, Block> namespaceMap = new HashMap<>();
private final Short2ObjectSortedMap<BlockSupplier> stateSet = new Short2ObjectAVLTreeMap<>();
private Registry() {
}
public synchronized @Nullable Block fromNamespaceId(@NotNull NamespaceID namespaceID) {
return namespaceMap.get(namespaceID);
}
public synchronized @Nullable Block fromNamespaceId(@NotNull String namespaceID) {
return namespaceMap.get(NamespaceID.from(namespaceID));
}
public synchronized @Nullable Block fromStateId(short stateId) {
BlockSupplier supplier = stateSet.get(stateId);
return supplier.get(stateId);
}
public synchronized void register(@NotNull NamespaceID namespaceID, @NotNull Block block,
@NotNull IntRange range, @NotNull BlockSupplier blockSupplier) {
this.namespaceMap.put(namespaceID, block);
IntStream.range(range.getMinimum(), range.getMaximum()).forEach(value -> stateSet.put((short) value, blockSupplier));
}
@FunctionalInterface
interface BlockSupplier {
@NotNull Block get(short stateId);
}
}
}

View File

@ -1,51 +0,0 @@
package net.minestom.server.instance.block;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class BlockAlternative {
private final short id;
private final String[] properties;
public BlockAlternative(short id, String... properties) {
this.id = id;
this.properties = properties;
}
public short getId() {
return id;
}
public String[] getProperties() {
return properties;
}
public Map<String, String> createPropertiesMap() {
Map<String, String> map = new HashMap<>();
for (String p : properties) {
String[] parts = p.split("=");
map.put(parts[0], parts[1]);
}
return map;
}
public String getProperty(String key) {
for (String p : properties) {
String[] parts = p.split("=");
if (parts.length > 1)
if (parts[0].equals(key))
return parts[1];
}
return null;
}
@Override
public String toString() {
return "BlockAlternative{" +
"id=" + id +
", properties=" + Arrays.toString(properties) +
'}';
}
}

View File

@ -0,0 +1,116 @@
package net.minestom.server.instance.block;
import net.minestom.server.item.Material;
import net.minestom.server.map.MapColors;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public interface BlockData {
// Block properties
/**
* Gets the blast (explosion) resistance of a {@link Block}
* @return a double denoting the blast resistance.
*/
double getExplosionResistance();
/**
* Gets the corresponding {@link Material} of a {@link Block}
* @return the corresponding {@link Material} or null if not applicable.
*/
@Nullable Material getCorrespondingItem();
/**
* Gets the friction value of a {@link Block}
* @return a double denoting the friction.
*/
double getFriction();
/**
* Gets the speed factor of a {@link Block}
* @return a double denoting the speed factor.
*/
double getSpeedFactor();
/**
* Gets the jump factor of a {@link Block}
* @return a double denoting the jump factor.
*/
double getJumpFactor();
/**
* Checks if a {@link Block} is a block entity.
* @return a boolean, true when a Block is a block entity, false otherwise.
*/
boolean isBlockEntity();
// State properties
/**
* Gets the hardness (destroy speed) of a {@link Block}.
* @return a double denoting the hardness.
*/
double getHardness();
/**
* Gets the light level emitted by a {@link Block}
* @return an int representing the light emission.
*/
int getLightEmission();
/**
* Checks if a {@link Block} is occluding.
* @return a boolean, true if a Block is occluding, false otherwise.
*/
boolean isOccluding();
/**
* Gets the piston push reaction of a {@link Block}
* @return a {@link String} containing the push reaction of a Block.
*/
String getPushReaction(); // TODO: Dedicated object?
/**
* Checks if a {@link Block} is blocking motion
* @return a boolean, true if a Block is blocking motion, false otherwise.
*/
boolean isBlockingMotion();
/**
* Checks if a {@link Block} is flammable.
* @return a boolean, true if a Block is flammable, false otherwise.
*/
boolean isFlammable();
/**
* Checks if a {@link Block} is an instance of air.
* @return a boolean, true if a Block is air, false otherwise.
*/
boolean isAir();
/**
* Checks if a {@link Block} is an instance of a fluid.
* @return a boolean, true if a Block is a liquid, false otherwise.
*/
boolean isLiquid();
/**
* Checks if a {@link Block} is replaceable.
* @return a boolean, true if a Block is replaceable, false otherwise.
*/
boolean isReplaceable();
/**
* Checks if a {@link Block} is solid.
* @return a boolean, true if a Block is solid, false otherwise.
*/
boolean isSolid();
/**
* Checks if a {@link Block} is solid and blocking.
* @return a boolean, true if a Block is solid and blocking, false otherwise.
*/
boolean isSolidBlocking();
/**
* Gets the corresponding {@link MapColors} of a {@link Block}
* @return the corresponding {@link MapColors}.
*/
@NotNull MapColors getMapColor();
/**
* Gets the piston bounding box of a {@link Block}
* @return a {@link String} containing the bounding box of a Block.
*/
String getBoundingBox(); // TODO: Dedicated object?
}

View File

@ -0,0 +1,168 @@
package net.minestom.server.instance.block;
import net.minestom.server.item.Material;
import net.minestom.server.map.MapColors;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class BlockDataImpl implements BlockData {
private final double explosionResistance;
private final @NotNull Material item;
private final double friction;
private final double speedFactor;
private final double jumpFactor;
private final double hardness;
private final boolean blockEntity;
private final int lightEmission;
private final boolean occluding;
private final String pushReaction; // TODO: Dedicated object?
private final boolean blockingMotion;
private final boolean flammable;
private final boolean air;
private final boolean liquid;
private final boolean replaceable;
private final boolean solid;
private final boolean solidBlocking;
private final @NotNull MapColors mapColor;
private final String boundingBox; // TODO: Dedicated object?
BlockDataImpl(
double explosionResistance,
@NotNull Material item,
double friction,
double speedFactor,
double jumpFactor,
boolean blockEntity,
double hardness,
int lightEmission,
boolean occluding,
String pushReaction,
boolean blockingMotion,
boolean flammable,
boolean air,
boolean liquid,
boolean replaceable,
boolean solid,
boolean solidBlocking,
@NotNull MapColors mapColor,
String boundingBox
) {
this.explosionResistance = explosionResistance;
this.item = item;
this.friction = friction;
this.speedFactor = speedFactor;
this.jumpFactor = jumpFactor;
this.hardness = hardness;
this.blockEntity = blockEntity;
this.lightEmission = lightEmission;
this.occluding = occluding;
this.pushReaction = pushReaction;
this.blockingMotion = blockingMotion;
this.air = air;
this.flammable = flammable;
this.liquid = liquid;
this.replaceable = replaceable;
this.solid = solid;
this.solidBlocking = solidBlocking;
this.mapColor = mapColor;
this.boundingBox = boundingBox;
}
@Override
public double getExplosionResistance() {
return explosionResistance;
}
@Override
public @Nullable Material getCorrespondingItem() {
return item;
}
@Override
public double getFriction() {
return friction;
}
@Override
public double getSpeedFactor() {
return speedFactor;
}
@Override
public double getJumpFactor() {
return jumpFactor;
}
@Override
public double getHardness() {
return hardness;
}
@Override
public boolean isBlockEntity() {
return blockEntity;
}
@Override
public int getLightEmission() {
return lightEmission;
}
@Override
public boolean isOccluding() {
return occluding;
}
@Override
public String getPushReaction() {
return pushReaction;
}
@Override
public boolean isBlockingMotion() {
return blockingMotion;
}
@Override
public boolean isFlammable() {
return flammable;
}
@Override
public boolean isAir() {
return air;
}
@Override
public boolean isLiquid() {
return liquid;
}
@Override
public boolean isReplaceable() {
return replaceable;
}
@Override
public boolean isSolid() {
return solid;
}
@Override
public boolean isSolidBlocking() {
return solidBlocking;
}
@Override
public @NotNull MapColors getMapColor() {
return mapColor;
}
@Override
public String getBoundingBox() {
return boundingBox;
}
}

View File

@ -0,0 +1,269 @@
package net.minestom.server.instance.block;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import it.unimi.dsi.fastutil.shorts.Short2ObjectAVLTreeMap;
import it.unimi.dsi.fastutil.shorts.Short2ObjectSortedMap;
import net.minestom.server.MinecraftServer;
import net.minestom.server.item.Material;
import net.minestom.server.map.MapColors;
import net.minestom.server.registry.Registries;
import net.minestom.server.tag.Tag;
import net.minestom.server.utils.NamespaceID;
import net.minestom.server.utils.math.IntRange;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.*;
class BlockImpl implements Block {
private static final Logger LOGGER = LoggerFactory.getLogger(BlockImpl.class);
private static final Short2ObjectSortedMap<BlockData> blockData = new Short2ObjectAVLTreeMap<>();
static {
loadBlockData();
}
private final NamespaceID namespaceID;
private final int blockId;
private final short minStateId, stateId;
private final List<BlockProperty<?>> properties;
protected BlockImpl original = null;
private LinkedHashMap<BlockProperty<?>, Object> propertiesMap;
private NBTCompound compound;
private BlockImpl(NamespaceID namespaceID,
int blockId,
short minStateId, short stateId,
List<BlockProperty<?>> properties,
LinkedHashMap<BlockProperty<?>, Object> propertiesMap,
NBTCompound compound) {
this.namespaceID = namespaceID;
this.blockId = blockId;
this.minStateId = minStateId;
this.stateId = stateId;
this.properties = properties;
this.propertiesMap = propertiesMap;
this.compound = compound;
}
private BlockImpl(NamespaceID namespaceID,
int blockId, short minStateId, short stateId,
List<BlockProperty<?>> properties,
LinkedHashMap<BlockProperty<?>, Object> propertiesMap) {
this(namespaceID, blockId, minStateId, stateId, properties, propertiesMap, null);
}
@Override
public @NotNull <T> Block withProperty(@NotNull BlockProperty<T> property, @NotNull T value) {
if (properties.isEmpty()) {
// This block doesn't have any state
return this;
}
final int index = properties.indexOf(property);
if (index == -1) {
// Invalid state
return this;
}
// Find properties map
LinkedHashMap<BlockProperty<?>, Object> map;
if (propertiesMap == null) {
// Represents the first id, create a new map
map = new LinkedHashMap<>();
properties.forEach(prop -> map.put(prop, prop.equals(property) ? value : null));
} else {
// Change property
map = (LinkedHashMap<BlockProperty<?>, Object>) propertiesMap.clone();
map.put(property, value);
}
var block = new BlockImpl(namespaceID, blockId, minStateId, computeId(minStateId, properties, map), properties, map);
block.original = original;
return block;
}
@Override
public <T> @Nullable T getTag(@NotNull Tag<T> tag) {
return tag.read(compound);
}
@Override
public boolean hasTag(@NotNull Tag<?> tag) {
return compound.containsKey(tag.getKey());
}
@Override
public @NotNull <T> Block withTag(@NotNull Tag<T> tag, @Nullable T value) {
if ((compound == null || compound.getKeys().isEmpty()) && value == null) {
// No change
return this;
}
// Apply tag
NBTCompound compound = Objects.requireNonNullElseGet(this.compound, NBTCompound::new);
tag.write(compound, value);
if (compound.getKeys().isEmpty()) {
compound = null;
}
var block = new BlockImpl(namespaceID, blockId, minStateId, stateId, properties, propertiesMap, compound);
block.original = original;
return block;
}
@Override
public @NotNull Block getDefaultBlock() {
return original;
}
@Override
public @NotNull NamespaceID getNamespaceId() {
return namespaceID;
}
@Override
public @NotNull Map<String, String> createPropertiesMap() {
Map<String, String> properties = new HashMap<>();
propertiesMap.forEach((blockProperty, o) -> properties.put(blockProperty.getName(), o.toString()));
return properties;
}
@Override
public int getBlockId() {
return blockId;
}
@Override
public short getStateId() {
return stateId;
}
@Override
public @NotNull BlockData getData() {
return blockData.get(stateId);
}
protected static BlockImpl create(NamespaceID namespaceID, short blockId, short minStateId, short maxStateId,
short defaultStateId, List<BlockProperty<?>> properties) {
var block = new BlockImpl(namespaceID, blockId, minStateId, defaultStateId, properties, computeMap(defaultStateId, properties));
block.original = block;
Block.REGISTRY.register(namespaceID, block,
new IntRange((int) minStateId, (int) maxStateId), requestedStateId -> {
var requestedBlock = new BlockImpl(namespaceID, blockId, minStateId, requestedStateId, properties, computeMap(requestedStateId, properties));
requestedBlock.original = block;
return requestedBlock;
});
return block;
}
private static short computeId(short id, List<BlockProperty<?>> properties,
LinkedHashMap<BlockProperty<?>, Object> propertiesMap) {
int[] factors = computeFactors(properties);
int index = 0;
for (var entry : propertiesMap.entrySet()) {
var property = entry.getKey();
var value = entry.getValue();
if (value != null) {
var values = property.getPossibleValues();
id += values.indexOf(value) * factors[index++];
}
}
return id;
}
private static LinkedHashMap<BlockProperty<?>, Object> computeMap(short deltaId, List<BlockProperty<?>> properties) {
LinkedHashMap<BlockProperty<?>, Object> result = new LinkedHashMap<>();
int[] factors = computeFactors(properties);
int index = 0;
for (var property : properties) {
final int factor = factors[index++];
final int valueIndex = deltaId / factor;
final var possibilities = property.getPossibleValues();
final var value = possibilities.get(valueIndex);
result.put(property, value);
}
return result;
}
private static int[] computeFactors(List<BlockProperty<?>> properties) {
final int size = properties.size();
int[] result = new int[size];
int factor = 1;
ListIterator<BlockProperty<?>> li = properties.listIterator(properties.size());
// Iterate in reverse.
int i = size;
while (li.hasPrevious()) {
var property = li.previous();
result[--i] = factor;
factor *= property.getPossibleValues().size();
}
return result;
}
/**
* Loads the {@link BlockData} from the JAR Resources to the Map.
*/
private static void loadBlockData() {
// E.G. 1_16_5_blocks.json
InputStream blocksIS = BlockImpl.class.getResourceAsStream("/minestom_data/" + MinecraftServer.VERSION_NAME_UNDERSCORED + "_blocks.json");
if (blocksIS == null) {
LOGGER.error("Failed to find blocks.json");
return;
}
// Get map Colors as we will need these
MapColors[] mapColors = MapColors.values();
JsonArray blocks = new Gson().fromJson(new InputStreamReader(blocksIS), JsonArray.class);
for (JsonElement blockEntry : blocks) {
// Load Data
JsonObject block = blockEntry.getAsJsonObject();
double explosionResistance = block.get("explosionResistance").getAsDouble();
double friction = block.get("friction").getAsDouble();
double speedFactor = block.get("speedFactor").getAsDouble();
double jumpFactor = block.get("jumpFactor").getAsDouble();
boolean blockEntity = block.get("blockEntity").getAsBoolean();
Material item = Registries.getMaterial(block.get("itemId").getAsString());
JsonArray states = block.get("states").getAsJsonArray();
for (JsonElement stateEntry : states) {
// Load Data
JsonObject state = stateEntry.getAsJsonObject();
short stateId = state.get("id").getAsShort();
blockData.put(
stateId,
new BlockDataImpl(
explosionResistance,
item,
friction,
speedFactor,
jumpFactor,
blockEntity,
state.get("destroySpeed").getAsDouble(),
state.get("lightEmission").getAsInt(),
state.get("doesOcclude").getAsBoolean(),
state.get("pushReaction").getAsString(),
state.get("blocksMotion").getAsBoolean(),
state.get("isFlammable").getAsBoolean(),
state.get("air").getAsBoolean(),
state.get("isLiquid").getAsBoolean(),
state.get("isReplaceable").getAsBoolean(),
state.get("isSolid").getAsBoolean(),
state.get("isSolidBlocking").getAsBoolean(),
mapColors[state.get("mapColorId").getAsInt()],
state.get("boundingBox").getAsString()
)
);
}
}
}
}

View File

@ -0,0 +1,26 @@
package net.minestom.server.instance.block;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
public class BlockProperty<T> {
private final String name;
private final List<T> possibleValues;
@SafeVarargs
public BlockProperty(@NotNull String name, @NotNull T... possibleValues) {
this.name = name;
this.possibleValues = Arrays.asList(possibleValues);
}
public @NotNull String getName() {
return name;
}
public @NotNull List<T> getPossibleValues() {
return possibleValues;
}
}