diff --git a/src/main/java/org/embeddedt/modernfix/ModernFix.java b/src/main/java/org/embeddedt/modernfix/ModernFix.java index fbc0614a..14dddb81 100644 --- a/src/main/java/org/embeddedt/modernfix/ModernFix.java +++ b/src/main/java/org/embeddedt/modernfix/ModernFix.java @@ -2,9 +2,11 @@ package org.embeddedt.modernfix; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.Item; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.server.ServerStartedEvent; @@ -21,6 +23,8 @@ import net.minecraftforge.fml.loading.FMLConfig; import net.minecraftforge.fml.loading.FMLLoader; import net.minecraftforge.fml.util.ObfuscationReflectionHelper; import net.minecraftforge.network.NetworkConstants; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.RegisterEvent; import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -99,6 +103,7 @@ public class ModernFix { MinecraftForge.EVENT_BUS.register(this); FMLJavaModLoadingContext.get().getModEventBus().addListener(this::commonSetup); FMLJavaModLoadingContext.get().getModEventBus().addListener(this::onLoadComplete); + FMLJavaModLoadingContext.get().getModEventBus().addListener(this::registerItems); DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> MinecraftForge.EVENT_BUS.register(new ModernFixClient())); ModLoadingContext.get().registerExtensionPoint(IExtensionPoint.DisplayTest.class, () -> new IExtensionPoint.DisplayTest(() -> NetworkConstants.IGNORESERVERONLY, (a, b) -> true)); ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, ModernFixConfig.COMMON_CONFIG); @@ -108,6 +113,17 @@ public class ModernFix { ModFileScanDataDeduplicator.deduplicate(); } + private void registerItems(RegisterEvent event) { + if(Boolean.getBoolean("modernfix.largeRegistryTest")) { + event.register(ForgeRegistries.Keys.ITEMS, helper -> { + Item.Properties props = new Item.Properties(); + for(int i = 0; i < 1000000; i++) { + helper.register(new ResourceLocation("modernfix", "item_" + i), new Item(props)); + } + }); + } + } + private static boolean dfuModPresent() { if(FMLConfig.isOptimizedDFUDisabled()) return true; diff --git a/src/main/java/org/embeddedt/modernfix/ModernFixClient.java b/src/main/java/org/embeddedt/modernfix/ModernFixClient.java index 06fc2635..c35e7c9a 100644 --- a/src/main/java/org/embeddedt/modernfix/ModernFixClient.java +++ b/src/main/java/org/embeddedt/modernfix/ModernFixClient.java @@ -1,11 +1,14 @@ package org.embeddedt.modernfix; +import com.mojang.blaze3d.platform.InputConstants; import com.mojang.datafixers.util.Pair; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import net.minecraft.client.KeyMapping; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.DebugScreenOverlay; import net.minecraft.client.gui.screens.ConnectScreen; import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraftforge.client.ConfigScreenHandler; import net.minecraftforge.client.event.CustomizeGuiOverlayEvent; import net.minecraft.util.MemoryReserve; import net.minecraftforge.client.event.ScreenEvent; @@ -14,6 +17,7 @@ import net.minecraft.network.syncher.SynchedEntityData; import net.minecraft.world.entity.Entity; import net.minecraftforge.client.gui.overlay.ForgeGui; import net.minecraftforge.client.event.*; +import net.minecraftforge.client.settings.KeyConflictContext; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.TagsUpdatedEvent; import net.minecraftforge.event.TickEvent; @@ -21,13 +25,18 @@ import net.minecraftforge.event.level.LevelEvent; import net.minecraftforge.event.server.ServerStartingEvent; import net.minecraftforge.eventbus.api.EventPriority; import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.IExtensionPoint; import net.minecraftforge.fml.ModContainer; import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.ModLoadingContext; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import net.minecraftforge.fml.util.ObfuscationReflectionHelper; import net.minecraftforge.network.NetworkEvent; import org.embeddedt.modernfix.core.ModernFixMixinPlugin; import org.embeddedt.modernfix.core.config.ModernFixConfig; import org.embeddedt.modernfix.packet.EntityIDSyncPacket; +import org.embeddedt.modernfix.screen.ModernFixConfigScreen; import org.embeddedt.modernfix.world.IntegratedWatchdog; import java.lang.management.ManagementFactory; @@ -53,6 +62,26 @@ public class ModernFixClient { if(mfContainer.isPresent()) brandingString = "ModernFix " + mfContainer.get().getModInfo().getVersion().toString(); } + + FMLJavaModLoadingContext.get().getModEventBus().addListener(this::keyBindRegister); + ModLoadingContext.get().registerExtensionPoint( + ConfigScreenHandler.ConfigScreenFactory.class, + () -> new ConfigScreenHandler.ConfigScreenFactory((mc, screen) -> new ModernFixConfigScreen(screen)) + ); + } + + private KeyMapping configKey; + + private void keyBindRegister(RegisterKeyMappingsEvent event) { + configKey = new KeyMapping("key.modernfix.config", KeyConflictContext.UNIVERSAL, InputConstants.UNKNOWN, "key.modernfix"); + event.register(configKey); + } + + @SubscribeEvent + public void onConfigKey(TickEvent.ClientTickEvent event) { + if(event.phase == TickEvent.Phase.START && configKey.consumeClick()) { + Minecraft.getInstance().setScreen(new ModernFixConfigScreen(Minecraft.getInstance().screen)); + } } public void resetWorldLoadStateMachine() { diff --git a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java index 69cc0687..0014ce0d 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -17,6 +17,8 @@ public class ModernFixEarlyConfig { public static final boolean OPTIFINE_PRESENT; + private File configFile; + static { boolean hasOfClass = false; try { @@ -34,7 +36,8 @@ public class ModernFixEarlyConfig { return FMLLoader.getLoadingModList().getModFileById(modId) != null; } - private ModernFixEarlyConfig() { + private ModernFixEarlyConfig(File file) { + this.configFile = file; // Defines the default rules which can be configured by the user or other mods. // You must manually add a rule for any new mixins not covered by an existing package rule. this.addMixinRule("core", true); // TODO: Don't actually allow the user to disable this @@ -63,6 +66,7 @@ public class ModernFixEarlyConfig { this.addMixinRule("perf.fast_forge_dummies", true); this.addMixinRule("perf.dynamic_structure_manager", true); this.addMixinRule("bugfix.chunk_deadlock", true); + this.addMixinRule("bugfix.paper_chunk_patches", true); this.addMixinRule("perf.thread_priorities", true); this.addMixinRule("perf.scan_cache", true); this.addMixinRule("perf.kubejs", modPresent("kubejs")); @@ -185,7 +189,7 @@ public class ModernFixEarlyConfig { * created. The file on disk will then be updated to include any new options. */ public static ModernFixEarlyConfig load(File file) { - ModernFixEarlyConfig config = new ModernFixEarlyConfig(); + ModernFixEarlyConfig config = new ModernFixEarlyConfig(file); Properties props = new Properties(); if(file.exists()) { try (FileInputStream fin = new FileInputStream(file)){ @@ -197,7 +201,7 @@ public class ModernFixEarlyConfig { } try { - config.writeConfig(file, props); + config.save(); } catch (IOException e) { LOGGER.warn("Could not write configuration file", e); } @@ -205,8 +209,8 @@ public class ModernFixEarlyConfig { return config; } - private void writeConfig(File file, Properties props) throws IOException { - File dir = file.getParentFile(); + public void save() throws IOException { + File dir = configFile.getParentFile(); if (!dir.exists()) { if (!dir.mkdirs()) { @@ -216,23 +220,24 @@ public class ModernFixEarlyConfig { throw new IOException("The parent file is not a directory"); } - try (Writer writer = new FileWriter(file)) { + try (Writer writer = new FileWriter(configFile)) { writer.write("# This is the configuration file for ModernFix.\n"); writer.write("#\n"); writer.write("# The following options can be enabled or disabled if there is a compatibility issue.\n"); writer.write("# Add a line mixin.example_name=true/false without the # sign to enable/disable a rule.\n"); - List lines = this.options.keySet().stream() + List keys = this.options.keySet().stream() .filter(key -> !key.equals("mixin.core")) .sorted() - .map(key -> "# " + key + "\n") .collect(Collectors.toList()); - for(String line : lines) { - writer.write(line); + for(String line : keys) { + if(!line.equals("mixin.core")) + writer.write("# " + line + "\n"); } - for (Map.Entry entry : props.entrySet()) { - String key = (String) entry.getKey(); - String value = (String) entry.getValue(); - writer.write(key + "=" + value + "\n"); + + for (String key : keys) { + Option option = this.options.get(key); + if(option.isUserDefined()) + writer.write(key + "=" + option.isEnabled() + "\n"); } } } @@ -251,4 +256,8 @@ public class ModernFixEarlyConfig { .filter(Option::isOverridden) .count(); } + + public Map getOptionMap() { + return Collections.unmodifiableMap(this.options); + } } diff --git a/src/main/java/org/embeddedt/modernfix/core/config/Option.java b/src/main/java/org/embeddedt/modernfix/core/config/Option.java index e268ff29..f370e8bc 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/Option.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/Option.java @@ -19,11 +19,15 @@ public class Option { } public void setEnabled(boolean enabled, boolean userDefined) { + if(this.enabled == enabled) + return; this.enabled = enabled; this.userDefined = userDefined; } public void addModOverride(boolean enabled, String modId) { + if(this.enabled == enabled) + return; this.enabled = enabled; if (this.modDefined == null) { @@ -57,6 +61,10 @@ public class Option { this.modDefined = null; } + public void clearUserDefined() { + this.userDefined = false; + } + public Collection getDefiningMods() { return this.modDefined != null ? Collections.unmodifiableCollection(this.modDefined) : Collections.emptyList(); } diff --git a/src/main/java/org/embeddedt/modernfix/duck/IPaperChunkHolder.java b/src/main/java/org/embeddedt/modernfix/duck/IPaperChunkHolder.java new file mode 100644 index 00000000..0d83c721 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/duck/IPaperChunkHolder.java @@ -0,0 +1,5 @@ +package org.embeddedt.modernfix.duck; + +public interface IPaperChunkHolder { + boolean mfix$canAdvanceStatus(); +} diff --git a/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicBakedModelProvider.java b/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicBakedModelProvider.java index d9bc92b4..4f809d86 100644 --- a/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicBakedModelProvider.java +++ b/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicBakedModelProvider.java @@ -34,6 +34,7 @@ public class DynamicBakedModelProvider implements Map vanillaKey(Object o) { diff --git a/src/main/java/org/embeddedt/modernfix/dynamicresources/ModelLocationCache.java b/src/main/java/org/embeddedt/modernfix/dynamicresources/ModelLocationCache.java index 25886e20..8cb9fd39 100644 --- a/src/main/java/org/embeddedt/modernfix/dynamicresources/ModelLocationCache.java +++ b/src/main/java/org/embeddedt/modernfix/dynamicresources/ModelLocationCache.java @@ -10,8 +10,10 @@ import net.minecraft.Util; import net.minecraft.client.renderer.block.BlockModelShaper; import net.minecraft.client.resources.model.ModelResourceLocation; import net.minecraft.core.Registry; +import net.minecraft.world.item.Item; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.registries.ForgeRegistries; import java.util.ArrayList; import java.util.Collections; @@ -20,7 +22,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; public class ModelLocationCache { - private static final LoadingCache locationCache = CacheBuilder.newBuilder() + private static final LoadingCache blockLocationCache = CacheBuilder.newBuilder() .maximumSize(10000) .build(new CacheLoader() { @Override @@ -29,9 +31,26 @@ public class ModelLocationCache { } }); + private static final LoadingCache itemLocationCache = CacheBuilder.newBuilder() + .maximumSize(10000) + .build(new CacheLoader() { + @Override + public ModelResourceLocation load(Item key) throws Exception { + return new ModelResourceLocation(ForgeRegistries.ITEMS.getKey(key), "inventory"); + } + }); + public static ModelResourceLocation get(BlockState state) { try { - return locationCache.get(state); + return blockLocationCache.get(state); + } catch(ExecutionException e) { + throw new RuntimeException(e.getCause()); + } + } + + public static ModelResourceLocation get(Item item) { + try { + return itemLocationCache.get(item); } catch(ExecutionException e) { throw new RuntimeException(e.getCause()); } diff --git a/src/main/java/org/embeddedt/modernfix/mixin/bugfix/paper_chunk_patches/ChunkHolderMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/bugfix/paper_chunk_patches/ChunkHolderMixin.java new file mode 100644 index 00000000..0a51f6d7 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/bugfix/paper_chunk_patches/ChunkHolderMixin.java @@ -0,0 +1,61 @@ +package org.embeddedt.modernfix.mixin.bugfix.paper_chunk_patches; + +import com.mojang.datafixers.util.Either; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import org.embeddedt.modernfix.duck.IPaperChunkHolder; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@Mixin(ChunkHolder.class) +public abstract class ChunkHolderMixin implements IPaperChunkHolder { + + @Shadow public abstract CompletableFuture> getFutureIfPresentUnchecked(ChunkStatus arg); + + @Shadow @Final private static List CHUNK_STATUSES; + + public ChunkStatus mfix$getChunkHolderStatus() { + for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) { + CompletableFuture> future = this.getFutureIfPresentUnchecked(curr); + Either either = future.getNow(null); + if (either == null || !either.left().isPresent()) { + continue; + } + return curr; + } + + return null; + } + + public ChunkAccess mfix$getAvailableChunkNow() { + // TODO can we just getStatusFuture(EMPTY)? + for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) { + CompletableFuture> future = this.getFutureIfPresentUnchecked(curr); + Either either = future.getNow(null); + if (either == null || !either.left().isPresent()) { + continue; + } + return either.left().get(); + } + return null; + } + + private static ChunkStatus mfix$getNextStatus(ChunkStatus status) { + if (status == ChunkStatus.FULL) { + return status; + } + return CHUNK_STATUSES.get(status.getIndex() + 1); + } + + @Override + public boolean mfix$canAdvanceStatus() { + ChunkStatus status = mfix$getChunkHolderStatus(); + ChunkAccess chunk = mfix$getAvailableChunkNow(); + return chunk != null && (status == null || chunk.getStatus().isOrAfter(mfix$getNextStatus(status))); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/mixin/bugfix/paper_chunk_patches/ChunkMapMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/bugfix/paper_chunk_patches/ChunkMapMixin.java new file mode 100644 index 00000000..e147a76e --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/bugfix/paper_chunk_patches/ChunkMapMixin.java @@ -0,0 +1,115 @@ +package org.embeddedt.modernfix.mixin.bugfix.paper_chunk_patches; + +import com.mojang.datafixers.util.Either; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.*; +import net.minecraft.server.level.progress.ChunkProgressListener; +import net.minecraft.util.thread.BlockableEventLoop; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import net.minecraftforge.server.ServerLifecycleHooks; +import org.embeddedt.modernfix.duck.IPaperChunkHolder; +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.ModifyArg; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Function; + + +@Mixin(ChunkMap.class) +public abstract class ChunkMapMixin { + @Shadow @Final private BlockableEventLoop mainThreadExecutor; + + @Shadow @Final private ChunkMap.DistanceManager distanceManager; + + @Shadow protected abstract CompletableFuture> protoChunkToFullChunk(ChunkHolder arg); + + @Shadow @Final private ServerLevel level; + @Shadow @Final private ThreadedLevelLightEngine lightEngine; + @Shadow @Final private ChunkProgressListener progressListener; + + @Shadow protected abstract CompletableFuture> scheduleChunkGeneration(ChunkHolder chunkHolder, ChunkStatus chunkStatus); + + @Shadow @Final private StructureTemplateManager structureTemplateManager; + private Executor mainInvokingExecutor; + + @Inject(method = "", at = @At("RETURN"), cancellable = true) + private void setup(CallbackInfo ci) { + this.mainInvokingExecutor = (runnable) -> { + if(ServerLifecycleHooks.getCurrentServer().isSameThread()) + runnable.run(); + else + this.mainThreadExecutor.execute(runnable); + }; + } + + + /* https://github.com/PaperMC/Paper/blob/ver/1.17.1/patches/server/0752-Fix-chunks-refusing-to-unload-at-low-TPS.patch */ + @ModifyArg(method = "prepareAccessibleChunk", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenApplyAsync(Ljava/util/function/Function;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"), index = 1) + private Executor useMainThreadExecutor(Executor executor) { + return this.mainThreadExecutor; + } + + /* https://github.com/PaperMC/Paper/blob/master/patches/removed/1.19.2-legacy-chunksystem/0482-Improve-Chunk-Status-Transition-Speed.patch */ + @ModifyArg(method = "prepareEntityTickingChunk", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenApplyAsync(Ljava/util/function/Function;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"), index = 1) + private Executor useMainInvokingExecutor(Executor executor) { + return this.mainInvokingExecutor; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Redirect(method = "scheduleChunkGeneration", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenComposeAsync(Ljava/util/function/Function;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;")) + private CompletableFuture skipWorkerIfPossible(CompletableFuture inputFuture, Function function, Executor executor, ChunkHolder holder) { + Executor targetExecutor = (runnable) -> { + if(((IPaperChunkHolder)holder).mfix$canAdvanceStatus()) { + this.mainInvokingExecutor.execute(runnable); + return; + } + executor.execute(runnable); + }; + return inputFuture.thenComposeAsync(function, targetExecutor); + } + + /** + * @author embeddedt + * @reason revert 1.17 chunk system changes, significantly reduces time and RAM needed to load chunks + */ + @Inject(method = "schedule", at = @At("HEAD"), cancellable = true) + private void useLegacySchedulingLogic(ChunkHolder holder, ChunkStatus requiredStatus, CallbackInfoReturnable>> cir) { + if(requiredStatus != ChunkStatus.EMPTY) { + ChunkPos chunkpos = holder.getPos(); + CompletableFuture> future = holder.getOrScheduleFuture(requiredStatus.getParent(), (ChunkMap)(Object)this); + cir.setReturnValue(future.thenComposeAsync((either) -> { + Optional optional = either.left(); + if(!optional.isPresent()) + return CompletableFuture.completedFuture(either); + + if (requiredStatus == ChunkStatus.LIGHT) { + this.distanceManager.addTicket(TicketType.LIGHT, chunkpos, 33 + ChunkStatus.getDistance(ChunkStatus.LIGHT), chunkpos); + } + + // from original method + if (optional.get().getStatus().isOrAfter(requiredStatus)) { + CompletableFuture> completablefuture = requiredStatus.load(this.level, this.structureTemplateManager, this.lightEngine, (arg2) -> { + return this.protoChunkToFullChunk(holder); + }, (ChunkAccess)optional.get()); + this.progressListener.onStatusChange(chunkpos, requiredStatus); + return completablefuture; + } else { + return this.scheduleChunkGeneration(holder, requiredStatus); + } + }, this.mainThreadExecutor).thenComposeAsync(CompletableFuture::completedFuture, this.mainThreadExecutor)); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/mixin/bugfix/paper_chunk_patches/SortedArraySetMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/bugfix/paper_chunk_patches/SortedArraySetMixin.java new file mode 100644 index 00000000..ea9e9167 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/bugfix/paper_chunk_patches/SortedArraySetMixin.java @@ -0,0 +1,51 @@ +package org.embeddedt.modernfix.mixin.bugfix.paper_chunk_patches; + +import net.minecraft.util.SortedArraySet; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.function.Predicate; + +@Mixin(SortedArraySet.class) +public abstract class SortedArraySetMixin extends AbstractSet { + @Shadow private int size; + + @Shadow private T[] contents; + + // Paper start - optimise removeIf + @Override + public boolean removeIf(Predicate filter) { + // prev. impl used an iterator, which could be n^2 and creates garbage + int i = 0, len = this.size; + T[] backingArray = this.contents; + + for (;;) { + if (i >= len) { + return false; + } + if (!filter.test(backingArray[i])) { + ++i; + continue; + } + break; + } + + // we only want to write back to backingArray if we really need to + + int lastIndex = i; // this is where new elements are shifted to + + for (; i < len; ++i) { + T curr = backingArray[i]; + if (!filter.test(curr)) { // if test throws we're screwed + backingArray[lastIndex++] = curr; + } + } + + // cleanup end + Arrays.fill(backingArray, lastIndex, len, null); + this.size = lastIndex; + return true; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/mixin/devenv/GameDataMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/devenv/GameDataMixin.java new file mode 100644 index 00000000..34f531a3 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/devenv/GameDataMixin.java @@ -0,0 +1,16 @@ +package org.embeddedt.modernfix.mixin.devenv; + +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.registries.ForgeRegistry; +import net.minecraftforge.registries.GameData; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(GameData.class) +public class GameDataMixin { + + @Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/registries/ForgeRegistry;dump(Lnet/minecraft/resources/ResourceLocation;)V")) + private static void noDump(ForgeRegistry reg, ResourceLocation id) { + } +} diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ItemModelShaperMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ItemModelShaperMixin.java index 987b352b..58daef94 100644 --- a/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ItemModelShaperMixin.java +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ItemModelShaperMixin.java @@ -5,9 +5,11 @@ import net.minecraft.client.resources.model.BakedModel; import net.minecraft.client.resources.model.ModelManager; import net.minecraft.client.resources.model.ModelResourceLocation; import net.minecraft.core.Holder; +import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.Item; import net.minecraftforge.client.model.ForgeItemModelShaper; import net.minecraftforge.registries.ForgeRegistries; +import org.embeddedt.modernfix.dynamicresources.ModelLocationCache; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Overwrite; @@ -23,6 +25,8 @@ public abstract class ItemModelShaperMixin extends ItemModelShaper { super(arg); } + private static final ModelResourceLocation SENTINEL = new ModelResourceLocation(new ResourceLocation("modernfix", "sentinel"), "sentinel"); + /** * @reason Get the stored location for that item and meta, and get the model * from that location from the model manager. @@ -30,7 +34,11 @@ public abstract class ItemModelShaperMixin extends ItemModelShaper { @Overwrite @Override public BakedModel getItemModel(Item item) { - ModelResourceLocation map = locations.get(ForgeRegistries.ITEMS.getDelegateOrThrow(item)); + ModelResourceLocation map = locations.getOrDefault(ForgeRegistries.ITEMS.getDelegateOrThrow(item), SENTINEL); + if(map == SENTINEL) { + /* generate the appropriate location from our cache */ + map = ModelLocationCache.get(item); + } return map == null ? null : getModelManager().getModel(map); } diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ItemRendererMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ItemRendererMixin.java new file mode 100644 index 00000000..3e47df2d --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ItemRendererMixin.java @@ -0,0 +1,20 @@ +package org.embeddedt.modernfix.mixin.perf.dynamic_resources; + +import net.minecraft.client.renderer.ItemModelShaper; +import net.minecraft.client.renderer.entity.ItemRenderer; +import net.minecraft.client.resources.model.ModelResourceLocation; +import net.minecraft.world.item.Item; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ItemRenderer.class) +public class ItemRendererMixin { + /** + * Don't waste space putting all these locations into the cache, compute them on demand later. + */ + @Redirect(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/ItemModelShaper;register(Lnet/minecraft/world/item/Item;Lnet/minecraft/client/resources/model/ModelResourceLocation;)V")) + private void skipDefaultRegistration(ItemModelShaper shaper, Item item, ModelResourceLocation mrl) { + + } +} diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ctm/TextureMetadataHandlerMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ctm/TextureMetadataHandlerMixin.java index bd6bf7af..e8749470 100644 --- a/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ctm/TextureMetadataHandlerMixin.java +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ctm/TextureMetadataHandlerMixin.java @@ -12,6 +12,7 @@ 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.CallbackInfo; import team.chisel.ctm.CTM; import team.chisel.ctm.client.model.AbstractCTMBakedModel; @@ -33,6 +34,11 @@ public abstract class TextureMetadataHandlerMixin { MinecraftForge.EVENT_BUS.addListener(this::onDynamicModelBake); } + @Redirect(method = "onModelBake", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/BakedModel;isCustomRenderer()Z")) + private boolean checkModelValid(BakedModel model) { + return model == null || model.isCustomRenderer(); + } + public void onDynamicModelBake(DynamicModelBakeEvent event) { UnbakedModel rootModel = event.getUnbakedModel(); BakedModel baked = event.getModel(); diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/rs/ClientSetupMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/rs/ClientSetupMixin.java new file mode 100644 index 00000000..5196ff34 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/rs/ClientSetupMixin.java @@ -0,0 +1,30 @@ +package org.embeddedt.modernfix.mixin.perf.dynamic_resources.rs; + +import com.refinedmods.refinedstorage.render.BakedModelOverrideRegistry; +import com.refinedmods.refinedstorage.setup.ClientSetup; +import net.minecraft.client.resources.model.ModelResourceLocation; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.common.MinecraftForge; +import org.embeddedt.modernfix.dynamicresources.DynamicModelBakeEvent; +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(ClientSetup.class) +public class ClientSetupMixin { + @Shadow @Final private static BakedModelOverrideRegistry BAKED_MODEL_OVERRIDE_REGISTRY; + + @Inject(method = "onClientSetup", at = @At("RETURN"), remap = false) + private static void addDynamicListener(CallbackInfo ci) { + MinecraftForge.EVENT_BUS.addListener(ClientSetupMixin::onDynamicModelBake); + } + + private static void onDynamicModelBake(DynamicModelBakeEvent event) { + BakedModelOverrideRegistry.BakedModelOverrideFactory factory = BAKED_MODEL_OVERRIDE_REGISTRY.get(event.getLocation() instanceof ModelResourceLocation ? new ResourceLocation(event.getLocation().getNamespace(), event.getLocation().getPath()) : event.getLocation()); + if(factory != null) + event.setModel(factory.create(event.getModel(), event.getModelBakery().getBakedTopLevelModels())); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ForgeRegistryMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ForgeRegistryMixin.java index 163e27f5..90df21e0 100644 --- a/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ForgeRegistryMixin.java +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ForgeRegistryMixin.java @@ -1,15 +1,29 @@ package org.embeddedt.modernfix.mixin.perf.fast_registry_validation; import net.minecraftforge.fml.util.ObfuscationReflectionHelper; +import com.google.common.collect.BiMap; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; import net.minecraftforge.registries.ForgeRegistry; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.Marker; +import org.embeddedt.modernfix.registry.FastForgeRegistry; +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.lang.reflect.Method; +import java.util.*; @Mixin(value = ForgeRegistry.class, remap = false) -public class ForgeRegistryMixin { +public class ForgeRegistryMixin { private static Method bitSetTrimMethod = null; private static boolean bitSetTrimMethodRetrieved = false; @@ -25,4 +39,71 @@ public class ForgeRegistryMixin { } return bitSetTrimMethod; } + + private int expectedNextBit = -1; + + /** + * Avoid calling nextClearBit and scanning the whole registry for every block registration. + */ + @Redirect(method = "add(ILnet/minecraft/resources/ResourceLocation;Ljava/lang/Object;Ljava/lang/String;)I", at = @At(value = "INVOKE", target = "Ljava/util/BitSet;nextClearBit(I)I")) + private int useCachedBit(BitSet availabilityMap, int minimum) { + int bit = availabilityMap.nextClearBit(expectedNextBit != -1 ? expectedNextBit : minimum); + expectedNextBit = bit + 1; + return bit; + } + + @Inject(method = { "sync", "clear", "block" }, at = @At("HEAD")) + private void clearBitCache(CallbackInfo ci) { + expectedNextBit = -1; + } + + @Inject(method = "createAndAddDummy", at = @At(value = "INVOKE", target = "Ljava/util/BitSet;clear(I)V")) + private void clearBitCache2(CallbackInfo ci) { + expectedNextBit = -1; + } + + @Redirect(method = "add(ILnet/minecraft/resources/ResourceLocation;Ljava/lang/Object;Ljava/lang/String;)I", at = @At(value = "INVOKE", target = "Lorg/apache/logging/log4j/Logger;trace(Lorg/apache/logging/log4j/Marker;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V")) + private void skipTrace(Logger logger, Marker marker, String s, Object o, Object o1, Object o2, Object o3, Object o4) { + + } + + @Shadow @Final @Mutable private BiMap ids; + + @Shadow @Final @Mutable private BiMap, V> keys; + + @Shadow @Final private ResourceKey> key; + + @Shadow @Final @Mutable private BiMap names; + + @Shadow @Final @Mutable private BiMap owners; + + private FastForgeRegistry fastRegistry; + + /** + * The following code replaces the Forge HashBiMaps with a more efficient data structure based around + * an array list for IDs and one HashMap going from value -> information. + */ + @Inject(method = "", at = @At("RETURN")) + private void replaceBackingMaps(CallbackInfo ci) { + this.fastRegistry = new FastForgeRegistry<>(this.key); + this.ids = fastRegistry.getIds(); + this.keys = fastRegistry.getKeys(); + this.names = fastRegistry.getNames(); + this.owners = fastRegistry.getOwners(); + } + + @Inject(method = "freeze", at = @At("RETURN")) + private void optimizeRegistry(CallbackInfo ci) { + this.fastRegistry.optimize(); + } + + @Redirect(method = "sync", at = @At(value = "INVOKE", target = "Lcom/google/common/collect/BiMap;clear()V")) + private void clearBiMap(BiMap map) { + if(map == this.owners) { + this.fastRegistry.clear(); + } else if(map == this.keys || map == this.names || map == this.ids) { + // do nothing, the registry is faster at clearing everything at once + } else + map.clear(); + } } diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ForgeRegistrySnapshotMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ForgeRegistrySnapshotMixin.java new file mode 100644 index 00000000..ad941ab0 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ForgeRegistrySnapshotMixin.java @@ -0,0 +1,33 @@ +package org.embeddedt.modernfix.mixin.perf.fast_registry_validation; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.registries.ForgeRegistry; +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; +import java.util.Set; + +@Mixin(ForgeRegistry.Snapshot.class) +public class ForgeRegistrySnapshotMixin { + @Shadow @Final @Mutable public Map ids; + + @Shadow @Final @Mutable public Set dummied; + + /** + * The only good reason to use tree maps here is to keep the order the same. But we are tracking IDs + * anyway so order shouldn't matter. We replace the maps that will be most used. + */ + @Inject(method = "", at = @At("RETURN")) + private void replaceSnapshotMaps(CallbackInfo ci) { + this.ids = new Object2ObjectOpenHashMap<>(); + this.dummied = new ObjectOpenHashSet<>(); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ResourceKeyMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ResourceKeyMixin.java new file mode 100644 index 00000000..804dce98 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ResourceKeyMixin.java @@ -0,0 +1,28 @@ +package org.embeddedt.modernfix.mixin.perf.fast_registry_validation; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Map; + +@Mixin(ResourceKey.class) +public class ResourceKeyMixin { + private static Map>> INTERNING_MAP = new Object2ObjectOpenHashMap<>(); + @Inject(method = "create(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/resources/ResourceKey;", at = @At("HEAD"), cancellable = true) + private static void createEfficient(ResourceLocation parent, ResourceLocation location, CallbackInfoReturnable> cir) { + synchronized (ResourceKey.class) { + Map> keys = INTERNING_MAP.computeIfAbsent(parent, k -> new Object2ObjectOpenHashMap<>()); + ResourceKey key = keys.get(location); + if(key == null) { + key = new ResourceKey<>(parent, location); + keys.put(location, key); + } + cir.setReturnValue((ResourceKey)key); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/registry/FastForgeRegistry.java b/src/main/java/org/embeddedt/modernfix/registry/FastForgeRegistry.java new file mode 100644 index 00000000..67be3c21 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/registry/FastForgeRegistry.java @@ -0,0 +1,602 @@ +package org.embeddedt.modernfix.registry; + +import com.google.common.collect.BiMap; +import com.google.common.collect.Iterators; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class FastForgeRegistry { + private final BiMap ids; + private final DataFieldBiMap names; + private final DataFieldBiMap> keys; + private final DataFieldBiMap owners; + private final ResourceKey> registryKey; + + private final ObjectArrayList valuesById; + private final Object2ObjectOpenHashMap infoByValue; + + private void storeId(V value, int id) { + RegistryValueData pair = infoByValue.computeIfAbsent(value, k -> new RegistryValueData()); + pair.id = id; + } + + private void updateInfoPairAndClearIfNull(V v, Consumer consumer) { + infoByValue.compute(v, (oldValue, oldPair) -> { + if(oldPair == null) + oldPair = new RegistryValueData(); + consumer.accept(oldPair); + if(oldPair.isEmpty()) + return null; + else + return oldPair; + }); + } + + private void ensureArrayCanFitId(int id) { + int desiredSize = id + 1; + while(valuesById.size() < desiredSize) { + valuesById.add(null); + } + } + + public void clear() { + this.infoByValue.clear(); + for(int i = 0; i < this.valuesById.size(); i++) { + this.valuesById.set(i, null); + } + this.names.clearUnsafe(); + this.keys.clearUnsafe(); + this.owners.clearUnsafe(); + } + + public FastForgeRegistry(ResourceKey> registryKey) { + this.registryKey = registryKey; + this.valuesById = new ObjectArrayList<>(); + this.infoByValue = new Object2ObjectOpenHashMap<>(); + this.keys = new DataFieldBiMap<>(p -> (ResourceKey) p.key, (p, k) -> p.key = k); + this.owners = new DataFieldBiMap<>(p -> p.overrideOwner, (p, k) -> p.overrideOwner = k); + this.names = new DataFieldBiMap<>(p -> p.location, (p, l) -> p.location = l); + // IDs require a specialized implementation, as we back the K->V direction with an array + this.ids = new BiMap() { + @Nullable + @Override + public V put(@Nullable Integer key, @Nullable V value) { + RegistryValueData data = infoByValue.get(value); + int unboxedKey = key; + if(data != null && data.id != -1 && data.id != unboxedKey) + throw new IllegalArgumentException("Existing mapping for ID " + data.id + " value " + value + " when new ID " + unboxedKey + " was requested"); + ensureArrayCanFitId(unboxedKey); + V oldValue = valuesById.set(unboxedKey, value); + storeId(value, unboxedKey); + return oldValue; + } + + @Nullable + @Override + public V forcePut(@Nullable Integer key, @Nullable V value) { + ensureArrayCanFitId(key); + V oldValue = valuesById.set(key, value); + if(oldValue != null) { + updateInfoPairAndClearIfNull(oldValue, pair -> pair.id = -1); + } + storeId(value, key); + return oldValue; + } + + @Override + public void putAll(Map map) { + map.forEach(this::put); + } + + @Override + public Set values() { + return Collections.unmodifiableSet(infoByValue.keySet()); + } + + @Override + public BiMap inverse() { + return new BiMap() { + @Nullable + @Override + public Integer put(@Nullable V key, @Nullable Integer value) { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public Integer forcePut(@Nullable V key, @Nullable Integer value) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map map) { + throw new UnsupportedOperationException(); + } + + @Override + public Set values() { + throw new UnsupportedOperationException(); + } + + @Override + public BiMap inverse() { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isEmpty() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsKey(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsValue(Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public Integer get(Object key) { + RegistryValueData pair = infoByValue.get(key); + if(pair == null) + return null; + return pair.id == -1 ? null : pair.id; + } + + @Override + public Integer remove(Object key) { + RegistryValueData pair = infoByValue.get(key); + if(pair == null) + return null; + int id = pair.id; + if(id != -1) + valuesById.set(id, null); + updateInfoPairAndClearIfNull((V)key, p -> p.id = -1); + return id == -1 ? null : id; + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Set keySet() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Set> entrySet() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public int size() { + return infoByValue.size(); + } + + @Override + public boolean isEmpty() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsKey(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsValue(Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public V get(Object key) { + int id = (Integer)key; + if(id < 0 || id >= valuesById.size()) + return null; + else + return valuesById.get(id); + } + + @Override + public V remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + valuesById.clear(); + infoByValue.values().removeIf(pair -> { + pair.id = -1; + return pair.isEmpty(); + }); + } + + @NotNull + @Override + public Set keySet() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Set> entrySet() { + throw new UnsupportedOperationException(); + } + + @Override + public void forEach(BiConsumer action) { + for(int i = 0 ; i < valuesById.size(); i++) { + V val = valuesById.get(i); + if(val != null) + action.accept(i, val); + } + } + }; + } + + public void optimize() { + this.keys.optimize(); + this.owners.optimize(); + this.names.optimize(); + this.infoByValue.trim(); + } + + public BiMap getIds() { + return ids; + } + + public BiMap, V> getKeys() { + return keys; + } + + public BiMap getNames() { + return names; + } + + public DataFieldBiMap getOwners() { + return owners; + } + + /** + * Custom BiMap implementation that uses one internal hash map for the K->V direction, and the shared global + * information hash map for the V->K direction. + */ + class DataFieldBiMap implements BiMap { + public final Object2ObjectOpenHashMap valuesByKey = new Object2ObjectOpenHashMap<>(); + private final Function getter; + private final BiConsumer setter; + + public DataFieldBiMap(Function getter, BiConsumer setter) { + this.getter = getter; + this.setter = setter; + } + + public void optimize() { + this.valuesByKey.trim(); + } + + public void clearUnsafe() { + this.valuesByKey.clear(); + } + + private V forcePut(@Nullable K key, @Nullable V value, boolean throwOnExisting) { + if(throwOnExisting) { + RegistryValueData dataForValue = infoByValue.get(value); + // null check could be wrong if null is a valid value but doesn't matter in Forge's use + if(dataForValue != null && getter.apply(dataForValue) != null && !Objects.equals(getter.apply(dataForValue), key)) { + throw new IllegalArgumentException("Existing mapping for key " + key + " value " + value); + } + } + V oldValue = valuesByKey.put(key, value); + if(oldValue != null) { + updateInfoPairAndClearIfNull(oldValue, p -> setter.accept(p, null)); + } + updateInfoPairAndClearIfNull(value, p -> setter.accept(p, key)); + return oldValue; + } + + @Nullable + @Override + public V put(@Nullable K key, @Nullable V value) { + return forcePut(key, value, true); + } + + @Nullable + @Override + public V forcePut(@Nullable K key, @Nullable V value) { + return forcePut(key, value, false); + } + + @Override + public void putAll(Map map) { + map.forEach(this::put); + } + + @Override + public Set values() { + return Collections.unmodifiableSet(infoByValue.keySet()); + } + + private DataFieldBiMapInverse inverse = null; + + @Override + public BiMap inverse() { + if(inverse == null) + inverse = new DataFieldBiMapInverse<>(this); + return inverse; + } + + @Override + public int size() { + return valuesByKey.size(); + } + + @Override + public boolean isEmpty() { + return valuesByKey.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return valuesByKey.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return infoByValue.containsKey(value); + } + + @Override + public V get(Object key) { + return valuesByKey.get(key); + } + + @Override + public V remove(Object key) { + V value = get(key); + if(value == null) + return null; + inverse().remove(value); + return value; + } + + @Override + public void clear() { + valuesByKey.values().forEach(v -> updateInfoPairAndClearIfNull(v, p -> p.key = null)); + valuesByKey.clear(); + } + + @NotNull + @Override + public Set keySet() { + return Collections.unmodifiableSet(valuesByKey.keySet()); + } + + @NotNull + @Override + public Set> entrySet() { + return Collections.unmodifiableSet(valuesByKey.entrySet()); + } + + + } + + class DataFieldBiMapInverse implements BiMap { + private final DataFieldBiMap forward; + + public DataFieldBiMapInverse(DataFieldBiMap forward) { + this.forward = forward; + } + + @Nullable + @Override + public K put(@Nullable V key, @Nullable K value) { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public K forcePut(@Nullable V key, @Nullable K value) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map map) { + throw new UnsupportedOperationException(); + } + + @Override + public Set values() { + throw new UnsupportedOperationException(); + } + + @Override + public BiMap inverse() { + return forward; + } + + @Override + public int size() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isEmpty() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsKey(Object key) { + return infoByValue.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return forward.valuesByKey.containsKey(value); + } + + @Override + public K get(Object key) { + RegistryValueData pair = infoByValue.get(key); + if(pair == null) + return null; + else + return forward.getter.apply(pair); + } + + @Override + public K remove(Object key) { + RegistryValueData pair = infoByValue.get(key); + if(pair == null) + return null; + else { + K rk = forward.getter.apply(pair); + forward.valuesByKey.remove(rk); + updateInfoPairAndClearIfNull((V)key, p -> forward.setter.accept(p, null)); + return rk; + } + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Set keySet() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Set> entrySet() { + throw new UnsupportedOperationException(); + } + } + + class FastForgeRegistryLocationSet implements Set { + private final Set> backingSet; + + public FastForgeRegistryLocationSet(Set> backingSet) { + this.backingSet = backingSet; + } + + @Override + public int size() { + return backingSet.size(); + } + + @Override + public boolean isEmpty() { + return backingSet.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return backingSet.contains(ResourceKey.create(FastForgeRegistry.this.registryKey, (ResourceLocation)o)); + } + + @NotNull + @Override + public Iterator iterator() { + return Iterators.transform(backingSet.iterator(), ResourceKey::location); + } + + @NotNull + @Override + public Object[] toArray() { + Object[] keyArray = backingSet.toArray(); + for(int i = 0; i < keyArray.length; i++) { + keyArray[i] = ((ResourceKey)keyArray[i]).location(); + } + return keyArray; + } + + @NotNull + @Override + public T[] toArray(@NotNull T[] a) { + Object[] keyArray = backingSet.toArray(); + T[] finalArray = Arrays.copyOf(a, keyArray.length); + for(int i = 0; i < keyArray.length; i++) { + finalArray[i] = (T)((ResourceKey)keyArray[i]).location(); + } + return finalArray; + } + + @Override + public boolean add(ResourceLocation resourceLocation) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsAll(@NotNull Collection c) { + for(Object o : c) { + if(!contains(o)) + return false; + } + return true; + } + + @Override + public boolean addAll(@NotNull Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(@NotNull Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(@NotNull Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + } + + static class RegistryValueData { + public ResourceKey key; + public ResourceLocation location; + public int id = -1; + public Object overrideOwner; + + boolean isEmpty() { + return key == null && location == null && id == -1 && overrideOwner == null; + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/screen/ModernFixConfigScreen.java b/src/main/java/org/embeddedt/modernfix/screen/ModernFixConfigScreen.java new file mode 100644 index 00000000..53a7d02b --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/screen/ModernFixConfigScreen.java @@ -0,0 +1,40 @@ +package org.embeddedt.modernfix.screen; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; + +import javax.annotation.Nullable; + +public class ModernFixConfigScreen extends Screen { + private OptionList optionList; + private Screen lastScreen; + + public boolean madeChanges = false; + private Button doneButton; + public ModernFixConfigScreen(@Nullable Screen lastScreen) { + super(Component.translatable("modernfix.config")); + this.lastScreen = lastScreen; + } + + @Override + protected void init() { + this.optionList = new OptionList(this, this.minecraft); + this.addWidget(this.optionList); + this.doneButton = new Button.Builder(CommonComponents.GUI_DONE, (arg) -> { + this.minecraft.setScreen(lastScreen); + }).pos(this.width / 2 - 100, this.height - 29).size(200, 20).build(); + this.addRenderableWidget(this.doneButton); + } + + @Override + public void render(PoseStack poseStack, int mouseX, int mouseY, float partialTicks) { + this.renderBackground(poseStack); + this.optionList.render(poseStack, mouseX, mouseY, partialTicks); + drawCenteredString(poseStack, this.font, this.title, this.width / 2, 8, 16777215); + this.doneButton.setMessage(madeChanges ? Component.translatable("modernfix.config.done_restart") : CommonComponents.GUI_DONE); + super.render(poseStack, mouseX, mouseY, partialTicks); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/screen/OptionList.java b/src/main/java/org/embeddedt/modernfix/screen/OptionList.java new file mode 100644 index 00000000..00775d00 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/screen/OptionList.java @@ -0,0 +1,122 @@ +package org.embeddedt.modernfix.screen; + +import com.google.common.collect.ImmutableList; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.ChatFormatting; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.ContainerObjectSelectionList; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.components.toasts.SystemToast; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.core.ModernFixMixinPlugin; +import org.embeddedt.modernfix.core.config.Option; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +public class OptionList extends ContainerObjectSelectionList { + private final int maxNameWidth; + + private static final Component OPTION_ON = Component.translatable("modernfix.option.on").withStyle(style -> style.withColor(ChatFormatting.GREEN)); + private static final Component OPTION_OFF = Component.translatable("modernfix.option.off").withStyle(style -> style.withColor(ChatFormatting.RED)); + + private ModernFixConfigScreen mainScreen; + + + public OptionList(ModernFixConfigScreen arg, Minecraft arg2) { + super(arg2,arg.width + 45, arg.height, 43, arg.height - 32, 20); + + this.mainScreen = arg; + + int maxW = 0; + Map optionMap = ModernFixMixinPlugin.config.getOptionMap(); + List sortedKeys = optionMap.keySet().stream().filter(key -> !key.equals("mixin.core")).sorted().collect(Collectors.toList()); + for(String key : sortedKeys) { + Option option = optionMap.get(key); + int w = this.minecraft.font.width(key); + maxW = Math.max(w, maxW); + this.addEntry(new OptionEntry(key, option)); + } + this.maxNameWidth = maxW; + } + + protected int getScrollbarPosition() { + return super.getScrollbarPosition() + 15 + 20; + } + + public int getRowWidth() { + return super.getRowWidth() + 32; + } + + class OptionEntry extends Entry { + private final String name; + + private final Button toggleButton; + private final Option option; + + public OptionEntry(String optionName, Option option) { + this.name = optionName; + this.option = option; + this.toggleButton = new Button.Builder(Component.literal(""), (arg) -> { + this.option.setEnabled(!this.option.isEnabled(), !this.option.isUserDefined()); + try { + ModernFixMixinPlugin.config.save(); + if(!OptionList.this.mainScreen.madeChanges) { + OptionList.this.mainScreen.madeChanges = true; + } + } catch(IOException e) { + // revert + this.option.setEnabled(!this.option.isEnabled(), !this.option.isUserDefined()); + ModernFix.LOGGER.error("Unable to save config", e); + } + }).pos(0, 0).size(95, 20).build(); + } + + @Override + public void render(PoseStack matrixStack, int index, int top, int left, int width, int height, int mouseX, int mouseY, boolean isMouseOver, float partialTicks) { + MutableComponent nameComponent = Component.literal(this.name); + if(this.option.isUserDefined()) + nameComponent = nameComponent.withStyle(ChatFormatting.ITALIC).append(Component.translatable("modernfix.config.not_default")); + OptionList.this.minecraft.font.draw(matrixStack, nameComponent, (float)(left + 160 - OptionList.this.maxNameWidth), (float)(top + height / 2 - 4), 16777215); + this.toggleButton.setPosition(left + 175, top); + this.toggleButton.setMessage(getOptionMessage(this.option)); + this.toggleButton.render(matrixStack, mouseX, mouseY, partialTicks); + } + + private Component getOptionMessage(Option option) { + return option.isEnabled() ? OPTION_ON : OPTION_OFF; + } + + @Override + public List children() { + return ImmutableList.of(this.toggleButton); + } + + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return this.toggleButton.mouseClicked(mouseX, mouseY, button); + } + + public boolean mouseReleased(double mouseX, double mouseY, int button) { + return this.toggleButton.mouseReleased(mouseX, mouseY, button); + } + + @Override + public List narratables() { + return Collections.emptyList(); + } + } + + public abstract static class Entry extends ContainerObjectSelectionList.Entry { + public Entry() { + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/util/CachedResourcePath.java b/src/main/java/org/embeddedt/modernfix/util/CachedResourcePath.java index 5b32b41f..889311ae 100644 --- a/src/main/java/org/embeddedt/modernfix/util/CachedResourcePath.java +++ b/src/main/java/org/embeddedt/modernfix/util/CachedResourcePath.java @@ -25,30 +25,30 @@ public class CachedResourcePath { private static final String[] NO_PREFIX = new String[0]; public CachedResourcePath(Path path) { - this(NO_PREFIX, path, path.getNameCount()); + this(NO_PREFIX, path, path.getNameCount(), true); } public CachedResourcePath(String s) { // normalize so we can guarantee there are no empty sections - this(NO_PREFIX, SLASH_SPLITTER.splitToList(FileUtil.normalize(s))); + this(NO_PREFIX, SLASH_SPLITTER.splitToList(FileUtil.normalize(s)), false); } - public CachedResourcePath(String[] prefixElements, Collection collection) { - this(prefixElements, collection, collection.size()); + public CachedResourcePath(String[] prefixElements, Collection collection, boolean intern) { + this(prefixElements, collection, collection.size(), intern); } - public CachedResourcePath(String[] prefixElements, Iterable path, int count) { + public CachedResourcePath(String[] prefixElements, Iterable path, int count, boolean intern) { String[] components = new String[prefixElements.length + count]; int i = 0; while(i < prefixElements.length) { - components[i] = PATH_COMPONENT_INTERNER.intern(prefixElements[i]); + components[i] = intern ? PATH_COMPONENT_INTERNER.intern(prefixElements[i]) : prefixElements[i]; i++; } for(Object component : path) { String s = component.toString(); if(s.length() == 0) continue; - components[i] = PATH_COMPONENT_INTERNER.intern(s); + components[i] = intern ? PATH_COMPONENT_INTERNER.intern(s) : s; i++; } pathComponents = components; diff --git a/src/main/java/org/embeddedt/modernfix/util/CanonizingStringMap.java b/src/main/java/org/embeddedt/modernfix/util/CanonizingStringMap.java index 737e9bca..e5bb2f2d 100644 --- a/src/main/java/org/embeddedt/modernfix/util/CanonizingStringMap.java +++ b/src/main/java/org/embeddedt/modernfix/util/CanonizingStringMap.java @@ -98,7 +98,8 @@ public class CanonizingStringMap implements Map { @NotNull @Override public Set keySet() { - return Collections.unmodifiableSet(this.backingMap.keySet()); + // has to be modifiable because mods (cough, Tinkers) use it to clear the tag + return this.backingMap.keySet(); } @NotNull diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index ad15f64e..741b3a48 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -33,3 +33,6 @@ public net.minecraft.client.resources.model.ModelBakery$ModelBakerImpl public net.minecraft.client.resources.model.ModelBakery$ModelBakerImpl (Lnet/minecraft/client/resources/model/ModelBakery;Ljava/util/function/BiFunction;Lnet/minecraft/resources/ResourceLocation;)V # public net.minecraft.nbt.CompoundTag (Ljava/util/Map;)V # public net.minecraft.client.renderer.texture.TextureAtlasSprite (Lnet/minecraft/client/renderer/texture/TextureAtlas;Lnet/minecraft/client/renderer/texture/TextureAtlasSprite$Info;IIIIILcom/mojang/blaze3d/platform/NativeImage;)V # +public net.minecraft.server.level.DistanceManager m_140792_(Lnet/minecraft/server/level/TicketType;Lnet/minecraft/world/level/ChunkPos;ILjava/lang/Object;)V # addTicket +public net.minecraft.server.level.ChunkMap$DistanceManager +public net.minecraft.resources.ResourceKey (Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)V # diff --git a/src/main/resources/assets/modernfix/lang/en_us.json b/src/main/resources/assets/modernfix/lang/en_us.json index b757242b..ddd8c10b 100644 --- a/src/main/resources/assets/modernfix/lang/en_us.json +++ b/src/main/resources/assets/modernfix/lang/en_us.json @@ -1,6 +1,13 @@ { + "key.modernfix": "ModernFix", + "key.modernfix.config": "Open config screen", "modernfix.jei_load": "Loading JEI, this may take a while", "modernfix.no_lazydfu": "ModernFix detected that DFU rules were compiled on startup. This slows down game launching. Installing LazyDFU to resolve this is highly recommended.", + "modernfix.config": "ModernFix mixin config", + "modernfix.config.done_restart": "Done (restart required)", + "modernfix.option.on": "on", + "modernfix.option.off": "off", + "modernfix.config.not_default": " (modified)", "asynclocator.map.locating": "Map (Locating...)", "asynclocator.map.none": "Map (No nearby feature found)" } diff --git a/src/main/resources/modernfix.mixins.json b/src/main/resources/modernfix.mixins.json index 32a7a465..d83edf57 100644 --- a/src/main/resources/modernfix.mixins.json +++ b/src/main/resources/modernfix.mixins.json @@ -8,6 +8,9 @@ "mixins": [ "core.BootstrapMixin", "bugfix.edge_chunk_not_saved.ChunkManagerMixin", + "bugfix.paper_chunk_patches.ChunkMapMixin", + "bugfix.paper_chunk_patches.ChunkHolderMixin", + "bugfix.paper_chunk_patches.SortedArraySetMixin", "perf.dynamic_structure_manager.StructureManagerMixin", "bugfix.chunk_deadlock.ServerChunkCacheMixin", "perf.dedicated_reload_executor.MinecraftServerMixin", @@ -29,10 +32,12 @@ "feature.branding.BrandingControlMixin", "feature.direct_stack_trace.CrashReportMixin", "perf.nbt_memory_usage.CompoundTagMixin", - "perf.fast_registry_validation.ForgeRegistryMixin", "perf.kubejs.RecipeEventJSMixin", "perf.tag_id_caching.TagEntryMixin", "perf.tag_id_caching.TagOrElementLocationMixin", + "perf.fast_registry_validation.ForgeRegistryMixin", + "perf.fast_registry_validation.ForgeRegistrySnapshotMixin", + "perf.fast_registry_validation.ResourceKeyMixin", "perf.cache_strongholds.ChunkGeneratorMixin", "perf.cache_upgraded_structures.StructureManagerMixin", "perf.cache_strongholds.ServerLevelMixin", @@ -41,7 +46,8 @@ "perf.compress_blockstate.BlockBehaviourMixin", "perf.dedup_blockstate_flattening_map.BlockStateDataMixin", "perf.dedup_blockstate_flattening_map.ChunkPalettedStorageFixMixin", - "devenv.MinecraftServerMixin" + "devenv.MinecraftServerMixin", + "devenv.GameDataMixin" ], "client": [ "core.MinecraftMixin", @@ -55,9 +61,11 @@ "perf.dynamic_resources.BlockModelShaperMixin", "perf.dynamic_resources.ItemModelShaperMixin", "perf.dynamic_resources.ModelBakerImplMixin", + "perf.dynamic_resources.ItemRendererMixin", "perf.dynamic_resources.ModelBakeryMixin", "perf.dynamic_resources.ModelManagerMixin", "perf.dynamic_resources.ae2.RegistrationMixin", + "perf.dynamic_resources.rs.ClientSetupMixin", "perf.dynamic_resources.ctm.TextureMetadataHandlerMixin", "perf.dynamic_resources.ctm.CTMPackReloadListenerMixin", "perf.dynamic_resources.supermartijncore.ClientRegistrationHandlerMixin",