diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5e9620582..8e8c393cd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ adventure = "4.9.3" kotlin = "1.6.10" hydrazine = "1.7.2" dependencyGetter = "v1.0.1" -minestomData = "801b8007cf" +minestomData = "c7caadddbf" hephaistos = "2.4.1" jetbrainsAnnotations = "23.0.0" 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 cea0426db..c69286466 100644 --- a/src/main/java/net/minestom/server/instance/block/BlockImpl.java +++ b/src/main/java/net/minestom/server/instance/block/BlockImpl.java @@ -2,6 +2,8 @@ package net.minestom.server.instance.block; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; import net.minestom.server.registry.Registry; import net.minestom.server.tag.Tag; import net.minestom.server.utils.ArrayUtils; @@ -9,46 +11,71 @@ import net.minestom.server.utils.ObjectArray; import net.minestom.server.utils.block.BlockUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; import org.jglrxavpok.hephaistos.nbt.NBTCompound; import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound; import java.time.Duration; +import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.function.Function; record BlockImpl(@NotNull Registry.BlockEntry registry, - @NotNull Map properties, + @NotNull String[] propertiesArray, @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<>(); + // Block id -> valid property keys (order is important for lookup) + private static final ObjectArray PROPERTIES_KEYS = new ObjectArray<>(); + // Block id -> Map + private static final ObjectArray> POSSIBLE_STATES = new ObjectArray<>(); private static final Registry.Container CONTAINER = Registry.createContainer(Registry.Resource.BLOCKS, (namespace, object) -> { + final int blockId = ((Number) object.get("id")).intValue(); final var stateObject = (Map) object.get("states"); - // Retrieve the block states + + // Retrieve properties + String[] possibleProperties = new String[0]; + { + var properties = (Map) object.get("properties"); + if (properties != null) { + possibleProperties = new String[properties.size()]; + int i = 0; + for (var entry : properties.entrySet()) { + possibleProperties[i++] = entry.getKey(); + } + } + } + PROPERTIES_KEYS.set(blockId, possibleProperties); + + // Retrieve block states { final var stateEntries = stateObject.entrySet(); final int propertiesCount = stateEntries.size(); - Map[] propertiesKeys = new Map[propertiesCount]; - Block[] blocksValues = new Block[propertiesCount]; + PropertiesHolder[] propertiesKeys = new PropertiesHolder[propertiesCount]; + BlockImpl[] blocksValues = new BlockImpl[propertiesCount]; int propertiesOffset = 0; for (var stateEntry : stateEntries) { 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), - propertyMap, null, null); + + String[] propertiesArray = new String[possibleProperties.length]; + int i = 0; + for (var entry : propertyMap.entrySet()) { + propertiesArray[i++] = entry.getValue(); + } + + final BlockImpl block = new BlockImpl(Registry.block(namespace, object, stateOverride), + propertiesArray, null, null); BLOCK_STATE_MAP.set(block.stateId(), block); - propertiesKeys[propertiesOffset] = propertyMap; + propertiesKeys[propertiesOffset] = new PropertiesHolder(propertiesArray); blocksValues[propertiesOffset++] = block; } - POSSIBLE_STATES.set(((Number) object.get("id")).intValue(), - ArrayUtils.toMap(propertiesKeys, blocksValues, propertiesOffset)); + POSSIBLE_STATES.set(blockId, ArrayUtils.toMap(propertiesKeys, blocksValues, propertiesOffset)); } // Register default state final int defaultState = ((Number) object.get("defaultStateId")).intValue(); @@ -84,28 +111,32 @@ record BlockImpl(@NotNull Registry.BlockEntry registry, return CONTAINER.values(); } - public BlockImpl { - properties = Map.copyOf(properties); - } - @Override public @NotNull Block withProperty(@NotNull String property, @NotNull String value) { - var properties = new HashMap<>(this.properties); - final String oldProperty = properties.replace(property, value); - if (oldProperty == null) - throw new IllegalArgumentException("Property " + property + " does not exist"); + final String[] keys = PROPERTIES_KEYS.get(id()); + final int index = ArrayUtils.indexOf(keys, property); + if (index == -1) { + throw new IllegalArgumentException("Property " + property + " is not valid for block " + this); + } + var properties = this.propertiesArray.clone(); + properties[index] = value; return compute(properties); } @Override public @NotNull Block withProperties(@NotNull Map<@NotNull String, @NotNull String> properties) { if (properties.isEmpty()) return this; - if (this.properties.size() == properties.size()) { - return compute(properties); // Map should be complete + final String[] keys = PROPERTIES_KEYS.get(id()); + assert keys != null; + String[] result = this.propertiesArray.clone(); + for (var entry : properties.entrySet()) { + final int index = ArrayUtils.indexOf(keys, entry.getKey()); + if (index == -1) { + throw new IllegalArgumentException("Property " + entry.getKey() + " is not valid for block " + this); + } + result[index] = entry.getValue(); } - var newProperties = new HashMap<>(this.properties); - newProperties.putAll(properties); - return compute(newProperties); + return compute(result); } @Override @@ -113,17 +144,25 @@ record BlockImpl(@NotNull Registry.BlockEntry registry, 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, properties, finalNbt, handler); + return new BlockImpl(registry, propertiesArray, finalNbt, handler); } @Override public @NotNull Block withHandler(@Nullable BlockHandler handler) { - return new BlockImpl(registry, properties, nbt, handler); + return new BlockImpl(registry, propertiesArray, nbt, handler); + } + + @Override + public @Unmodifiable @NotNull Map properties() { + final String[] keys = PROPERTIES_KEYS.get(id()); + assert keys != null; + var map = new Object2ObjectArrayMap<>(keys, propertiesArray, keys.length); + return Map.class.cast(Object2ObjectMaps.unmodifiable(map)); } @Override public @NotNull Collection<@NotNull Block> possibleStates() { - return possibleProperties().values(); + return Collection.class.cast(possibleProperties().values()); } @Override @@ -131,20 +170,54 @@ record BlockImpl(@NotNull Registry.BlockEntry registry, return tag.read(Objects.requireNonNullElse(nbt, NBTCompound.EMPTY)); } - private Map, Block> possibleProperties() { + private Map possibleProperties() { return POSSIBLE_STATES.get(id()); } @Override public String toString() { - return String.format("%s{properties=%s, nbt=%s, handler=%s}", name(), properties, nbt, handler); + return String.format("%s{properties=%s, nbt=%s, handler=%s}", name(), properties(), nbt, handler); } - private Block compute(Map properties) { - if (this.properties.equals(properties)) return this; - Block block = possibleProperties().get(properties); + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof BlockImpl block)) return false; + return stateId() == block.stateId() && Objects.equals(nbt, block.nbt) && Objects.equals(handler, block.handler); + } + + @Override + public int hashCode() { + return Objects.hash(stateId(), nbt, handler); + } + + private Block compute(String[] properties) { + if (Arrays.equals(propertiesArray, properties)) return this; + BlockImpl block = possibleProperties().get(new PropertiesHolder(properties)); if (block == null) - throw new IllegalArgumentException("Invalid properties: " + properties + " for block " + this); - return nbt == null && handler == null ? block : new BlockImpl(block.registry(), block.properties(), nbt, handler); + throw new IllegalArgumentException("Invalid properties: " + Arrays.toString(properties) + " for block " + this); + return nbt == null && handler == null ? block : new BlockImpl(block.registry(), block.propertiesArray, nbt, handler); + } + + private static final class PropertiesHolder { + private final String[] properties; + private final int hashCode; + + public PropertiesHolder(String[] properties) { + this.properties = properties; + this.hashCode = Arrays.hashCode(properties); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PropertiesHolder that)) return false; + return Arrays.equals(properties, that.properties); + } + + @Override + public int hashCode() { + return hashCode; + } } } diff --git a/src/main/java/net/minestom/server/utils/ArrayUtils.java b/src/main/java/net/minestom/server/utils/ArrayUtils.java index fb5cfe494..75b6ff5ff 100644 --- a/src/main/java/net/minestom/server/utils/ArrayUtils.java +++ b/src/main/java/net/minestom/server/utils/ArrayUtils.java @@ -64,4 +64,33 @@ public final class ArrayUtils { list.getElements(0, array, 0, array.length); return array; } + + private static final int INDEX_NOT_FOUND = -1; + + public static int indexOf(final Object[] array, final Object objectToFind) { + return indexOf(array, objectToFind, 0); + } + + public static int indexOf(final Object[] array, final Object objectToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + if (objectToFind == null) { + for (int i = startIndex; i < array.length; i++) { + if (array[i] == null) { + return i; + } + } + } else { + for (int i = startIndex; i < array.length; i++) { + if (objectToFind.equals(array[i])) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } }