Skip to content

Commit 6aae0f9

Browse files
committed
Add debug feature to catch illegal block entity map mutation
1 parent b72959f commit 6aae0f9

File tree

4 files changed

+146
-0
lines changed

4 files changed

+146
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.embeddedt.modernfix.common.mixin.feature.blockentity_incorrect_thread;
2+
3+
import net.minecraft.core.BlockPos;
4+
import net.minecraft.core.Registry;
5+
import net.minecraft.world.level.ChunkPos;
6+
import net.minecraft.world.level.Level;
7+
import net.minecraft.world.level.LevelHeightAccessor;
8+
import net.minecraft.world.level.block.entity.BlockEntity;
9+
import net.minecraft.world.level.chunk.ChunkAccess;
10+
import net.minecraft.world.level.chunk.LevelChunkSection;
11+
import net.minecraft.world.level.chunk.UpgradeData;
12+
import net.minecraft.world.level.levelgen.blending.BlendingData;
13+
import org.embeddedt.modernfix.util.ConcurrencySanitizingMap;
14+
import org.spongepowered.asm.mixin.Final;
15+
import org.spongepowered.asm.mixin.Mixin;
16+
import org.spongepowered.asm.mixin.Mutable;
17+
import org.spongepowered.asm.mixin.Shadow;
18+
import org.spongepowered.asm.mixin.injection.At;
19+
import org.spongepowered.asm.mixin.injection.Inject;
20+
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
21+
22+
import java.util.Map;
23+
24+
@Mixin(ChunkAccess.class)
25+
public class ChunkAccessMixin {
26+
@Shadow @Final @Mutable protected Map<BlockPos, BlockEntity> blockEntities;
27+
28+
@Inject(method = "<init>", at = @At("RETURN"))
29+
private void wrapInConcurrencyDetector(ChunkPos chunkPos, UpgradeData upgradeData, LevelHeightAccessor levelHeightAccessor, Registry biomeRegistry, long inhabitedTime, LevelChunkSection[] sections, BlendingData blendingData, CallbackInfo ci) {
30+
if (levelHeightAccessor instanceof Level level) {
31+
this.blockEntities = new ConcurrencySanitizingMap<>(this.blockEntities, ((LevelThreadAccessor)level).getThread());
32+
}
33+
}
34+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.embeddedt.modernfix.common.mixin.feature.blockentity_incorrect_thread;
2+
3+
import net.minecraft.world.level.Level;
4+
import org.spongepowered.asm.mixin.Mixin;
5+
import org.spongepowered.asm.mixin.gen.Accessor;
6+
7+
@Mixin(Level.class)
8+
public interface LevelThreadAccessor {
9+
@Accessor
10+
Thread getThread();
11+
}

common/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ public DefaultSettingMapBuilder put(String key, Boolean value) {
168168
.put("mixin.perf.worldgen_allocation", false) // experimental
169169
.put("mixin.feature.cause_lag_by_disabling_threads", false)
170170
.put("mixin.bugfix.missing_block_entities", false)
171+
.put("mixin.feature.blockentity_incorrect_thread", false)
171172
.put("mixin.perf.clear_mixin_classinfo", false)
172173
.put("mixin.perf.deduplicate_climate_parameters", false)
173174
.put("mixin.bugfix.packet_leak", false)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package org.embeddedt.modernfix.util;
2+
3+
import org.jetbrains.annotations.NotNull;
4+
import org.jetbrains.annotations.Nullable;
5+
6+
import java.util.Collection;
7+
import java.util.Map;
8+
import java.util.Set;
9+
10+
/**
11+
* A map wrapper that throws if the map is ever accessed on the wrong thread.
12+
*/
13+
public class ConcurrencySanitizingMap<K, V> implements Map<K, V> {
14+
private final Map<K, V> map;
15+
private final Thread owner;
16+
17+
public ConcurrencySanitizingMap(Map<K, V> map, Thread owner) {
18+
this.map = map;
19+
this.owner = owner;
20+
}
21+
22+
private void checkThread() {
23+
var current = Thread.currentThread();
24+
if (current != owner) {
25+
throw new IllegalStateException("Map is being accessed on thread " + current + " while owned by " + owner);
26+
}
27+
}
28+
29+
@Override
30+
public int size() {
31+
checkThread();
32+
return map.size();
33+
}
34+
35+
@Override
36+
public boolean isEmpty() {
37+
checkThread();
38+
return map.isEmpty();
39+
}
40+
41+
@Override
42+
public boolean containsKey(Object key) {
43+
checkThread();
44+
return map.containsKey(key);
45+
}
46+
47+
@Override
48+
public boolean containsValue(Object value) {
49+
checkThread();
50+
return map.containsValue(value);
51+
}
52+
53+
@Override
54+
public V get(Object key) {
55+
checkThread();
56+
return map.get(key);
57+
}
58+
59+
@Override
60+
public @Nullable V put(K key, V value) {
61+
checkThread();
62+
return map.put(key, value);
63+
}
64+
65+
@Override
66+
public V remove(Object key) {
67+
checkThread();
68+
return map.remove(key);
69+
}
70+
71+
@Override
72+
public void putAll(@NotNull Map<? extends K, ? extends V> m) {
73+
checkThread();
74+
map.putAll(m);
75+
}
76+
77+
@Override
78+
public void clear() {
79+
checkThread();
80+
map.clear();
81+
}
82+
83+
@Override
84+
public @NotNull Set<K> keySet() {
85+
checkThread();
86+
return map.keySet();
87+
}
88+
89+
@Override
90+
public @NotNull Collection<V> values() {
91+
checkThread();
92+
return map.values();
93+
}
94+
95+
@Override
96+
public @NotNull Set<Entry<K, V>> entrySet() {
97+
checkThread();
98+
return map.entrySet();
99+
}
100+
}

0 commit comments

Comments
 (0)