Add mechanism for retrieving BlockEntity data

This commit adds a mechanism for retrieving block entity data.
Block entity data is required to support for example text on signs,
banner patterns, or mods such as Domum Ornamentum.
This commit is contained in:
Gerber Lóránt Viktor 2024-03-16 22:09:25 +01:00
parent 10fb88df4b
commit b10d15657a
11 changed files with 442 additions and 1 deletions

View File

@ -56,6 +56,7 @@ import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.World;
import de.bluecolored.bluemap.core.world.block.Block;
import de.bluecolored.bluemap.core.world.block.entity.BlockEntity;
import java.io.IOException;
import java.nio.file.Path;
@ -528,6 +529,11 @@ public class Commands<S> {
lines.put("block-light", block.getBlockLightLevel());
lines.put("sun-light", block.getSunLightLevel());
BlockEntity blockEntity = block.getBlockEntity();
if (blockEntity != null) {
lines.put("block-entity", blockEntity);
}
Object[] textElements = lines.entrySet().stream()
.flatMap(e -> Stream.of(TextColor.GRAY, e.getKey(), ": ", TextColor.WHITE, e.getValue(), "\n"))
.toArray(Object[]::new);

View File

@ -24,6 +24,9 @@
*/
package de.bluecolored.bluemap.core.world;
import de.bluecolored.bluemap.core.world.block.entity.BlockEntity;
import org.jetbrains.annotations.Nullable;
public interface Chunk {
Chunk EMPTY_CHUNK = new Chunk() {};
@ -72,4 +75,5 @@ public interface Chunk {
default int getOceanFloorY(int x, int z) { return 0; }
default @Nullable BlockEntity getBlockEntity(int x, int y, int z) { return null; };
}

View File

@ -28,6 +28,8 @@ import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.LightData;
import de.bluecolored.bluemap.core.world.World;
import de.bluecolored.bluemap.core.world.block.entity.BlockEntity;
import org.jetbrains.annotations.Nullable;
public class Block<T extends Block<T>> {
@ -147,6 +149,10 @@ public class Block<T extends Block<T>> {
return getLightData().getBlockLight();
}
public @Nullable BlockEntity getBlockEntity() {
return getChunk().getBlockEntity(x, y, z);
}
@Override
public String toString() {
if (world != null) {
@ -174,5 +180,4 @@ public class Block<T extends Block<T>> {
protected T self() {
return (T) this;
}
}

View File

@ -0,0 +1,62 @@
package de.bluecolored.bluemap.core.world.block.entity;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class BannerBlockEntity extends BlockEntity {
private final List<Pattern> patterns = new ArrayList<>();
private BannerBlockEntity(Map<String, Object> data) {
super(data);
@SuppressWarnings("unchecked")
List<Map<String, Object>> patterns = (List<Map<String, Object>>) data.getOrDefault("Patterns", List.of());
for (Map<String, Object> compound : patterns) {
this.patterns.add(new Pattern(compound));
}
}
public List<Pattern> getPatterns() {
return patterns;
}
@Override
public String toString() {
return "BannerBlockEntity{" +
"patterns=" + patterns +
"} " + super.toString();
}
public static class Pattern {
private final String code;
private final Color color;
private Pattern(Map<String, Object> data) {
this.code = (String) data.get("Pattern");
this.color = Color.values()[(int) data.get("Color")];
}
public String getCode() {
return code;
}
public Color getColor() {
return color;
}
@Override
public String toString() {
return "Pattern{" +
"code='" + code + '\'' +
", color=" + color +
'}';
}
}
public enum Color {
WHITE, ORANGE, MAGENTA, LIGHT_BLUE, YELLOW, LIME, PINK, GRAY, LIGHT_GRAY, CYAN, PURPLE, BLUE, BROWN, GREEN,
RED, BLACK
}
}

View File

@ -0,0 +1,111 @@
package de.bluecolored.bluemap.core.world.block.entity;
import com.google.gson.reflect.TypeToken;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluenbt.BlueNBT;
import de.bluecolored.bluenbt.NBTDeserializer;
import de.bluecolored.bluenbt.NBTReader;
import de.bluecolored.bluenbt.TypeDeserializer;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.stream.Collectors;
@NBTDeserializer(BlockEntity.BlockEntityDeserializer.class)
public class BlockEntity {
private static final BlueNBT BLUENBT = new BlueNBT();
private static final Map<String, Class<? extends BlockEntity>> ID_MAPPING = Map.of(
"minecraft:sign", SignBlockEntity.class,
"minecraft:skull", SkullBlockEntity.class,
"minecraft:banner", BannerBlockEntity.class
);
protected final String id;
protected final int x, y, z;
protected final boolean keepPacked;
protected BlockEntity(Map<String, Object> data) {
this.id = (String) data.get("id");
this.x = (int) data.get("x");
this.y = (int) data.get("y");
this.z = (int) data.get("z");
this.keepPacked = (byte) data.getOrDefault("keepPacked", (byte) 0) == 1;
}
public String getId() {
return id;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getZ() {
return z;
}
public boolean isKeepPacked() {
return keepPacked;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BlockEntity that = (BlockEntity) o;
return x == that.x && y == that.y && z == that.z && keepPacked == that.keepPacked && Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id, x, y, z, keepPacked);
}
@Override
public String toString() {
return "BlockEntity{" +
"id='" + id + '\'' +
", x=" + x +
", y=" + y +
", z=" + z +
", keepPacked=" + keepPacked +
'}';
}
public static class BlockEntityDeserializer implements TypeDeserializer<BlockEntity> {
@Override
public BlockEntity read(NBTReader reader) throws IOException {
@SuppressWarnings("unchecked") Map<String, Object> data =
(Map<String, Object>) BLUENBT.read(reader, TypeToken.getParameterized(Map.class, String.class, Object.class));
String id = (String) data.get("id");
if (id == null || id.isBlank()) {
return null;
}
Class<? extends BlockEntity> dataClass = ID_MAPPING.getOrDefault(id, BlockEntity.class);
try {
Constructor<? extends BlockEntity> constructor = dataClass.getDeclaredConstructor(Map.class);
if (constructor.trySetAccessible()) {
return constructor.newInstance(data);
}
} catch (NoSuchMethodException e) {
Logger.global.logError(
String.format("No constructor in %s that takes a Map!", dataClass.getCanonicalName()), e);
} catch (Exception e) {
Logger.global.logError("Failed to instantiate BlockEntity instance!", e);
}
return null;
}
}
}

View File

@ -0,0 +1,89 @@
package de.bluecolored.bluemap.core.world.block.entity;
import java.util.List;
import java.util.Map;
public class SignBlockEntity extends BlockEntity {
private final TextData frontText;
private final TextData backText;
@SuppressWarnings("unchecked")
private SignBlockEntity(Map<String, Object> data) {
super(data);
// Versions before 1.20 used a different format
if (data.containsKey("front_text")) {
this.frontText = new TextData((Map<String, Object>) data.getOrDefault("front_text", Map.of()));
this.backText = new TextData((Map<String, Object>) data.getOrDefault("back_text", Map.of()));
} else {
this.frontText = new TextData(
(byte) data.getOrDefault("GlowingText", (byte) 0) == 1,
(String) data.getOrDefault("Color", ""),
List.of(
(String) data.getOrDefault("Text1", ""),
(String) data.getOrDefault("Text2", ""),
(String) data.getOrDefault("Text3", ""),
(String) data.getOrDefault("Text4", "")
)
);
this.backText = new TextData(false, "", List.of());
}
}
public TextData getFrontText() {
return frontText;
}
public TextData getBackText() {
return backText;
}
@Override
public String toString() {
return "SignBlockEntity{" +
"frontText=" + frontText +
", backText=" + backText +
"} " + super.toString();
}
public static class TextData {
private final boolean hasGlowingText;
private final String color;
private final List<String> messages;
@SuppressWarnings("unchecked")
private TextData(Map<String, Object> data) {
this.hasGlowingText = (byte) data.getOrDefault("has_glowing_text", (byte) 0) == 1;
this.color = (String) data.getOrDefault("color", "");
this.messages = (List<String>) data.getOrDefault("messages", List.of());
}
public TextData(boolean hasGlowingText, String color, List<String> messages) {
this.hasGlowingText = hasGlowingText;
this.color = color;
this.messages = messages;
}
public boolean isHasGlowingText() {
return hasGlowingText;
}
public String getColor() {
return color;
}
public List<String> getMessages() {
return messages;
}
@Override
public String toString() {
return "TextData{" +
"hasGlowingText=" + hasGlowingText +
", color='" + color + '\'' +
", messages=" + messages +
'}';
}
}
}

View File

@ -0,0 +1,113 @@
package de.bluecolored.bluemap.core.world.block.entity;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class SkullBlockEntity extends BlockEntity {
private final @Nullable String noteBlockSound;
private final @Nullable String extraType;
private final @Nullable SkullOwner skullOwner;
private SkullBlockEntity(Map<String, Object> data) {
super(data);
this.noteBlockSound = (String) data.get("note_block_sound");
this.extraType = (String) data.get("ExtraType");
@SuppressWarnings("unchecked")
Map<String, Object> ownerData = (Map<String, Object>) data.get("SkullOwner");
this.skullOwner = ownerData != null ? new SkullOwner(ownerData) : null;
}
public @Nullable String getNoteBlockSound() {
return noteBlockSound;
}
public @Nullable String getExtraType() {
return extraType;
}
public SkullOwner getSkullOwner() {
return skullOwner;
}
@Override
public String toString() {
return "SkullBlockEntity{" +
"noteBlockSound='" + noteBlockSound + '\'' +
", extraType='" + extraType + '\'' +
", skullOwner=" + skullOwner +
"} " + super.toString();
}
public static class SkullOwner {
private final @Nullable UUID id;
private final @Nullable String name;
private final List<Texture> textures = new ArrayList<>();
@SuppressWarnings("unchecked")
private SkullOwner(Map<String, Object> data) {
int[] uuidInts = (int[]) data.get("Id");
this.id = new UUID((long) uuidInts[0] << 32 | uuidInts[1], (long) uuidInts[2] << 32 | uuidInts[3]);
this.name = (String) data.get("Name");
Map<String, Object> properties = (Map<String, Object>) data.getOrDefault("Properties", Map.of());
List<Map<String, Object>> textures = (List<Map<String, Object>>) properties.getOrDefault("textures", List.of());
for (Map<String, Object> compound : textures) {
this.textures.add(new Texture(compound));
}
}
public UUID getId() {
return id;
}
public String getName() {
return name;
}
public List<Texture> getTextures() {
return textures;
}
@Override
public String toString() {
return "SkullOwner{" +
"id=" + id +
", name='" + name + '\'' +
", textures=" + textures +
'}';
}
}
public static class Texture {
private final @Nullable String signature;
private final String value;
private Texture(Map<String, Object> data) {
this.signature = (String) data.get("signature");
this.value = (String) data.getOrDefault("value", "");
}
public String getSignature() {
return signature;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return "Texture{" +
"signature='" + signature + '\'' +
", value='" + value + '\'' +
'}';
}
}
}

View File

@ -27,6 +27,7 @@ package de.bluecolored.bluemap.core.world.mca;
import com.google.gson.reflect.TypeToken;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.block.entity.BlockEntity;
import de.bluecolored.bluemap.core.world.mca.data.BlockStateDeserializer;
import de.bluecolored.bluemap.core.world.mca.data.KeyDeserializer;
import de.bluecolored.bluenbt.BlueNBT;
@ -37,6 +38,7 @@ public class MCAUtil {
static {
BLUENBT.register(TypeToken.get(BlockState.class), new BlockStateDeserializer());
BLUENBT.register(TypeToken.get(Key.class), new KeyDeserializer());
BLUENBT.register(TypeToken.get(BlockEntity.class), new BlockEntity.BlockEntityDeserializer());
}
/**

View File

@ -30,12 +30,17 @@ import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.DimensionType;
import de.bluecolored.bluemap.core.world.LightData;
import de.bluecolored.bluemap.core.world.block.entity.BlockEntity;
import de.bluecolored.bluemap.core.world.mca.MCAUtil;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import de.bluecolored.bluenbt.NBTName;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Chunk_1_13 extends MCAChunk {
private static final Key STATUS_EMPTY = new Key("minecraft", "empty");
@ -58,6 +63,7 @@ public class Chunk_1_13 extends MCAChunk {
private final int sectionMin, sectionMax;
final int[] biomes;
private final Map<Long, BlockEntity> blockEntities;
public Chunk_1_13(MCAWorld world, Data data) {
super(world, data);
@ -113,6 +119,10 @@ public class Chunk_1_13 extends MCAChunk {
this.sectionMin = 0;
this.sectionMax = 0;
}
this.blockEntities = level.blockEntities.stream().collect(Collectors.toMap(
it -> (long) it.getY() << 40 | (it.getX() & 0xF) << 4 | it.getZ() & 0xF, it -> it
));
}
@Override
@ -195,6 +205,11 @@ public class Chunk_1_13 extends MCAChunk {
);
}
@Override
public @Nullable BlockEntity getBlockEntity(int x, int y, int z) {
return blockEntities.get((long) y << 40 | (x & 0xF) << 4 | z & 0xF);
}
private @Nullable Section getSection(int y) {
y -= sectionMin;
if (y < 0 || y >= this.sections.length) return null;
@ -273,6 +288,7 @@ public class Chunk_1_13 extends MCAChunk {
private HeightmapsData heightmaps = new HeightmapsData();
private SectionData @Nullable [] sections = null;
private int[] biomes = EMPTY_INT_ARRAY;
@NBTName("TileEntities") private List<BlockEntity> blockEntities = List.of();
}
@Getter

View File

@ -30,6 +30,7 @@ import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.DimensionType;
import de.bluecolored.bluemap.core.world.LightData;
import de.bluecolored.bluemap.core.world.block.entity.BlockEntity;
import de.bluecolored.bluemap.core.world.mca.MCAUtil;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import de.bluecolored.bluemap.core.world.mca.PackedIntArrayAccess;
@ -37,6 +38,10 @@ import de.bluecolored.bluenbt.NBTName;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Chunk_1_16 extends MCAChunk {
private static final Key STATUS_EMPTY = new Key("minecraft", "empty");
@ -57,6 +62,7 @@ public class Chunk_1_16 extends MCAChunk {
private final int sectionMin, sectionMax;
private final int[] biomes;
private final Map<Long, BlockEntity> blockEntities;
public Chunk_1_16(MCAWorld world, Data data) {
super(world, data);
@ -112,6 +118,10 @@ public class Chunk_1_16 extends MCAChunk {
this.sectionMin = 0;
this.sectionMax = 0;
}
this.blockEntities = level.blockEntities.stream().collect(Collectors.toMap(
it -> (long) it.getY() << 40 | (it.getX() & 0xF) << 4 | it.getZ() & 0xF, it -> it
));
}
@Override
@ -191,6 +201,11 @@ public class Chunk_1_16 extends MCAChunk {
return oceanFloorHeights.get((z & 0xF) << 4 | x & 0xF);
}
@Override
public @Nullable BlockEntity getBlockEntity(int x, int y, int z) {
return blockEntities.get((long) y << 40 | (x & 0xF) << 4 | z & 0xF);
}
private @Nullable Section getSection(int y) {
y -= sectionMin;
if (y < 0 || y >= this.sections.length) return null;
@ -261,6 +276,7 @@ public class Chunk_1_16 extends MCAChunk {
private HeightmapsData heightmaps = new HeightmapsData();
private SectionData @Nullable [] sections = null;
private int[] biomes = EMPTY_INT_ARRAY;
@NBTName("TileEntities") private List<BlockEntity> blockEntities = List.of();
}
@Getter

View File

@ -30,6 +30,7 @@ import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.DimensionType;
import de.bluecolored.bluemap.core.world.LightData;
import de.bluecolored.bluemap.core.world.block.entity.BlockEntity;
import de.bluecolored.bluemap.core.world.mca.MCAUtil;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import de.bluecolored.bluemap.core.world.mca.PackedIntArrayAccess;
@ -37,6 +38,10 @@ import de.bluecolored.bluenbt.NBTName;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Chunk_1_18 extends MCAChunk {
private static final Key STATUS_EMPTY = new Key("minecraft", "empty");
@ -57,6 +62,8 @@ public class Chunk_1_18 extends MCAChunk {
private final Section[] sections;
private final int sectionMin, sectionMax;
private final Map<Long, BlockEntity> blockEntities;
public Chunk_1_18(MCAWorld world, Data data) {
super(world, data);
@ -108,6 +115,10 @@ public class Chunk_1_18 extends MCAChunk {
this.sectionMin = 0;
this.sectionMax = 0;
}
this.blockEntities = data.blockEntities.stream().collect(Collectors.toMap(
it -> (long) it.getY() << 40 | (it.getX() & 0xF) << 4 | it.getZ() & 0xF, it -> it
));
}
@Override
@ -182,6 +193,11 @@ public class Chunk_1_18 extends MCAChunk {
return oceanFloorHeights.get((z & 0xF) << 4 | x & 0xF) + worldMinY;
}
@Override
public @Nullable BlockEntity getBlockEntity(int x, int y, int z) {
return blockEntities.get((long) y << 40 | (x & 0xF) << 4 | z & 0xF);
}
private @Nullable Section getSection(int y) {
y -= sectionMin;
if (y < 0 || y >= this.sections.length) return null;
@ -263,6 +279,7 @@ public class Chunk_1_18 extends MCAChunk {
private long inhabitedTime = 0;
private HeightmapsData heightmaps = new HeightmapsData();
private SectionData @Nullable [] sections = null;
@NBTName("block_entities") private List<BlockEntity> blockEntities = List.of();
}
@Getter