diff --git a/src/main/java/net/minestom/server/instance/block/Block.java b/src/main/java/net/minestom/server/instance/block/Block.java index 159433ff2..05a156c51 100644 --- a/src/main/java/net/minestom/server/instance/block/Block.java +++ b/src/main/java/net/minestom/server/instance/block/Block.java @@ -93,7 +93,9 @@ public sealed interface Block extends ProtocolObject, TagReadable, Blocks permit } @Contract(pure = true) - boolean hasNbt(); + default boolean hasNbt() { + return nbt() != null; + } /** * Returns the block handler. diff --git a/src/main/java/net/minestom/server/instance/block/BlockImpl.java b/src/main/java/net/minestom/server/instance/block/BlockImpl.java index f68747ca6..c9f0fe2bd 100644 --- a/src/main/java/net/minestom/server/instance/block/BlockImpl.java +++ b/src/main/java/net/minestom/server/instance/block/BlockImpl.java @@ -12,29 +12,35 @@ import org.jglrxavpok.hephaistos.nbt.NBTCompound; import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound; import java.time.Duration; -import java.util.*; -import java.util.concurrent.atomic.AtomicReference; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; import java.util.function.Function; -final class BlockImpl implements Block { +record BlockImpl(@NotNull Registry.BlockEntry registry, + @NotNull Map properties, + @Nullable NBTCompound nbt, + @Nullable BlockHandler handler) implements Block { // Block state -> block object private static final ObjectArray BLOCK_STATE_MAP = new ObjectArray<>(); + // Block id -> Map + private static final ObjectArray, Block>> POSSIBLE_STATES = new ObjectArray<>(); private static final Registry.Container CONTAINER = new Registry.Container<>(Registry.Resource.BLOCKS, (container, namespace, object) -> { final var stateObject = (Map) object.get("states"); // Loop each state var propertyEntry = new HashMap, Block>(); - AtomicReference, Block>> ref = new AtomicReference<>(); for (var stateEntry : stateObject.entrySet()) { final String query = stateEntry.getKey(); final var stateOverride = (Map) stateEntry.getValue(); final var propertyMap = BlockUtils.parseProperties(query); final Block block = new BlockImpl(Registry.block(namespace, object, stateOverride), - ref, propertyMap, null, null); + propertyMap, null, null); BLOCK_STATE_MAP.set(block.stateId(), block); propertyEntry.put(propertyMap, block); } - ref.setPlain(Map.copyOf(propertyEntry)); + POSSIBLE_STATES.set(((Number) object.get("id")).intValue(), Map.copyOf(propertyEntry)); // Register default state final int defaultState = ((Number) object.get("defaultStateId")).intValue(); container.register(getState(defaultState)); @@ -46,6 +52,7 @@ final class BlockImpl implements Block { static { BLOCK_STATE_MAP.trim(); + POSSIBLE_STATES.trim(); } static Block get(@NotNull String namespace) { @@ -68,30 +75,16 @@ final class BlockImpl implements Block { return CONTAINER.values(); } - private final Registry.BlockEntry registry; - private final AtomicReference, Block>> possibleProperties; - private final Map properties; - private final NBTCompound nbt; - private final BlockHandler handler; - - private int hashCode; // Cache - - BlockImpl(@NotNull Registry.BlockEntry registry, - @NotNull AtomicReference, Block>> possibleProperties, - @NotNull Map properties, - @Nullable NBTCompound nbt, - @Nullable BlockHandler handler) { - this.registry = registry; - this.possibleProperties = possibleProperties; - this.properties = properties; - this.nbt = nbt; - this.handler = handler; + public BlockImpl { + properties = Map.copyOf(properties); } @Override public @NotNull Block withProperty(@NotNull String property, @NotNull String value) { var properties = new HashMap<>(this.properties); - properties.replace(property, value); + final String oldProperty = properties.replace(property, value); + if (oldProperty == null) + throw new IllegalArgumentException("Property " + property + " does not exist"); return compute(properties); } @@ -111,27 +104,12 @@ final class BlockImpl implements Block { var temporaryNbt = new MutableNBTCompound(Objects.requireNonNullElse(nbt, NBTCompound.EMPTY)); tag.write(temporaryNbt, value); final var finalNbt = temporaryNbt.getSize() > 0 ? NBT_CACHE.get(temporaryNbt.toCompound(), Function.identity()) : null; - return new BlockImpl(registry, possibleProperties, properties, finalNbt, handler); + return new BlockImpl(registry, properties, finalNbt, handler); } @Override public @NotNull Block withHandler(@Nullable BlockHandler handler) { - return new BlockImpl(registry, possibleProperties, properties, nbt, handler); - } - - @Override - public boolean hasNbt() { - return nbt != null; - } - - @Override - public @Nullable BlockHandler handler() { - return handler; - } - - @Override - public @NotNull Map properties() { - return Collections.unmodifiableMap(properties); + return new BlockImpl(registry, properties, nbt, handler); } @Override @@ -139,47 +117,18 @@ final class BlockImpl implements Block { return possibleProperties().values(); } - @Override - public @NotNull Registry.BlockEntry registry() { - return registry; - } - @Override public @Nullable T getTag(@NotNull Tag tag) { return nbt != null ? tag.read(nbt) : null; } private Map, Block> possibleProperties() { - return possibleProperties.getPlain(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - BlockImpl block = (BlockImpl) o; - return stateId() == block.stateId() && - Objects.equals(nbt, block.nbt) && - Objects.equals(handler, block.handler); - } - - @Override - public int hashCode() { - int result = hashCode; - if (result == 0) { - result = Objects.hash(stateId(), nbt, handler); - this.hashCode = result; - } - return result; + return POSSIBLE_STATES.get(id()); } @Override public String toString() { - return name() + "{" + - "properties=" + properties + - ", nbt=" + nbt + - ", handler=" + handler + - '}'; + return String.format("%s{properties=%s, nbt=%s, handler=%s}", name(), properties, nbt, handler); } private Block compute(Map properties) { @@ -187,7 +136,6 @@ final class BlockImpl implements Block { Block block = possibleProperties().get(properties); if (block == null) throw new IllegalArgumentException("Invalid properties: " + properties + " for block " + this); - return nbt == null && handler == null ? block : - new BlockImpl(block.registry(), possibleProperties, block.properties(), nbt, handler); + return nbt == null && handler == null ? block : new BlockImpl(block.registry(), block.properties(), nbt, handler); } } diff --git a/src/main/java/net/minestom/server/tag/Tag.java b/src/main/java/net/minestom/server/tag/Tag.java index 8a1065528..b16716771 100644 --- a/src/main/java/net/minestom/server/tag/Tag.java +++ b/src/main/java/net/minestom/server/tag/Tag.java @@ -50,7 +50,14 @@ public class Tag { * Writing will override all tags. Proceed with caution. */ @ApiStatus.Experimental - public static final Tag NBT = new Tag<>(null, NBTCompoundLike::toCompound, MutableNBTCompound::copyFrom); + public static final Tag NBT = new Tag<>(null, NBTCompoundLike::toCompound, + (original, updated) -> { + if (updated == null) { + original.clear(); + return; + } + original.copyFrom(updated); + }); private final String key; private final Function readFunction; diff --git a/src/test/java/instance/BlockTest.java b/src/test/java/instance/BlockTest.java index 8a1750912..70cfbdc54 100644 --- a/src/test/java/instance/BlockTest.java +++ b/src/test/java/instance/BlockTest.java @@ -1,19 +1,51 @@ package instance; import net.minestom.server.instance.block.Block; +import org.jglrxavpok.hephaistos.nbt.NBT; import org.jglrxavpok.hephaistos.nbt.NBTCompound; -import org.jglrxavpok.hephaistos.nbt.NBTInt; import org.junit.jupiter.api.Test; import java.util.Map; +import java.util.Objects; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; public class BlockTest { + @Test + public void testNBT() { + Block block = Block.CHEST; + assertFalse(block.hasNbt()); + assertNull(block.nbt()); + + var nbt = new NBTCompound(Map.of("key", NBT.Int(5))); + block = block.withNbt(nbt); + assertTrue(block.hasNbt()); + assertEquals(block.nbt(), nbt); + + block = block.withNbt(null); + assertFalse(block.hasNbt()); + assertNull(block.nbt()); + } + + @Test + public void testProperty() { + Block block = Block.CHEST; + assertEquals(block.properties(), Objects.requireNonNull(Block.fromBlockId(block.id())).properties()); + + for (var possible : block.possibleStates()) { + assertEquals(possible, block.withProperties(possible.properties())); + } + + assertEquals(block.withProperty("facing", "north").getProperty("facing"), "north"); + assertNotEquals(block.withProperty("facing", "north"), block.withProperty("facing", "south")); + + assertThrows(Exception.class, () -> block.withProperty("random", "randomKey")); + } + @Test public void testEquality() { - var nbt = new NBTCompound(Map.of("key", new NBTInt(5))); + var nbt = new NBTCompound(Map.of("key", NBT.Int(5))); Block b1 = Block.CHEST; Block b2 = Block.CHEST; assertEquals(b1.withNbt(nbt), b2.withNbt(nbt)); @@ -21,4 +53,11 @@ public class BlockTest { assertEquals(b1.withProperty("facing", "north").getProperty("facing"), "north"); assertEquals(b1.withProperty("facing", "north"), b2.withProperty("facing", "north")); } + + @Test + public void testMutability() { + Block block = Block.CHEST; + assertThrows(Exception.class, () -> block.properties().put("facing", "north")); + assertThrows(Exception.class, () -> block.withProperty("facing", "north").properties().put("facing", "south")); + } }