diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/feature/blockentity_incorrect_thread/ChunkAccessMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/feature/blockentity_incorrect_thread/ChunkAccessMixin.java new file mode 100644 index 00000000..1aa9091a --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/feature/blockentity_incorrect_thread/ChunkAccessMixin.java @@ -0,0 +1,34 @@ +package org.embeddedt.modernfix.common.mixin.feature.blockentity_incorrect_thread; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Registry; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import org.embeddedt.modernfix.util.ConcurrencySanitizingMap; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Map; + +@Mixin(ChunkAccess.class) +public class ChunkAccessMixin { + @Shadow @Final @Mutable protected Map blockEntities; + + @Inject(method = "", at = @At("RETURN")) + private void wrapInConcurrencyDetector(ChunkPos chunkPos, UpgradeData upgradeData, LevelHeightAccessor levelHeightAccessor, Registry biomeRegistry, long inhabitedTime, LevelChunkSection[] sections, BlendingData blendingData, CallbackInfo ci) { + if (levelHeightAccessor instanceof Level level) { + this.blockEntities = new ConcurrencySanitizingMap<>(this.blockEntities, ((LevelThreadAccessor)level).getThread()); + } + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/feature/blockentity_incorrect_thread/LevelThreadAccessor.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/feature/blockentity_incorrect_thread/LevelThreadAccessor.java new file mode 100644 index 00000000..8cd13990 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/feature/blockentity_incorrect_thread/LevelThreadAccessor.java @@ -0,0 +1,11 @@ +package org.embeddedt.modernfix.common.mixin.feature.blockentity_incorrect_thread; + +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(Level.class) +public interface LevelThreadAccessor { + @Accessor + Thread getThread(); +} diff --git a/common/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java b/common/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java index f113e866..01fe319b 100644 --- a/common/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/common/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -167,6 +167,7 @@ public class ModernFixEarlyConfig { .put("mixin.perf.worldgen_allocation", false) // experimental .put("mixin.feature.cause_lag_by_disabling_threads", false) .put("mixin.bugfix.missing_block_entities", false) + .put("mixin.feature.blockentity_incorrect_thread", false) .put("mixin.perf.clear_mixin_classinfo", false) .put("mixin.perf.deduplicate_climate_parameters", false) .put("mixin.bugfix.packet_leak", false) diff --git a/common/src/main/java/org/embeddedt/modernfix/util/ConcurrencySanitizingMap.java b/common/src/main/java/org/embeddedt/modernfix/util/ConcurrencySanitizingMap.java new file mode 100644 index 00000000..a0055391 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/util/ConcurrencySanitizingMap.java @@ -0,0 +1,100 @@ +package org.embeddedt.modernfix.util; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * A map wrapper that throws if the map is ever accessed on the wrong thread. + */ +public class ConcurrencySanitizingMap implements Map { + private final Map map; + private final Thread owner; + + public ConcurrencySanitizingMap(Map map, Thread owner) { + this.map = map; + this.owner = owner; + } + + private void checkThread() { + var current = Thread.currentThread(); + if (current != owner) { + throw new IllegalStateException("Map is being accessed on thread " + current + " while owned by " + owner); + } + } + + @Override + public int size() { + checkThread(); + return map.size(); + } + + @Override + public boolean isEmpty() { + checkThread(); + return map.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + checkThread(); + return map.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + checkThread(); + return map.containsValue(value); + } + + @Override + public V get(Object key) { + checkThread(); + return map.get(key); + } + + @Override + public @Nullable V put(K key, V value) { + checkThread(); + return map.put(key, value); + } + + @Override + public V remove(Object key) { + checkThread(); + return map.remove(key); + } + + @Override + public void putAll(@NotNull Map m) { + checkThread(); + map.putAll(m); + } + + @Override + public void clear() { + checkThread(); + map.clear(); + } + + @Override + public @NotNull Set keySet() { + checkThread(); + return map.keySet(); + } + + @Override + public @NotNull Collection values() { + checkThread(); + return map.values(); + } + + @Override + public @NotNull Set> entrySet() { + checkThread(); + return map.entrySet(); + } +}