diff --git a/build.gradle b/build.gradle index 7161b4f4..5efcf8b9 100644 --- a/build.gradle +++ b/build.gradle @@ -105,6 +105,7 @@ allprojects { maven { url 'https://maven.terraformersmc.com/releases' } + maven { url = "https://jitpack.io" } } } @@ -217,6 +218,7 @@ configure(subprojects.findAll {it.name == "forge" || it.name == "fabric"}) { client { vmArgs "-Xmx1G" vmArgs "-Xms1G" + property("mixin.debug.export", "true") } } } diff --git a/common/build.gradle b/common/build.gradle index 5e2ef966..b74ad52e 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -10,6 +10,7 @@ dependencies { // We depend on fabric loader here to use the fabric @Environment annotations and get the mixin dependencies // Do NOT use other classes from fabric loader modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}" + implementation(annotationProcessor("com.github.llamalad7.mixinextras:mixinextras-common:${rootProject.mixinextras_version}")) modCompileOnly("dev.latvian.mods:kubejs:${kubejs_version}") { transitive = false diff --git a/common/src/main/java/org/embeddedt/modernfix/chunk/SafeBlockGetter.java b/common/src/main/java/org/embeddedt/modernfix/chunk/SafeBlockGetter.java new file mode 100644 index 00000000..3d4efe52 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/chunk/SafeBlockGetter.java @@ -0,0 +1,78 @@ +package org.embeddedt.modernfix.chunk; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import org.jetbrains.annotations.Nullable; + +public class SafeBlockGetter implements BlockGetter { + private final ServerLevel wrapped; + private final Thread mainThread; + + public SafeBlockGetter(ServerLevel wrapped) { + this.wrapped = wrapped; + this.mainThread = Thread.currentThread(); + } + + public boolean shouldUse() { + return Thread.currentThread() != this.mainThread; + } + + @Nullable + private BlockGetter getChunkSafe(BlockPos pos) { + // can safely call getChunkForLighting off-thread + BlockGetter access = this.wrapped.getChunkSource().getChunkForLighting(pos.getX() >> 4, pos.getZ() >> 4); + if(!(access instanceof ChunkAccess)) + return null; + ChunkAccess chunk = (ChunkAccess)access; + if(!chunk.getStatus().isOrAfter(ChunkStatus.FULL)) + return null; + return chunk; + } + + @Override + public int getMaxBuildHeight() { + return this.wrapped.getMaxBuildHeight(); + } + + @Override + public int getMaxLightLevel() { + return this.wrapped.getMaxLightLevel(); + } + + @Override + public int getMinBuildHeight() { + return this.wrapped.getMinBuildHeight(); + } + + @Override + public int getHeight() { + return this.wrapped.getHeight(); + } + + @Nullable + @Override + public BlockEntity getBlockEntity(BlockPos pos) { + BlockGetter g = getChunkSafe(pos); + return g == null ? null : g.getBlockEntity(pos); + } + + @Override + public BlockState getBlockState(BlockPos pos) { + BlockGetter g = getChunkSafe(pos); + return g == null ? Blocks.AIR.defaultBlockState() : g.getBlockState(pos); + } + + @Override + public FluidState getFluidState(BlockPos pos) { + BlockGetter g = getChunkSafe(pos); + return g == null ? Fluids.EMPTY.defaultFluidState() : g.getFluidState(pos); + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/BlockStateBaseMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/BlockStateBaseMixin.java new file mode 100644 index 00000000..464130c8 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/BlockStateBaseMixin.java @@ -0,0 +1,22 @@ +package org.embeddedt.modernfix.common.mixin.bugfix.chunk_deadlock; + +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.state.BlockBehaviour; +import org.embeddedt.modernfix.chunk.SafeBlockGetter; +import org.embeddedt.modernfix.duck.ISafeBlockGetter; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +@Mixin(value = BlockBehaviour.BlockStateBase.class, priority = 100) +public class BlockStateBaseMixin { + @ModifyVariable(method = "getOffset", at = @At("HEAD"), argsOnly = true, index = 1) + private BlockGetter useSafeGetter(BlockGetter g) { + if(g instanceof ISafeBlockGetter) { + SafeBlockGetter replacement = ((ISafeBlockGetter) g).mfix$getSafeBlockGetter(); + if(replacement.shouldUse()) + return replacement; + } + return g; + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ServerLevelMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ServerLevelMixin.java new file mode 100644 index 00000000..e7c3b137 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ServerLevelMixin.java @@ -0,0 +1,18 @@ +package org.embeddedt.modernfix.common.mixin.bugfix.chunk_deadlock; + +import net.minecraft.server.level.ServerLevel; +import org.embeddedt.modernfix.chunk.SafeBlockGetter; +import org.embeddedt.modernfix.duck.ISafeBlockGetter; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(ServerLevel.class) +public class ServerLevelMixin implements ISafeBlockGetter { + @Unique + private final SafeBlockGetter mfix$safeBlockGetter = new SafeBlockGetter((ServerLevel)(Object)this); + + @Override + public SafeBlockGetter mfix$getSafeBlockGetter() { + return mfix$safeBlockGetter; + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/world_screen_skipped/WorldSelectionListMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/world_screen_skipped/WorldSelectionListMixin.java new file mode 100644 index 00000000..27f67f7a --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/world_screen_skipped/WorldSelectionListMixin.java @@ -0,0 +1,22 @@ +package org.embeddedt.modernfix.common.mixin.bugfix.world_screen_skipped; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen; +import net.minecraft.client.gui.screens.worldselection.WorldSelectionList; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +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; + +@Mixin(WorldSelectionList.WorldListEntry.class) +public class WorldSelectionListMixin { + @Shadow @Final private Minecraft minecraft; + + @Inject(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/worldselection/WorldSelectionList$WorldListEntry;doDeleteWorld()V", ordinal = 0, shift = At.Shift.AFTER), cancellable = true) + private void preventClosingCreateScreenAfterDelete(CallbackInfo ci) { + if(minecraft.screen instanceof CreateWorldScreen) + ci.cancel(); + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/reduce_blockstate_cache_rebuilds/BlockStateBaseMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/reduce_blockstate_cache_rebuilds/BlockStateBaseMixin.java index 3d3b3acc..2c209d88 100644 --- a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/reduce_blockstate_cache_rebuilds/BlockStateBaseMixin.java +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/reduce_blockstate_cache_rebuilds/BlockStateBaseMixin.java @@ -11,13 +11,10 @@ import net.minecraft.world.level.material.FluidState; import net.minecraft.world.level.material.Fluids; import org.embeddedt.modernfix.duck.IBlockState; import org.objectweb.asm.Opcodes; -import org.spongepowered.asm.mixin.Dynamic; import org.spongepowered.asm.mixin.Mixin; 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.Redirect; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(BlockBehaviour.BlockStateBase.class) @@ -48,7 +45,7 @@ public abstract class BlockStateBaseMixin extends StateHolder return cacheInvalid; } - private BlockBehaviour.BlockStateBase.Cache generateCache(BlockBehaviour.BlockStateBase base) { + private void mfix$generateCache() { if(cacheInvalid) { // Ensure that only one block's cache is built at a time synchronized (BlockBehaviour.BlockStateBase.class) { @@ -67,7 +64,6 @@ public abstract class BlockStateBaseMixin extends StateHolder } } - return this.cache; } @Redirect(method = "*", at = @At( @@ -77,7 +73,8 @@ public abstract class BlockStateBaseMixin extends StateHolder ordinal = 0 )) private BlockBehaviour.BlockStateBase.Cache dynamicCacheGen(BlockBehaviour.BlockStateBase base) { - return generateCache(base); + mfix$generateCache(); + return this.cache; } @Redirect(method = "*", at = @At( @@ -106,22 +103,4 @@ public abstract class BlockStateBaseMixin extends StateHolder return this.owner.isRandomlyTicking(this.asState()); return this.isRandomlyTicking; } - - @Dynamic - @Inject(method = "getPathNodeType", at = @At("HEAD"), require = 0, remap = false) - private void generateCacheLithium(CallbackInfoReturnable cir) { - generateCache((BlockBehaviour.BlockStateBase)(Object)this); - } - - @Dynamic - @Inject(method = "getNeighborPathNodeType", at = @At("HEAD"), require = 0, remap = false) - private void generateCacheLithium2(CallbackInfoReturnable cir) { - generateCache((BlockBehaviour.BlockStateBase)(Object)this); - } - - @Dynamic - @Inject(method = "getAllFlags", at = @At("HEAD"), require = 0, remap = false) - private void generateCacheLithium3(CallbackInfoReturnable cir) { - generateCache((BlockBehaviour.BlockStateBase)(Object)this); - } } diff --git a/common/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java b/common/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java index 6c902036..c61492a3 100644 --- a/common/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java +++ b/common/src/main/java/org/embeddedt/modernfix/core/ModernFixMixinPlugin.java @@ -1,14 +1,18 @@ package org.embeddedt.modernfix.core; +import com.google.common.collect.ImmutableSet; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.embeddedt.modernfix.core.config.ModernFixEarlyConfig; import org.embeddedt.modernfix.core.config.Option; import org.embeddedt.modernfix.platform.ModernFixPlatformHooks; import org.embeddedt.modernfix.world.ThreadDumper; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; import org.objectweb.asm.tree.*; import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; import org.spongepowered.asm.mixin.extensibility.IMixinInfo; +import org.spongepowered.asm.mixin.transformer.meta.MixinMerged; import java.io.File; import java.util.*; @@ -146,6 +150,111 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin { @Override public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + if(mixinClassName.equals("org.embeddedt.modernfix.common.mixin.perf.reduce_blockstate_cache_rebuilds.BlockStateBaseMixin")) { + try { + applyBlockStateCacheScan(targetClass); + } catch(RuntimeException e) { + ModernFixMixinPlugin.instance.logger.error("Applying blockstate cache ASM patch failed", e); + } + } ModernFixPlatformHooks.INSTANCE.applyASMTransformers(mixinClassName, targetClass); } + + private void applyBlockStateCacheScan(ClassNode targetClass) { + Set initCacheMethodNames = ImmutableSet.of("m_60611_", "func_215692_c", "method_26200", "initCache"); + Set whitelistedInjections = ImmutableSet.of( + "getFluidState", "method_26227", "m_60819_", "func_204520_s" + ); + Map injectorMethodNames = new HashMap<>(); + Map injectorMixinSource = new HashMap<>(); + String descriptor = Type.getDescriptor(MixinMerged.class); + for(MethodNode m : targetClass.methods) { + if((m.access & Opcodes.ACC_STATIC) != 0) + continue; + Set seenNodes = new HashSet<>(); + if(m.invisibleAnnotations != null) { + for(AnnotationNode ann : m.invisibleAnnotations) { + if(ann.desc.equals(descriptor)) { + seenNodes.add(ann); + } + } + } + if(m.visibleAnnotations != null) { + for(AnnotationNode ann : m.visibleAnnotations) { + if(ann.desc.equals(descriptor)) { + seenNodes.add(ann); + } + } + } + if(seenNodes.size() > 0) { + injectorMethodNames.put(m.name, m); + for(AnnotationNode node : seenNodes) { + for(int i = 0; i < node.values.size(); i += 2) { + if(Objects.equals(node.values.get(i), "mixin")) { + injectorMixinSource.put(m.name, (String)node.values.get(i + 1)); + break; + } + } + } + } + } + Set cacheCalledInjectors = new HashSet<>(); + // Search for initCache in the class + for(MethodNode m : targetClass.methods) { + if((m.access & Opcodes.ACC_STATIC) != 0) + continue; + if(initCacheMethodNames.contains(m.name)) { + // This is it. Check for any injectors it calls + for(AbstractInsnNode n : m.instructions) { + if(n instanceof MethodInsnNode) { + MethodInsnNode invoke = (MethodInsnNode)n; + if(((MethodInsnNode)n).owner.equals(targetClass.name) && injectorMethodNames.containsKey(((MethodInsnNode)n).name)) { + cacheCalledInjectors.add(invoke.name); + } + } + } + break; + } + } + Set accessedFieldNames = new HashSet<>(); + // We now know all methods that have been injected into initCache. See what fields they write to + injectorMethodNames.forEach((name, method) -> { + if(cacheCalledInjectors.contains(name)) { + for(AbstractInsnNode n : method.instructions) { + if(n instanceof FieldInsnNode) { + FieldInsnNode fieldAcc = (FieldInsnNode)n; + if(fieldAcc.getOpcode() == Opcodes.PUTFIELD && fieldAcc.owner.equals(targetClass.name)) { + accessedFieldNames.add(fieldAcc.name); + } + } + } + } + }); + // Lastly, scan all injected methods and see if they retrieve from the field. If so, inject a generateCache + // call at the start. + injectorMethodNames.forEach((name, method) -> { + // skip whitelisted injectors, and injectors called by initCache itself (to prevent recursion) + if(whitelistedInjections.contains(name) || cacheCalledInjectors.contains(name)) + return; + boolean needInjection = false; + for(AbstractInsnNode n : method.instructions) { + if(n instanceof FieldInsnNode) { + FieldInsnNode fieldAcc = (FieldInsnNode)n; + if(fieldAcc.getOpcode() == Opcodes.GETFIELD && accessedFieldNames.contains(fieldAcc.name)) { + needInjection = true; + break; + } + } + } + if(needInjection) { + ModernFixMixinPlugin.instance.logger.info("Injecting BlockStateBase cache population hook into {} from {}", + name, injectorMixinSource.getOrDefault(name, "[unknown mixin]")); + // inject this.mfix$generateCache() at method head + InsnList injection = new InsnList(); + injection.add(new VarInsnNode(Opcodes.ALOAD, 0)); + injection.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, targetClass.name, "mfix$generateCache", "()V")); + method.instructions.insert(injection); + } + }); + } } \ No newline at end of file diff --git a/common/src/main/java/org/embeddedt/modernfix/duck/ISafeBlockGetter.java b/common/src/main/java/org/embeddedt/modernfix/duck/ISafeBlockGetter.java new file mode 100644 index 00000000..3fb462ef --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/duck/ISafeBlockGetter.java @@ -0,0 +1,7 @@ +package org.embeddedt.modernfix.duck; + +import org.embeddedt.modernfix.chunk.SafeBlockGetter; + +public interface ISafeBlockGetter { + SafeBlockGetter mfix$getSafeBlockGetter(); +} diff --git a/common/src/main/resources/assets/modernfix/lang/zh_cn.json b/common/src/main/resources/assets/modernfix/lang/zh_cn.json index 846a902e..f0686d62 100644 --- a/common/src/main/resources/assets/modernfix/lang/zh_cn.json +++ b/common/src/main/resources/assets/modernfix/lang/zh_cn.json @@ -4,10 +4,11 @@ "modernfix.jei_load": "正在加载JEI,这可能会花费一段时间。", "modernfix.no_lazydfu": "未安装DFU载入优化。如果Minecraft需要从旧版本更新游戏数据,可能会出现极大的延迟。", "modernfix.no_ferritecore": "未安装铁氧体磁芯。内存占用将会非常高。", + "modernfix.connectedness_dynresoruces": "Connectedness模组(用于提供连接纹理)和现代化修复的动态资源(dynamic resources)功能不兼容。请删除Connectedness模组,或在现代化修复配置中禁用动态资源功能。", "modernfix.perf_mod_warning": "推荐安装这些模组,但你也可以在现代化修复的配置中禁用此警告。", "modernfix.config": "现代化修复Mixin配置", "modernfix.config.done_restart": "完成(生效需重启)", - "modernfix.message.reload_config": "在游戏外编辑完配置文件后,使用§b/mfrc§r命令使其生效。", + "modernfix.message.reload_config": "检测到模组配置文件的更改。为了避免加载尚未保存完毕的文件,重载过程必须通过使用§b/mfrc§r命令来触发。", "modernfix.option.on": "开启", "modernfix.option.off": "关闭", "modernfix.option.disabled": "已禁用", diff --git a/fabric/build.gradle b/fabric/build.gradle index 5c79bfc2..ed6e7c0f 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -31,6 +31,7 @@ configurations { dependencies { modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}" testImplementation "net.fabricmc:fabric-loader-junit:${rootProject.fabric_loader_version}" + include(implementation(annotationProcessor("com.github.llamalad7.mixinextras:mixinextras-fabric:${rootProject.mixinextras_version}"))) modCompileOnly(fabricApi.module("fabric-api-base", rootProject.fabric_api_version)) { exclude group: 'net.fabricmc', module: 'fabric-loader' } modCompileOnly(fabricApi.module("fabric-screen-api-v1", rootProject.fabric_api_version)) { exclude group: 'net.fabricmc', module: 'fabric-loader' } diff --git a/forge/build.gradle b/forge/build.gradle index b45b331b..67ec265e 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -45,6 +45,8 @@ repositories { dependencies { forge "net.minecraftforge:forge:${rootProject.forge_version}" + implementation(annotationProcessor("com.github.llamalad7.mixinextras:mixinextras-common:${rootProject.mixinextras_version}")) + implementation(include("com.github.llamalad7.mixinextras:mixinextras-forge:${rootProject.mixinextras_version}")) // Remove the next line if you don't want to depend on the API // modApi "me.shedaniel:architectury-forge:${rootProject.architectury_version}" diff --git a/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/bugfix/chunk_deadlock/ChunkMapLoadMixin.java b/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/bugfix/chunk_deadlock/ChunkMapLoadMixin.java new file mode 100644 index 00000000..cfed9788 --- /dev/null +++ b/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/bugfix/chunk_deadlock/ChunkMapLoadMixin.java @@ -0,0 +1,56 @@ +package org.embeddedt.modernfix.forge.mixin.bugfix.chunk_deadlock; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraftforge.fml.util.ObfuscationReflectionHelper; +import org.embeddedt.modernfix.ModernFix; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +import java.lang.reflect.Field; + +@Mixin(ChunkMap.class) +public abstract class ChunkMapLoadMixin { + @Shadow @Nullable protected abstract ChunkHolder getVisibleChunkIfPresent(long l); + + private static final Field currentlyLoadingField = ObfuscationReflectionHelper.findField(ChunkHolder.class, "currentlyLoading"); + + private static void setCurrentlyLoading(ChunkHolder holder, LevelChunk value) { + try { + currentlyLoadingField.set(holder, value); + } catch(ReflectiveOperationException e) { + e.printStackTrace(); + } + } + + /** + * Set currentlyLoading before calling runPostLoad and restore its old value afterwards. We track the old value + * to avoid conflicting with Forge if/when this feature is added. + */ + @WrapOperation(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunk;runPostLoad()V")) + private void setCurrentLoadingThenPostLoad(LevelChunk chunk, Operation operation) { + ChunkHolder holder = this.getVisibleChunkIfPresent(chunk.getPos().toLong()); + if(holder != null) { + LevelChunk prevLoading = null; + try { + prevLoading = (LevelChunk)currentlyLoadingField.get(holder); + } catch(ReflectiveOperationException e) { + e.printStackTrace(); + } + try { + setCurrentlyLoading(holder, chunk); + operation.call(chunk); + } finally { + setCurrentlyLoading(holder, prevLoading); + } + } else { + ModernFix.LOGGER.warn("Unable to find chunk holder for loading chunk"); + operation.call(chunk); + } + } +} diff --git a/gradle.properties b/gradle.properties index a7fabd4d..d5906f8e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,6 +2,7 @@ org.gradle.jvmargs=-Xmx2G junit_version=5.10.0-M1 +mixinextras_version=0.2.0-beta.9 mod_id=modernfix minecraft_version=1.19.4