net.minestom.server.instance.block.BlockImpl Maven / Gradle / Ivy
Show all versions of minestom-snapshots Show documentation
package net.minestom.server.instance.block;
import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.minestom.server.registry.Registry;
import net.minestom.server.tag.Tag;
import net.minestom.server.utils.block.BlockUtils;
import net.minestom.server.utils.collection.MergedMap;
import net.minestom.server.utils.collection.ObjectArray;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import org.jetbrains.annotations.Unmodifiable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
record BlockImpl(@NotNull Registry.BlockEntry registry,
int propertiesArray,
@Nullable CompoundBinaryTag nbt,
@Nullable BlockHandler handler) implements Block {
/**
* Number of bits used to store the index of a property value.
*
* 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
private static final ObjectArray BLOCK_STATE_MAP = ObjectArray.singleThread();
// Block id -> valid property keys (order is important for lookup)
private static final ObjectArray PROPERTIES_TYPE = ObjectArray.singleThread();
// Block id -> Map
private static final ObjectArray> POSSIBLE_STATES = ObjectArray.singleThread();
private static final Registry.Container CONTAINER = Registry.createStaticContainer(Registry.Resource.BLOCKS,
(namespace, properties) -> {
final int blockId = properties.getInt("id");
final Registry.Properties stateObject = properties.section("states");
// Retrieve properties
PropertyType[] propertyTypes;
{
Registry.Properties stateProperties = properties.section("properties");
if (stateProperties != null) {
final int stateCount = stateProperties.size();
if (stateCount > MAX_STATES) {
throw new IllegalStateException("Too many properties for block " + namespace);
}
propertyTypes = new PropertyType[stateCount];
int i = 0;
for (var entry : stateProperties) {
final var k = entry.getKey();
final var v = (List) entry.getValue();
propertyTypes[i++] = new PropertyType(k, v);
}
} else {
propertyTypes = new PropertyType[0];
}
}
PROPERTIES_TYPE.set(blockId, propertyTypes);
// Retrieve block states
{
final int propertiesCount = stateObject.size();
int[] propertiesKeys = new int[propertiesCount];
BlockImpl[] blocksValues = new BlockImpl[propertiesCount];
int propertiesOffset = 0;
for (var stateEntry : stateObject) {
final String query = stateEntry.getKey();
final var stateOverride = (Map) stateEntry.getValue();
final var propertyMap = BlockUtils.parseProperties(query);
assert propertyTypes.length == propertyMap.size();
int propertiesValue = 0;
for (Map.Entry entry : propertyMap.entrySet()) {
final byte keyIndex = findKeyIndex(propertyTypes, entry.getKey(), null);
final byte valueIndex = findValueIndex(propertyTypes[keyIndex], entry.getValue(), null);
propertiesValue = updateIndex(propertiesValue, keyIndex, valueIndex);
}
var mainProperties = Registry.Properties.fromMap(new MergedMap<>(stateOverride, properties.asMap()));
final BlockImpl block = new BlockImpl(Registry.block(namespace, mainProperties),
propertiesValue, null, null);
BLOCK_STATE_MAP.set(block.stateId(), block);
propertiesKeys[propertiesOffset] = propertiesValue;
blocksValues[propertiesOffset++] = block;
}
POSSIBLE_STATES.set(blockId, new Int2ObjectArrayMap<>(propertiesKeys, blocksValues, propertiesOffset));
}
// Register default state
final int defaultState = properties.getInt("defaultStateId");
return getState(defaultState);
});
static {
PROPERTIES_TYPE.trim();
BLOCK_STATE_MAP.trim();
POSSIBLE_STATES.trim();
}
static Block get(@NotNull String namespace) {
return CONTAINER.get(namespace);
}
static Block getSafe(@NotNull String namespace) {
return CONTAINER.getSafe(namespace);
}
static Block getId(int id) {
return CONTAINER.getId(id);
}
static Block getState(int stateId) {
return BLOCK_STATE_MAP.get(stateId);
}
static Collection values() {
return CONTAINER.values();
}
@Override
public @NotNull Block withProperty(@NotNull String property, @NotNull String value) {
final PropertyType[] propertyTypes = PROPERTIES_TYPE.get(id());
assert propertyTypes != null;
final byte keyIndex = findKeyIndex(propertyTypes, property, this);
final byte valueIndex = findValueIndex(propertyTypes[keyIndex], value, this);
final int updatedProperties = updateIndex(propertiesArray, keyIndex, valueIndex);
return compute(updatedProperties);
}
@Override
public @NotNull Block withProperties(@NotNull Map<@NotNull String, @NotNull String> properties) {
if (properties.isEmpty()) return this;
final PropertyType[] propertyTypes = PROPERTIES_TYPE.get(id());
assert propertyTypes != null;
int updatedProperties = this.propertiesArray;
for (Map.Entry entry : properties.entrySet()) {
final byte keyIndex = findKeyIndex(propertyTypes, entry.getKey(), this);
final byte valueIndex = findValueIndex(propertyTypes[keyIndex], entry.getValue(), this);
updatedProperties = updateIndex(updatedProperties, keyIndex, valueIndex);
}
return compute(updatedProperties);
}
@Override
public @NotNull Block withTag(@NotNull Tag tag, @Nullable T value) {
var builder = CompoundBinaryTag.builder();
if (nbt != null) builder.put(nbt);
tag.write(builder, value);
final CompoundBinaryTag temporaryNbt = builder.build();
final CompoundBinaryTag finalNbt = temporaryNbt.size() > 0 ? temporaryNbt : null;
return new BlockImpl(registry, propertiesArray, finalNbt, handler);
}
@Override
public @NotNull Block withNbt(@Nullable CompoundBinaryTag compound) {
return new BlockImpl(registry, propertiesArray, compound, handler);
}
@Override
public @NotNull Block withHandler(@Nullable BlockHandler handler) {
return new BlockImpl(registry, propertiesArray, nbt, handler);
}
@Override
public @Unmodifiable @NotNull Map properties() {
final PropertyType[] propertyTypes = PROPERTIES_TYPE.get(id());
assert propertyTypes != null;
final int length = propertyTypes.length;
if (length == 0) return Map.of();
String[] keys = new String[length];
String[] values = new String[length];
for (int i = 0; i < length; i++) {
PropertyType property = propertyTypes[i];
keys[i] = property.key();
final int index = extractIndex(propertiesArray, i);
values[i] = property.values().get(index);
}
return Object2ObjectMaps.unmodifiable(new Object2ObjectArrayMap<>(keys, values, length));
}
@Override
public @NotNull Block defaultState() {
return Block.fromBlockId(id());
}
@Override
public @NotNull Collection<@NotNull Block> possibleStates() {
return Collection.class.cast(possibleProperties().values());
}
@Override
public @UnknownNullability T getTag(@NotNull Tag tag) {
return tag.read(Objects.requireNonNullElse(nbt, CompoundBinaryTag.empty()));
}
private Int2ObjectArrayMap possibleProperties() {
return POSSIBLE_STATES.get(id());
}
@Override
public String toString() {
return String.format("%s{properties=%s, nbt=%s, handler=%s}", name(), properties(), nbt, handler);
}
@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(int updatedProperties) {
if (updatedProperties == this.propertiesArray) return this;
final BlockImpl block = possibleProperties().get(updatedProperties);
assert block != null;
// 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) {
for (byte i = 0; i < properties.length; i++) {
if (properties[i].key().equals(key)) return i;
}
if (block != null) {
throw new IllegalArgumentException("Property " + key + " is not valid for block " + block);
} else {
throw new IllegalArgumentException("Unknown property key: " + key);
}
}
private static byte findValueIndex(PropertyType propertyType, String value, BlockImpl block) {
final List values = propertyType.values();
final byte index = (byte) values.indexOf(value);
if (index != -1) return index;
if (block != null) {
throw new IllegalArgumentException("Property " + propertyType.key() + " value " + value + " is not valid for block " + block);
} else {
throw new IllegalArgumentException("Unknown property value: " + value);
}
}
private record PropertyType(String key, List values) {
}
static int updateIndex(int value, int index, byte newValue) {
final int position = index * BITS_PER_INDEX;
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;
}
static int extractIndex(int value, int index) {
final int position = index * BITS_PER_INDEX;
final int mask = (1 << BITS_PER_INDEX) - 1;
return ((value >> position) & mask);
}
}