Skip to content

Commit 2966134

Browse files
authored
Re-implement backing state maps (#2883)
* Re-implement backing state maps This uses a single array combined with efficiently computed indexes to avoid needing to do entire map equality comparisons or hold large hash-based tables, improving memory and CPU usage. * Give a good error message on property mismatch
1 parent e54c11d commit 2966134

File tree

6 files changed

+390
-161
lines changed

6 files changed

+390
-161
lines changed

worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockState.java

Lines changed: 18 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,12 @@
1919

2020
package com.sk89q.worldedit.world.block;
2121

22-
import com.google.common.collect.ImmutableList;
23-
import com.google.common.collect.ImmutableMap;
24-
import com.google.common.collect.Lists;
25-
import com.google.common.collect.Table;
26-
import com.google.common.collect.Tables;
27-
import com.sk89q.worldedit.WorldEdit;
28-
import com.sk89q.worldedit.extension.platform.Capability;
29-
import com.sk89q.worldedit.extension.platform.Watchdog;
3022
import com.sk89q.worldedit.internal.block.BlockStateIdAccess;
3123
import com.sk89q.worldedit.registry.state.Property;
3224
import com.sk89q.worldedit.util.concurrency.LazyReference;
33-
import it.unimi.dsi.fastutil.objects.Reference2ObjectArrayMap;
3425
import org.enginehub.linbus.tree.LinCompoundTag;
3526

36-
import java.util.Collections;
3727
import java.util.HashSet;
38-
import java.util.List;
3928
import java.util.Map;
4029
import java.util.Objects;
4130
import java.util.Set;
@@ -62,141 +51,46 @@ public void setInternalId(BlockState blockState, int internalId) {
6251

6352
private final BlockType blockType;
6453
private final Map<Property<?>, Object> values;
54+
private final int stateListIndex;
6555

6656
private final BaseBlock emptyBaseBlock;
6757
private final LazyReference<String> lazyStringRepresentation;
6858

69-
// Neighbouring state table.
70-
private Table<Property<?>, Object, BlockState> states;
71-
7259
/**
7360
* The internal ID of the block state.
7461
*/
7562
private volatile int internalId = BlockStateIdAccess.invalidId();
7663

77-
BlockState(BlockType blockType) {
78-
this(blockType, Collections.emptyMap());
79-
}
80-
81-
BlockState(BlockType blockType, Map<Property<?>, Object> values) {
64+
BlockState(BlockType blockType, Map<Property<?>, Object> values, int stateListIndex) {
8265
this.blockType = blockType;
8366
this.values = values;
67+
this.stateListIndex = stateListIndex;
8468
this.emptyBaseBlock = new BaseBlock(this);
8569
this.lazyStringRepresentation = LazyReference.from(BlockStateHolder.super::getAsString);
8670
}
8771

88-
/**
89-
* Generates a map of all possible states for a block type.
90-
*
91-
* @param blockType The block type
92-
* @return The map of states
93-
*/
94-
static Map<Map<Property<?>, Object>, BlockState> generateStateMap(BlockType blockType) {
95-
List<? extends Property<?>> properties = blockType.getProperties();
96-
ImmutableMap.Builder<Map<Property<?>, Object>, BlockState> stateMapBuilder = null;
97-
98-
if (!properties.isEmpty()) {
99-
// Create a list of lists of values, with a copy of the underlying lists
100-
List<List<Object>> separatedValues = Lists.newArrayListWithCapacity(properties.size());
101-
for (Property<?> prop : properties) {
102-
separatedValues.add(ImmutableList.copyOf(prop.values()));
103-
}
104-
105-
List<List<Object>> valueLists = Lists.cartesianProduct(separatedValues);
106-
stateMapBuilder = ImmutableMap.builderWithExpectedSize(valueLists.size());
107-
for (List<Object> valueList : valueLists) {
108-
int valueCount = valueList.size();
109-
Map<Property<?>, Object> valueMap = new Reference2ObjectArrayMap<>(valueCount);
110-
for (int i = 0; i < valueCount; i++) {
111-
Property<?> property = properties.get(i);
112-
Object value = valueList.get(i);
113-
valueMap.put(property, value);
114-
}
115-
valueMap = Collections.unmodifiableMap(valueMap);
116-
stateMapBuilder.put(valueMap, new BlockState(blockType, valueMap));
117-
}
118-
}
119-
120-
ImmutableMap<Map<Property<?>, Object>, BlockState> stateMap;
121-
122-
if (stateMapBuilder == null) {
123-
// No properties.
124-
stateMap = ImmutableMap.of(ImmutableMap.of(), new BlockState(blockType));
125-
} else {
126-
stateMap = stateMapBuilder.build();
127-
}
128-
129-
Watchdog watchdog = WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.GAME_HOOKS)
130-
.getWatchdog();
131-
long startTime = System.currentTimeMillis();
132-
133-
for (BlockState state : stateMap.values()) {
134-
state.populate(stateMap);
135-
136-
// Sometimes loading can take a while. This is the perfect spot to let MC know we're working.
137-
if (watchdog != null) {
138-
watchdog.tick();
139-
}
140-
}
141-
long timeTaken = System.currentTimeMillis() - startTime;
142-
if (timeTaken > 5000) {
143-
WorldEdit.logger.warn("Took more than 5 seconds to generate complete state map for " + blockType.id() + ". This block is likely improperly using properties. State count: " + stateMap.size() + ". " + timeTaken + "ms elapsed.");
144-
}
145-
146-
return stateMap;
147-
}
148-
149-
/**
150-
* Creates the underlying state table for object lookups.
151-
*
152-
* @param stateMap The state map to generate the table from
153-
*/
154-
private void populate(Map<Map<Property<?>, Object>, BlockState> stateMap) {
155-
Table<Property<?>, Object, BlockState> table = Tables.newCustomTable(
156-
new Reference2ObjectArrayMap<>(this.values.size()),
157-
Reference2ObjectArrayMap::new
158-
);
159-
160-
for (final Map.Entry<Property<?>, Object> entry : this.values.entrySet()) {
161-
final Property<Object> property = (Property<Object>) entry.getKey();
162-
163-
for (Object value : property.values()) {
164-
if (value != entry.getValue()) {
165-
BlockState modifiedState = stateMap.get(this.withValue(property, value));
166-
if (modifiedState != null) {
167-
table.put(property, value, modifiedState);
168-
} else {
169-
WorldEdit.logger.warn(stateMap);
170-
WorldEdit.logger.warn("Found a null state at " + this.withValue(property, value));
171-
}
172-
}
173-
}
174-
}
175-
176-
this.states = Tables.unmodifiableTable(table);
177-
}
178-
179-
private <V> Map<Property<?>, Object> withValue(final Property<V> property, final V value) {
180-
final Map<Property<?>, Object> values = new Reference2ObjectArrayMap<>(this.values.size());
181-
for (Map.Entry<Property<?>, Object> entry : this.values.entrySet()) {
182-
if (entry.getKey().equals(property)) {
183-
values.put(entry.getKey(), value);
184-
} else {
185-
values.put(entry.getKey(), entry.getValue());
186-
}
187-
}
188-
return Collections.unmodifiableMap(values);
189-
}
190-
19172
@Override
19273
public BlockType getBlockType() {
19374
return this.blockType;
19475
}
19576

19677
@Override
19778
public <V> BlockState with(final Property<V> property, final V value) {
198-
BlockState result = states.get(property, value);
199-
return result == null ? this : result;
79+
if (this.stateListIndex == -1) {
80+
return this;
81+
}
82+
Object currentValue = this.values.get(property);
83+
if (Objects.equals(currentValue, value)) {
84+
return this;
85+
}
86+
87+
int newIndex = blockType.getInternalStateList().updateIndexOrInvalid(
88+
this.stateListIndex, property, currentValue, value
89+
);
90+
if (newIndex == -1) {
91+
return this;
92+
}
93+
return blockType.getInternalStateList().get(newIndex);
20094
}
20195

20296
@Override

worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockType.java

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
package com.sk89q.worldedit.world.block;
2121

2222
import com.google.common.collect.ImmutableList;
23-
import com.google.common.collect.Iterables;
2423
import com.sk89q.worldedit.WorldEdit;
2524
import com.sk89q.worldedit.extension.platform.Capability;
2625
import com.sk89q.worldedit.registry.Keyed;
@@ -32,9 +31,10 @@
3231
import com.sk89q.worldedit.world.item.ItemTypes;
3332
import com.sk89q.worldedit.world.registry.BlockMaterial;
3433
import com.sk89q.worldedit.world.registry.LegacyMapper;
35-
import it.unimi.dsi.fastutil.objects.Reference2ObjectArrayMap;
34+
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap;
35+
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
3636

37-
import java.util.Collections;
37+
import java.util.Arrays;
3838
import java.util.List;
3939
import java.util.Map;
4040
import java.util.function.Function;
@@ -46,25 +46,34 @@ public class BlockType implements Keyed {
4646

4747
public static final NamespacedRegistry<BlockType> REGISTRY = new NamespacedRegistry<>("block type", "block_type", "minecraft", true);
4848

49+
private static Map<String, ? extends Property<?>> computeProperties(BlockType self) {
50+
Map<String, ? extends Property<?>> propertiesMap = WorldEdit.getInstance().getPlatformManager()
51+
.queryCapability(Capability.GAME_HOOKS).getRegistries().getBlockRegistry().getProperties(self);
52+
String[] propertyNames = propertiesMap.keySet().toArray(new String[0]);
53+
Arrays.sort(propertyNames);
54+
Object[] properties = new Object[propertyNames.length];
55+
for (int i = 0; i < propertyNames.length; i++) {
56+
properties[i] = propertiesMap.get(propertyNames[i]);
57+
}
58+
return Object2ObjectMaps.unmodifiable(new Object2ObjectArrayMap<>(propertyNames, properties));
59+
}
60+
4961
private final String id;
50-
private final Function<BlockState, BlockState> values;
51-
private final LazyReference<BlockState> defaultState
52-
= LazyReference.from(this::computeDefaultState);
62+
private final LazyReference<BlockState> defaultState;
5363
@SuppressWarnings("this-escape")
5464
private final LazyReference<FuzzyBlockState> emptyFuzzy
5565
= LazyReference.from(() -> new FuzzyBlockState(this));
5666
@SuppressWarnings("this-escape")
5767
private final LazyReference<Map<String, ? extends Property<?>>> properties
58-
= LazyReference.from(this::computeProperties);
68+
= LazyReference.from(() -> computeProperties(this));
69+
@SuppressWarnings("this-escape")
70+
private final LazyReference<BlockTypeStateList> internalStateList =
71+
LazyReference.from(() -> BlockTypeStateList.createFor(this));
5972
@SuppressWarnings("this-escape")
6073
private final LazyReference<BlockMaterial> blockMaterial
6174
= LazyReference.from(() -> WorldEdit.getInstance().getPlatformManager()
6275
.queryCapability(Capability.GAME_HOOKS).getRegistries().getBlockRegistry().getMaterial(this));
6376
@SuppressWarnings("this-escape")
64-
private final LazyReference<Map<Map<Property<?>, Object>, BlockState>> blockStatesMap
65-
= LazyReference.from(() -> BlockState.generateStateMap(this));
66-
67-
@SuppressWarnings("this-escape")
6877
@Deprecated
6978
private final LazyReference<String> name = LazyReference.from(() -> WorldEdit.getInstance().getPlatformManager()
7079
.queryCapability(Capability.GAME_HOOKS).getRegistries().getBlockRegistry().getName(this));
@@ -77,36 +86,25 @@ public BlockType(String id) {
7786
this(id, null);
7887
}
7988

80-
public BlockType(String id, Function<BlockState, BlockState> values) {
89+
public BlockType(String id, Function<BlockState, BlockState> applyDefaultValues) {
8190
// If it has no namespace, assume minecraft.
8291
if (!id.contains(":")) {
8392
id = "minecraft:" + id;
8493
}
8594
this.id = id;
86-
this.values = values;
95+
this.defaultState = LazyReference.from(() -> computeDefaultState(applyDefaultValues));
8796
}
8897

89-
private Map<String, ? extends Property<?>> computeProperties() {
90-
var propertiesMap = WorldEdit.getInstance().getPlatformManager()
91-
.queryCapability(Capability.GAME_HOOKS).getRegistries().getBlockRegistry().getProperties(this);
92-
List<String> sortedPropertyNames = propertiesMap.keySet().stream().sorted().toList();
93-
Map<String, Property<?>> sortedPropertiesMap = new Reference2ObjectArrayMap<>(propertiesMap.size());
94-
for (String propertyName : sortedPropertyNames) {
95-
sortedPropertiesMap.put(propertyName, propertiesMap.get(propertyName));
96-
}
97-
return Collections.unmodifiableMap(sortedPropertiesMap);
98+
BlockTypeStateList getInternalStateList() {
99+
return internalStateList.getValue();
98100
}
99101

100-
private BlockState computeDefaultState() {
101-
BlockState defaultState = Iterables.getFirst(getBlockStatesMap().values(), null);
102-
if (values != null) {
103-
defaultState = values.apply(defaultState);
102+
private BlockState computeDefaultState(Function<BlockState, BlockState> applyDefaultValues) {
103+
BlockState state = getInternalStateList().getFirst();
104+
if (applyDefaultValues != null) {
105+
state = applyDefaultValues.apply(state);
104106
}
105-
return defaultState;
106-
}
107-
108-
private Map<Map<Property<?>, Object>, BlockState> getBlockStatesMap() {
109-
return blockStatesMap.getValue();
107+
return state;
110108
}
111109

112110
/**
@@ -190,7 +188,7 @@ public FuzzyBlockState getFuzzyMatcher() {
190188
* @return All possible states
191189
*/
192190
public List<BlockState> getAllStates() {
193-
return ImmutableList.copyOf(getBlockStatesMap().values());
191+
return getInternalStateList();
194192
}
195193

196194
/**
@@ -199,9 +197,9 @@ public List<BlockState> getAllStates() {
199197
* @return The state, if it exists
200198
*/
201199
public BlockState getState(Map<Property<?>, Object> key) {
202-
BlockState state = getBlockStatesMap().get(key);
203-
checkArgument(state != null, "%s has no state for %s", this, key);
204-
return state;
200+
BlockTypeStateList map = getInternalStateList();
201+
int index = map.calculateIndex(key);
202+
return map.get(index);
205203
}
206204

207205
/**
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* WorldEdit, a Minecraft world manipulation toolkit
3+
* Copyright (C) sk89q <http://www.sk89q.com>
4+
* Copyright (C) WorldEdit team and contributors
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
package com.sk89q.worldedit.world.block;
21+
22+
import com.google.errorprone.annotations.Immutable;
23+
import com.sk89q.worldedit.registry.state.Property;
24+
import it.unimi.dsi.fastutil.objects.AbstractObjectList;
25+
26+
import java.util.Collections;
27+
import java.util.Map;
28+
29+
/**
30+
* A specialized list for looking up block states from a block type.
31+
*/
32+
// Using AbstractObjectList over AbstractList since we don't need the modCount functionality
33+
@Immutable
34+
abstract class BlockTypeStateList extends AbstractObjectList<BlockState> {
35+
static BlockTypeStateList createFor(BlockType blockType) {
36+
if (blockType.getProperties().isEmpty()) {
37+
// Special case, we have only one state: the default state
38+
return new SingletonBlockTypeStateList(new BlockState(blockType, Collections.emptyMap(), 0));
39+
}
40+
return new DefaultBlockTypeStateList(blockType);
41+
}
42+
43+
/**
44+
* Calculates the index in the states array for the given property values.
45+
* This can later be used to perform fast lookups by replacing only a specific property.
46+
*
47+
* @param state the map of property values
48+
* @return the index in the states
49+
*/
50+
public abstract int calculateIndex(Map<Property<?>, ?> state);
51+
52+
/**
53+
* Updates the current index by changing a single property's value.
54+
*
55+
* @param currentIndex the current index
56+
* @param property the property to change
57+
* @param oldValue the old value
58+
* @param newValue the new value
59+
* @return the updated index, or {@code -1} if the property or value is invalid
60+
*/
61+
public abstract int updateIndexOrInvalid(int currentIndex, Property<?> property, Object oldValue, Object newValue);
62+
}

0 commit comments

Comments
 (0)