Use a single int for all block states (#2373)

This commit is contained in:
TheMode 2024-09-04 00:52:01 +02:00 committed by GitHub
parent b1ad94cd1b
commit a3dec124aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,13 +1,11 @@
package net.minestom.server.instance.block; package net.minestom.server.instance.block;
import com.github.benmanes.caffeine.cache.Cache; import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
import com.github.benmanes.caffeine.cache.Caffeine;
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.minestom.server.registry.Registry; import net.minestom.server.registry.Registry;
import net.minestom.server.tag.Tag; import net.minestom.server.tag.Tag;
import net.minestom.server.utils.ArrayUtils;
import net.minestom.server.utils.block.BlockUtils; import net.minestom.server.utils.block.BlockUtils;
import net.minestom.server.utils.collection.MergedMap; import net.minestom.server.utils.collection.MergedMap;
import net.minestom.server.utils.collection.ObjectArray; import net.minestom.server.utils.collection.ObjectArray;
@ -16,20 +14,30 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability; import org.jetbrains.annotations.UnknownNullability;
import org.jetbrains.annotations.Unmodifiable; import org.jetbrains.annotations.Unmodifiable;
import java.time.Duration; import java.util.Collection;
import java.util.*; import java.util.List;
import java.util.function.Function; import java.util.Map;
import java.util.Objects;
record BlockImpl(@NotNull Registry.BlockEntry registry, record BlockImpl(@NotNull Registry.BlockEntry registry,
byte @NotNull [] propertiesArray, int propertiesArray,
@Nullable CompoundBinaryTag nbt, @Nullable CompoundBinaryTag nbt,
@Nullable BlockHandler handler) implements Block { @Nullable BlockHandler handler) implements Block {
/**
* Number of bits used to store the index of a property value.
* <p>
* Block states are all stored within a single number.
*/
private static final int BITS_PER_INDEX = 4;
private static final int MAX_STATES = Integer.SIZE / BITS_PER_INDEX;
// Block state -> block object // Block state -> block object
private static final ObjectArray<Block> BLOCK_STATE_MAP = ObjectArray.singleThread(); private static final ObjectArray<Block> BLOCK_STATE_MAP = ObjectArray.singleThread();
// Block id -> valid property keys (order is important for lookup) // Block id -> valid property keys (order is important for lookup)
private static final ObjectArray<PropertyType[]> PROPERTIES_TYPE = ObjectArray.singleThread(); private static final ObjectArray<PropertyType[]> PROPERTIES_TYPE = ObjectArray.singleThread();
// Block id -> Map<PropertiesValues, Block> // Block id -> Map<Properties, Block>
private static final ObjectArray<Map<PropertiesHolder, BlockImpl>> POSSIBLE_STATES = ObjectArray.singleThread(); private static final ObjectArray<Int2ObjectArrayMap<BlockImpl>> POSSIBLE_STATES = ObjectArray.singleThread();
private static final Registry.Container<Block> CONTAINER = Registry.createStaticContainer(Registry.Resource.BLOCKS, private static final Registry.Container<Block> CONTAINER = Registry.createStaticContainer(Registry.Resource.BLOCKS,
(namespace, properties) -> { (namespace, properties) -> {
final int blockId = properties.getInt("id"); final int blockId = properties.getInt("id");
@ -41,6 +49,9 @@ record BlockImpl(@NotNull Registry.BlockEntry registry,
Registry.Properties stateProperties = properties.section("properties"); Registry.Properties stateProperties = properties.section("properties");
if (stateProperties != null) { if (stateProperties != null) {
final int stateCount = stateProperties.size(); final int stateCount = stateProperties.size();
if (stateCount > MAX_STATES) {
throw new IllegalStateException("Too many properties for block " + namespace);
}
propertyTypes = new PropertyType[stateCount]; propertyTypes = new PropertyType[stateCount];
int i = 0; int i = 0;
for (var entry : stateProperties) { for (var entry : stateProperties) {
@ -57,7 +68,7 @@ record BlockImpl(@NotNull Registry.BlockEntry registry,
// Retrieve block states // Retrieve block states
{ {
final int propertiesCount = stateObject.size(); final int propertiesCount = stateObject.size();
PropertiesHolder[] propertiesKeys = new PropertiesHolder[propertiesCount]; int[] propertiesKeys = new int[propertiesCount];
BlockImpl[] blocksValues = new BlockImpl[propertiesCount]; BlockImpl[] blocksValues = new BlockImpl[propertiesCount];
int propertiesOffset = 0; int propertiesOffset = 0;
for (var stateEntry : stateObject) { for (var stateEntry : stateObject) {
@ -65,30 +76,26 @@ record BlockImpl(@NotNull Registry.BlockEntry registry,
final var stateOverride = (Map<String, Object>) stateEntry.getValue(); final var stateOverride = (Map<String, Object>) stateEntry.getValue();
final var propertyMap = BlockUtils.parseProperties(query); final var propertyMap = BlockUtils.parseProperties(query);
assert propertyTypes.length == propertyMap.size(); assert propertyTypes.length == propertyMap.size();
byte[] propertiesArray = new byte[propertyTypes.length]; int propertiesValue = 0;
for (var entry : propertyMap.entrySet()) { for (Map.Entry<String, String> entry : propertyMap.entrySet()) {
final byte keyIndex = findKeyIndex(propertyTypes, entry.getKey(), null); final byte keyIndex = findKeyIndex(propertyTypes, entry.getKey(), null);
final byte valueIndex = findValueIndex(propertyTypes[keyIndex], entry.getValue(), null); final byte valueIndex = findValueIndex(propertyTypes[keyIndex], entry.getValue(), null);
propertiesArray[keyIndex] = valueIndex; propertiesValue = updateIndex(propertiesValue, keyIndex, valueIndex);
} }
var mainProperties = Registry.Properties.fromMap(new MergedMap<>(stateOverride, properties.asMap())); var mainProperties = Registry.Properties.fromMap(new MergedMap<>(stateOverride, properties.asMap()));
final BlockImpl block = new BlockImpl(Registry.block(namespace, mainProperties), final BlockImpl block = new BlockImpl(Registry.block(namespace, mainProperties),
propertiesArray, null, null); propertiesValue, null, null);
BLOCK_STATE_MAP.set(block.stateId(), block); BLOCK_STATE_MAP.set(block.stateId(), block);
propertiesKeys[propertiesOffset] = new PropertiesHolder(propertiesArray); propertiesKeys[propertiesOffset] = propertiesValue;
blocksValues[propertiesOffset++] = block; blocksValues[propertiesOffset++] = block;
} }
POSSIBLE_STATES.set(blockId, ArrayUtils.toMap(propertiesKeys, blocksValues, propertiesOffset)); POSSIBLE_STATES.set(blockId, new Int2ObjectArrayMap<>(propertiesKeys, blocksValues, propertiesOffset));
} }
// Register default state // Register default state
final int defaultState = properties.getInt("defaultStateId"); final int defaultState = properties.getInt("defaultStateId");
return getState(defaultState); return getState(defaultState);
}); });
private static final Cache<CompoundBinaryTag, CompoundBinaryTag> NBT_CACHE = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(5))
.weakValues()
.build();
static { static {
PROPERTIES_TYPE.trim(); PROPERTIES_TYPE.trim();
@ -122,9 +129,8 @@ record BlockImpl(@NotNull Registry.BlockEntry registry,
assert propertyTypes != null; assert propertyTypes != null;
final byte keyIndex = findKeyIndex(propertyTypes, property, this); final byte keyIndex = findKeyIndex(propertyTypes, property, this);
final byte valueIndex = findValueIndex(propertyTypes[keyIndex], value, this); final byte valueIndex = findValueIndex(propertyTypes[keyIndex], value, this);
var properties = this.propertiesArray.clone(); final int updatedProperties = updateIndex(propertiesArray, keyIndex, valueIndex);
properties[keyIndex] = valueIndex; return compute(updatedProperties);
return compute(properties);
} }
@Override @Override
@ -132,13 +138,13 @@ record BlockImpl(@NotNull Registry.BlockEntry registry,
if (properties.isEmpty()) return this; if (properties.isEmpty()) return this;
final PropertyType[] propertyTypes = PROPERTIES_TYPE.get(id()); final PropertyType[] propertyTypes = PROPERTIES_TYPE.get(id());
assert propertyTypes != null; assert propertyTypes != null;
byte[] result = this.propertiesArray.clone(); int updatedProperties = this.propertiesArray;
for (var entry : properties.entrySet()) { for (Map.Entry<String, String> entry : properties.entrySet()) {
final byte keyIndex = findKeyIndex(propertyTypes, entry.getKey(), this); final byte keyIndex = findKeyIndex(propertyTypes, entry.getKey(), this);
final byte valueIndex = findValueIndex(propertyTypes[keyIndex], entry.getValue(), this); final byte valueIndex = findValueIndex(propertyTypes[keyIndex], entry.getValue(), this);
result[keyIndex] = valueIndex; updatedProperties = updateIndex(updatedProperties, keyIndex, valueIndex);
} }
return compute(result); return compute(updatedProperties);
} }
@Override @Override
@ -146,8 +152,8 @@ record BlockImpl(@NotNull Registry.BlockEntry registry,
var builder = CompoundBinaryTag.builder(); var builder = CompoundBinaryTag.builder();
if (nbt != null) builder.put(nbt); if (nbt != null) builder.put(nbt);
tag.write(builder, value); tag.write(builder, value);
var temporaryNbt = builder.build(); final CompoundBinaryTag temporaryNbt = builder.build();
final var finalNbt = temporaryNbt.size() > 0 ? NBT_CACHE.get(temporaryNbt, Function.identity()) : null; final CompoundBinaryTag finalNbt = temporaryNbt.size() > 0 ? temporaryNbt : null;
return new BlockImpl(registry, propertiesArray, finalNbt, handler); return new BlockImpl(registry, propertiesArray, finalNbt, handler);
} }
@ -170,9 +176,10 @@ record BlockImpl(@NotNull Registry.BlockEntry registry,
String[] keys = new String[length]; String[] keys = new String[length];
String[] values = new String[length]; String[] values = new String[length];
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
var property = propertyTypes[i]; PropertyType property = propertyTypes[i];
keys[i] = property.key(); keys[i] = property.key();
values[i] = property.values().get(propertiesArray[i]); final int index = extractIndex(propertiesArray, i);
values[i] = property.values().get(index);
} }
return Object2ObjectMaps.unmodifiable(new Object2ObjectArrayMap<>(keys, values, length)); return Object2ObjectMaps.unmodifiable(new Object2ObjectArrayMap<>(keys, values, length));
} }
@ -192,7 +199,7 @@ record BlockImpl(@NotNull Registry.BlockEntry registry,
return tag.read(Objects.requireNonNullElse(nbt, CompoundBinaryTag.empty())); return tag.read(Objects.requireNonNullElse(nbt, CompoundBinaryTag.empty()));
} }
private Map<PropertiesHolder, BlockImpl> possibleProperties() { private Int2ObjectArrayMap<BlockImpl> possibleProperties() {
return POSSIBLE_STATES.get(id()); return POSSIBLE_STATES.get(id());
} }
@ -213,11 +220,14 @@ record BlockImpl(@NotNull Registry.BlockEntry registry,
return Objects.hash(stateId(), nbt, handler); return Objects.hash(stateId(), nbt, handler);
} }
private Block compute(byte[] properties) { private Block compute(int updatedProperties) {
if (Arrays.equals(propertiesArray, properties)) return this; if (updatedProperties == this.propertiesArray) return this;
final BlockImpl block = possibleProperties().get(new PropertiesHolder(properties)); final BlockImpl block = possibleProperties().get(updatedProperties);
assert block != null; assert block != null;
return nbt == null && handler == null ? block : new BlockImpl(block.registry(), block.propertiesArray, nbt, handler); // Reuse the same block instance if possible
if (nbt == null && handler == null) return block;
// Otherwise copy with the nbt and handler
return new BlockImpl(block.registry(), block.propertiesArray, nbt, handler);
} }
private static byte findKeyIndex(PropertyType[] properties, String key, BlockImpl block) { private static byte findKeyIndex(PropertyType[] properties, String key, BlockImpl block) {
@ -245,25 +255,17 @@ record BlockImpl(@NotNull Registry.BlockEntry registry,
private record PropertyType(String key, List<String> values) { private record PropertyType(String key, List<String> values) {
} }
private static final class PropertiesHolder { static int updateIndex(int value, int index, byte newValue) {
private final byte[] properties; final int position = index * BITS_PER_INDEX;
private final int hashCode; final int mask = (1 << BITS_PER_INDEX) - 1;
value &= ~(mask << position); // Clear the bits at the specified position
value |= (newValue & mask) << position; // Set the new bits
return value;
}
public PropertiesHolder(byte[] properties) { static int extractIndex(int value, int index) {
this.properties = properties; final int position = index * BITS_PER_INDEX;
this.hashCode = Arrays.hashCode(properties); final int mask = (1 << BITS_PER_INDEX) - 1;
} return ((value >> position) & mask);
@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;
}
} }
} }