Compare commits

..

1 Commits
1.21.1 ... 1.20

Author SHA1 Message Date
embeddedt
292a6aeab3
Fix optimize_surface_rules breaking mods that provide custom BiomeManagers 2026-06-11 20:01:31 -04:00
210 changed files with 7225 additions and 2034 deletions

View File

@ -161,7 +161,6 @@ public class ClientMixinValidator {
clzsses = wrappedClzss.stream() clzsses = wrappedClzss.stream()
.map(AnnotationValue::getValue) .map(AnnotationValue::getValue)
.filter(o -> o instanceof TypeMirror)
.map(TypeMirror.class::cast) .map(TypeMirror.class::cast)
.collect(Collectors.toSet()); .collect(Collectors.toSet());

View File

@ -25,7 +25,7 @@ public record MixinConfig(
InjectorOptions injectors, OverwriteOptions overwrites InjectorOptions injectors, OverwriteOptions overwrites
) { ) {
public MixinConfig(String packageName, List<String> commonMixins, List<String> clientMixins) { public MixinConfig(String packageName, List<String> commonMixins, List<String> clientMixins) {
this(true, "0.8", packageName, "org.embeddedt.modernfix.core.ModernFixMixinPlugin", "JAVA_21", this(true, "0.8", packageName, "org.embeddedt.modernfix.core.ModernFixMixinPlugin", "JAVA_17",
commonMixins, clientMixins, InjectorOptions.DEFAULT, OverwriteOptions.DEFAULT); commonMixins, clientMixins, InjectorOptions.DEFAULT, OverwriteOptions.DEFAULT);
} }
public record InjectorOptions(int defaultRequire) { public record InjectorOptions(int defaultRequire) {

View File

@ -1,5 +1,5 @@
plugins { plugins {
id("net.neoforged.moddev") version("2.0.134") id("net.neoforged.moddev.legacyforge") version("2.0.134")
id("me.modmuss50.mod-publish-plugin") version("1.1.0") id("me.modmuss50.mod-publish-plugin") version("1.1.0")
} }
@ -16,11 +16,11 @@ val gitVersion = providers.of(GitVersionSource::class) {
version = gitVersion.get() version = gitVersion.get()
base.archivesName = "modernfix-neoforge" base.archivesName = "modernfix-forge"
neoForge { legacyForge {
enable { enable {
version = rootProject.properties["forge_version"].toString() forgeVersion = rootProject.properties["forge_version"].toString()
isDisableRecompilation = System.getenv("CI") == "true" isDisableRecompilation = System.getenv("CI") == "true"
} }
@ -51,8 +51,14 @@ neoForge {
} }
} }
mixin {
add(sourceSets.main.get(), "modernfix.refmap.json")
config("modernfix-modernfix.mixins.json")
}
tasks.named<Jar>("jar") { tasks.named<Jar>("jar") {
manifest.attributes(mapOf( manifest.attributes(mapOf(
"MixinConfigs" to "modernfix-modernfix.mixins.json",
"Specification-Version" to "1", "Specification-Version" to "1",
"Implementation-Title" to project.name, "Implementation-Title" to project.name,
"Implementation-Version" to version "Implementation-Version" to version
@ -60,7 +66,7 @@ tasks.named<Jar>("jar") {
} }
java { java {
val curSourceCompatLevel = JavaVersion.VERSION_21 val curSourceCompatLevel = JavaVersion.VERSION_17
sourceCompatibility = curSourceCompatLevel sourceCompatibility = curSourceCompatLevel
targetCompatibility = curSourceCompatLevel targetCompatibility = curSourceCompatLevel
} }
@ -106,17 +112,24 @@ dependencies {
"additionalRuntimeClasspath"(project(":annotations")) "additionalRuntimeClasspath"(project(":annotations"))
annotationProcessor(project(path = ":annotation-processor", configuration = "shadow")) annotationProcessor(project(path = ":annotation-processor", configuration = "shadow"))
val mixinextrasVersion = rootProject.properties["mixinextras_version"].toString()
implementation("io.github.llamalad7:mixinextras-common:${mixinextrasVersion}")
annotationProcessor("net.fabricmc:sponge-mixin:0.12.5+mixin.0.8.5")
annotationProcessor("io.github.llamalad7:mixinextras-common:${mixinextrasVersion}")
implementation("io.github.llamalad7:mixinextras-forge:${mixinextrasVersion}")
"jarJar"("io.github.llamalad7:mixinextras-forge:${mixinextrasVersion}")
val jei_version = rootProject.properties["jei_version"].toString() val jei_version = rootProject.properties["jei_version"].toString()
compileOnly("mezz.jei:jei-${minecraft_version}-neoforge:${jei_version}") modCompileOnly("mezz.jei:jei-${minecraft_version}-forge:${jei_version}")
compileOnly("curse.maven:spark-361579:${rootProject.properties["spark_version"].toString()}") modCompileOnly("curse.maven:spark-361579:${rootProject.properties["spark_version"].toString()}")
compileOnly("curse.maven:ctm-267602:${rootProject.properties["ctm_version"].toString()}") modCompileOnly("curse.maven:ctm-267602:${rootProject.properties["ctm_version"].toString()}")
compileOnly("curse.maven:ldlib-626676:${rootProject.properties["ldlib_version"].toString()}") modCompileOnly("curse.maven:ldlib-626676:${rootProject.properties["ldlib_version"].toString()}")
compileOnly("curse.maven:supermartijncore-454372:4455391") modCompileOnly("curse.maven:supermartijncore-454372:4455391")
compileOnly("curse.maven:patchouli-306770:6164575") modCompileOnly("curse.maven:patchouli-306770:6164575")
compileOnly("curse.maven:cofhcore-69162:5374122") modCompileOnly("curse.maven:cofhcore-69162:5374122")
compileOnly("curse.maven:resourcefullib-570073:5659871") modCompileOnly("curse.maven:resourcefullib-570073:5659871")
compileOnly("curse.maven:kubejs-238086:5853326") modCompileOnly("curse.maven:kubejs-238086:5853326")
compileOnly("curse.maven:terrablender-neoforge-940057:6054947") modCompileOnly("curse.maven:terrablender-563928:6290448")
} }
tasks.named<Jar>("jar") { tasks.named<Jar>("jar") {
@ -148,12 +161,12 @@ tasks.named<ProcessResources>("processResources") {
inputs.property("version", project.version) inputs.property("version", project.version)
filesMatching("META-INF/neoforge.mods.toml") { filesMatching("META-INF/mods.toml") {
expand("version" to project.version) expand("version" to project.version)
} }
} }
val finalJarTask = "jar" val finalJarTask = "reobfJar"
tasks.register<Copy>("copyJarNameConsistent") { tasks.register<Copy>("copyJarNameConsistent") {
from(tasks.named<Jar>(finalJarTask).get().outputs.files) from(tasks.named<Jar>(finalJarTask).get().outputs.files)
@ -177,7 +190,7 @@ publishMods {
changelog = "Please check the [GitHub wiki](https://github.com/embeddedt/ModernFix/wiki/Changelog) for major changes." changelog = "Please check the [GitHub wiki](https://github.com/embeddedt/ModernFix/wiki/Changelog) for major changes."
type = STABLE type = STABLE
modLoaders.add("neoforge") modLoaders.add("forge")
curseforge { curseforge {
projectId = "790626" projectId = "790626"

View File

@ -5,29 +5,28 @@ junit_version=5.10.0-M1
mixinextras_version=0.4.1 mixinextras_version=0.4.1
mod_id=modernfix mod_id=modernfix
minecraft_version=1.21.1 minecraft_version=1.20.1
enabled_platforms=neoforge enabled_platforms=forge
forge_version=21.1.111 forge_version=1.20.1-47.4.0
parchment_version=2024.11.17 parchment_version=2023.07.09
parchment_mc_version=1.21.1
refined_storage_version=4392788 refined_storage_version=4392788
jei_version=19.21.2.313 jei_version=15.8.0.11
rei_version=13.0.678 rei_version=11.0.597
ctm_version=5587515 ctm_version=5983309
ldlib_version=5782845 ldlib_version=5927130
kubejs_version=2101.7.1-build.181 kubejs_version=2001.6.5-build.16
rhino_version=2101.2.7-build.74 rhino_version=2001.2.3-build.10
supported_minecraft_versions=1.21.1 supported_minecraft_versions=1.20.1
fabric_loader_version=0.16.10 fabric_loader_version=0.16.10
fabric_api_version=0.102.1+1.21.1 fabric_api_version=0.86.0+1.20.1
continuity_version=3.0.0-beta.4+1.20.2 continuity_version=3.0.0-beta.2+1.19.3
modmenu_version=11.0.3 modmenu_version=7.0.0-beta.2
diagonal_fences_version=4558828 diagonal_fences_version=4558828
spark_version=6225208 spark_version=4587310
use_fabric_api_at_runtime=true use_fabric_api_at_runtime=true

View File

@ -5,7 +5,7 @@ import re
def get_valid_mixin_options(): def get_valid_mixin_options():
all_mixin_options = set() all_mixin_options = set()
# gather all mixins in mixin folders # gather all mixins in mixin folders
for platform in [ "common", "fabric", "forge" ]: for platform in [ "common", "forge" ]:
base_path = f"{platform}/src/main/java/org/embeddedt/modernfix/{platform}/mixin" base_path = f"{platform}/src/main/java/org/embeddedt/modernfix/{platform}/mixin"
for root, dirs, files in os.walk(base_path): for root, dirs, files in os.walk(base_path):
for file in files: for file in files:

View File

@ -7,12 +7,14 @@ import org.embeddedt.modernfix.api.constants.IntegrationConstants;
import org.embeddedt.modernfix.api.entrypoint.ModernFixClientIntegration; import org.embeddedt.modernfix.api.entrypoint.ModernFixClientIntegration;
import org.embeddedt.modernfix.core.ModernFixMixinPlugin; import org.embeddedt.modernfix.core.ModernFixMixinPlugin;
import org.embeddedt.modernfix.platform.ModernFixPlatformHooks; import org.embeddedt.modernfix.platform.ModernFixPlatformHooks;
import org.embeddedt.modernfix.searchtree.JEIBackedSearchTree;
import org.embeddedt.modernfix.searchtree.SearchTreeProviderRegistry;
import org.embeddedt.modernfix.spark.SparkLaunchProfiler; import org.embeddedt.modernfix.spark.SparkLaunchProfiler;
import org.embeddedt.modernfix.util.ClassInfoManager; import org.embeddedt.modernfix.util.ClassInfoManager;
import org.embeddedt.modernfix.world.IntegratedWatchdog; import org.embeddedt.modernfix.world.IntegratedWatchdog;
import java.lang.management.ManagementFactory; import java.lang.management.ManagementFactory;
import java.util.List; import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
public class ModernFixClient { public class ModernFixClient {
@ -38,6 +40,7 @@ public class ModernFixClient {
if(ModernFixMixinPlugin.instance.isOptionEnabled("feature.branding.F3Screen")) { if(ModernFixMixinPlugin.instance.isOptionEnabled("feature.branding.F3Screen")) {
brandingString = ModernFix.NAME + " " + ModernFixPlatformHooks.INSTANCE.getVersionString(); brandingString = ModernFix.NAME + " " + ModernFixPlatformHooks.INSTANCE.getVersionString();
} }
SearchTreeProviderRegistry.register(JEIBackedSearchTree.PROVIDER);
for(String className : ModernFixPlatformHooks.INSTANCE.getCustomModOptions().get(IntegrationConstants.CLIENT_INTEGRATION_CLASS)) { for(String className : ModernFixPlatformHooks.INSTANCE.getCustomModOptions().get(IntegrationConstants.CLIENT_INTEGRATION_CLASS)) {
try { try {
CLIENT_INTEGRATIONS.add((ModernFixClientIntegration)Class.forName(className).getDeclaredConstructor().newInstance()); CLIENT_INTEGRATIONS.add((ModernFixClientIntegration)Class.forName(className).getDeclaredConstructor().newInstance());

View File

@ -1,10 +1,11 @@
package org.embeddedt.modernfix.api.entrypoint; package org.embeddedt.modernfix.api.entrypoint;
import net.minecraft.client.resources.model.BakedModel; import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.ModelBakery; import net.minecraft.client.resources.model.*;
import net.minecraft.client.resources.model.ModelResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.client.resources.model.ModelState;
import net.minecraft.client.resources.model.UnbakedModel; import java.util.function.Function;
/** /**
* Implement this interface in a mod class and add it to "modernfix:integration_v1" in your mod metadata file * Implement this interface in a mod class and add it to "modernfix:integration_v1" in your mod metadata file
@ -20,10 +21,49 @@ public interface ModernFixClientIntegration {
default void onDynamicResourcesStatusChange(boolean enabled) { default void onDynamicResourcesStatusChange(boolean enabled) {
} }
/**
* Called to allow mods to observe the loading of an unbaked model and either make changes to it or wrap it with their
* own instance.
* @param location the ResourceLocation of the model (this may be a ModelResourceLocation)
* @param originalModel the original model
* @param bakery the model bakery - do not touch internal fields as they probably don't behave the way you expect
* with dynamic resources on
* @return the model which should actually be loaded for this resource location
*/
default UnbakedModel onUnbakedModelLoad(ResourceLocation location, UnbakedModel originalModel, ModelBakery bakery) {
return originalModel;
}
/**
* Called to allow mods to observe the use of an unbaked model at bake time and either make changes to it or wrap it with their
* own instance.
* @param location the ResourceLocation of the model (this may be a ModelResourceLocation)
* @param originalModel the original model
* @param bakery the model bakery - do not touch internal fields as they probably don't behave the way you expect
* with dynamic resources on
* @return the model which should actually be loaded for this resource location
*/
default UnbakedModel onUnbakedModelPreBake(ResourceLocation location, UnbakedModel originalModel, ModelBakery bakery) {
return originalModel;
}
/**
* Called to allow mods to observe the loading of a baked model and either make changes to it or wrap it with their
* own instance.
* @param location the ResourceLocation of the model (this may be a ModelResourceLocation)
* @param originalModel the original model
* @param bakery the model bakery - do not touch internal fields as they probably don't behave the way you expect
* with dynamic resources on
* @return the model which should actually be loaded for this resource location
*/
@Deprecated
default BakedModel onBakedModelLoad(ResourceLocation location, UnbakedModel baseModel, BakedModel originalModel, ModelState state, ModelBakery bakery) {
return originalModel;
}
/** /**
* Called to allow mods to observe the loading of a baked model and either make changes to it or wrap it with their * Called to allow mods to observe the loading of a baked model and either make changes to it or wrap it with their
* own instance. * own instance.
*
* @param location the ResourceLocation of the model (this may be a ModelResourceLocation) * @param location the ResourceLocation of the model (this may be a ModelResourceLocation)
* @param originalModel the original model * @param originalModel the original model
* @param bakery the model bakery - do not touch internal fields as they probably don't behave the way you expect * @param bakery the model bakery - do not touch internal fields as they probably don't behave the way you expect
@ -31,7 +71,7 @@ public interface ModernFixClientIntegration {
* @param textureGetter function to retrieve textures for this model * @param textureGetter function to retrieve textures for this model
* @return the model which should actually be loaded for this resource location * @return the model which should actually be loaded for this resource location
*/ */
default BakedModel onBakedModelLoad(ModelResourceLocation location, UnbakedModel baseModel, BakedModel originalModel, ModelState state, ModelBakery bakery, ModelBakery.TextureGetter textureGetter) { default BakedModel onBakedModelLoad(ResourceLocation location, UnbakedModel baseModel, BakedModel originalModel, ModelState state, ModelBakery bakery, Function<Material, TextureAtlasSprite> textureGetter) {
return originalModel; return onBakedModelLoad(location, baseModel, originalModel, state, bakery);
} }
} }

View File

@ -1,18 +1,23 @@
package org.embeddedt.modernfix.api.helpers; package org.embeddedt.modernfix.api.helpers;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.*; import net.minecraft.client.resources.model.*;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.StateDefinition;
import org.embeddedt.modernfix.duck.IExtendedModelBakery;
import org.embeddedt.modernfix.dynamicresources.ModelBakeryHelpers; import org.embeddedt.modernfix.dynamicresources.ModelBakeryHelpers;
import org.embeddedt.modernfix.util.DynamicMap; import org.embeddedt.modernfix.util.DynamicMap;
import org.jetbrains.annotations.Nullable;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Function;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class ModelHelpers { public final class ModelHelpers {
@ -23,7 +28,7 @@ public final class ModelHelpers {
* @return a list of all blockstates related to the model * @return a list of all blockstates related to the model
*/ */
public static ImmutableList<BlockState> getBlockStateForLocation(ModelResourceLocation location) { public static ImmutableList<BlockState> getBlockStateForLocation(ModelResourceLocation location) {
Optional<Block> blockOpt = BuiltInRegistries.BLOCK.getOptional(location.id()); Optional<Block> blockOpt = BuiltInRegistries.BLOCK.getOptional(new ResourceLocation(location.getNamespace(), location.getPath()));
if(blockOpt.isPresent()) if(blockOpt.isPresent())
return ModelBakeryHelpers.getBlockStatesForMRL(blockOpt.get().getStateDefinition(), location); return ModelBakeryHelpers.getBlockStatesForMRL(blockOpt.get().getStateDefinition(), location);
else else
@ -47,7 +52,7 @@ public final class ModelHelpers {
* @return a fake map of the top-level models * @return a fake map of the top-level models
*/ */
public static Map<ResourceLocation, BakedModel> createFakeTopLevelMap(BiFunction<ResourceLocation, ModelState, BakedModel> modelGetter) { public static Map<ResourceLocation, BakedModel> createFakeTopLevelMap(BiFunction<ResourceLocation, ModelState, BakedModel> modelGetter) {
return new DynamicMap<>(ResourceLocation.class, location -> modelGetter.apply(location, BlockModelRotation.X0_Y0)); return new DynamicMap<>(location -> modelGetter.apply(location, BlockModelRotation.X0_Y0));
} }
/** /**
@ -56,8 +61,6 @@ public final class ModelHelpers {
* @return an appropriate ModelBaker * @return an appropriate ModelBaker
*/ */
public static ModelBaker adaptBakery(ModelBakery bakery) { public static ModelBaker adaptBakery(ModelBakery bakery) {
throw new UnsupportedOperationException("TODO");
/*
return new ModelBaker() { return new ModelBaker() {
@Override @Override
public UnbakedModel getModel(ResourceLocation resourceLocation) { public UnbakedModel getModel(ResourceLocation resourceLocation) {
@ -69,8 +72,16 @@ public final class ModelHelpers {
public BakedModel bake(ResourceLocation resourceLocation, ModelState modelState) { public BakedModel bake(ResourceLocation resourceLocation, ModelState modelState) {
return ((IExtendedModelBakery)bakery).bakeDefault(resourceLocation, modelState); return ((IExtendedModelBakery)bakery).bakeDefault(resourceLocation, modelState);
} }
};
*/ @Override
public @Nullable BakedModel bake(ResourceLocation location, ModelState state, Function<Material, TextureAtlasSprite> sprites) {
throw new UnsupportedOperationException();
}
@Override
public Function<Material, TextureAtlasSprite> getModelTextureGetter() {
return Material::sprite;
}
};
} }
} }

View File

@ -0,0 +1,185 @@
package org.embeddedt.modernfix.benchmark;
import com.google.common.util.concurrent.MoreExecutors;
import com.mojang.datafixers.util.Either;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.*;
import net.minecraft.util.Unit;
import net.minecraft.world.entity.ai.village.poi.PoiManager;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.chunk.storage.ChunkSerializer;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.embeddedt.modernfix.ModernFix;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Function;
public class WorldgenBenchmark {
private static final TicketType<Unit> BENCHMARK_TICKET =
TicketType.create("modernfix_benchmark", (a, b) -> 0);
private static final List<ChunkStatus> ALL_STATUSES = ChunkStatus.getStatusList().stream()
.filter(s -> s.getIndex() > ChunkStatus.EMPTY.getIndex()
&& s.getIndex() < ChunkStatus.INITIALIZE_LIGHT.getIndex())
.toList();
private static final int REQUIRED_LOAD_RADIUS = ALL_STATUSES.stream().mapToInt(ChunkStatus::getRange).max().orElse(0);
public static String run(ServerLevel level, ChunkPos center, int testRadius, int iterations, ChunkStatus startStatus, ChunkStatus stopStatus) {
int startIndex = ALL_STATUSES.indexOf(startStatus);
if (startIndex < 0) {
throw new IllegalArgumentException("Invalid start status: " + startStatus);
}
int stopIndex = ALL_STATUSES.indexOf(stopStatus);
if (stopIndex < 0) {
throw new IllegalArgumentException("Invalid stop status:" + stopStatus);
}
List<ChunkStatus> setupStatuses = ALL_STATUSES.subList(0, startIndex);
List<ChunkStatus> timedStatuses = ALL_STATUSES.subList(startIndex, stopIndex + 1);
Context ctx = new Context(level, center, testRadius);
long[] timings = new long[timedStatuses.size()];
int testDiameter = 2 * testRadius + 1;
int numPositions = testDiameter * testDiameter;
ChunkPos[] testPositions = new ChunkPos[numPositions];
CompoundTag[] snapshots = new CompoundTag[numPositions];
ChunkAccess[][] neighborArrays = new ChunkAccess[numPositions][];
int idx = 0;
for (int tz = -testRadius; tz <= testRadius; tz++) {
for (int tx = -testRadius; tx <= testRadius; tx++) {
ChunkPos testPos = new ChunkPos(center.x + tx, center.z + tz);
testPositions[idx] = testPos;
neighborArrays[idx] = ctx.buildNeighborArray(testPos);
ProtoChunk setupProto = ctx.newProtoChunk(testPos);
neighborArrays[idx][ctx.centerIndex] = setupProto;
for (ChunkStatus status : setupStatuses) {
status.generate(ctx.executor, level, ctx.generator, ctx.templates,
ctx.lightEngine, ctx.noopPromotion, Arrays.asList(neighborArrays[idx])).join();
}
snapshots[idx] = ChunkSerializer.write(level, setupProto);
idx++;
ModernFix.LOGGER.info("worldgen benchmark setup progress: {}/{}", idx, numPositions);
}
}
ModernFix.LOGGER.info("worldgen benchmark setup complete");
for (int iter = 0; iter < iterations; iter++) {
ModernFix.LOGGER.info("worldgen benchmark iteration: {}/{}", iter + 1, iterations);
for (int p = 0; p < numPositions; p++) {
ProtoChunk restored = ChunkSerializer.read(
level, ctx.poiManager, testPositions[p], snapshots[p]);
neighborArrays[p][ctx.centerIndex] = restored;
List<ChunkAccess> neighborList = Arrays.asList(neighborArrays[p]);
for (int s = 0; s < timedStatuses.size(); s++) {
long t0 = System.nanoTime();
timedStatuses.get(s).generate(ctx.executor, level, ctx.generator,
ctx.templates, ctx.lightEngine, ctx.noopPromotion, neighborList).join();
timings[s] += System.nanoTime() - t0;
}
}
}
ModernFix.LOGGER.info("worldgen benchmark done");
ctx.cleanup();
return formatTimings(timedStatuses, timings, testRadius, iterations);
}
private static String formatTimings(List<ChunkStatus> statuses, long[] timings, int testRadius, int iterations) {
int totalChunks = (2 * testRadius + 1) * (2 * testRadius + 1) * iterations;
StringBuilder sb = new StringBuilder();
long total = 0;
for (int i = 0; i < timings.length; i++) {
total += timings[i];
String name = BuiltInRegistries.CHUNK_STATUS.getKey(statuses.get(i)).getPath();
sb.append(String.format(" %-22s %8.1f ms (%6.2f ms/chunk)\n",
name, timings[i] / 1e6, timings[i] / 1e6 / totalChunks));
}
sb.append(String.format(" %-22s %8.1f ms (%6.2f ms/chunk)\n",
"TOTAL", total / 1e6, total / 1e6 / totalChunks));
return sb.toString();
}
private static class Context {
final ServerLevel level;
final ServerChunkCache chunkSource;
final ChunkPos center;
final int loadRadius;
final int loadDiameter;
final ChunkAccess[] realChunks;
final int neighborDiameter;
final int centerIndex;
final Executor executor;
final ChunkGenerator generator;
final ThreadedLevelLightEngine lightEngine;
final StructureTemplateManager templates;
final PoiManager poiManager;
final Function<ChunkAccess, CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>> noopPromotion;
private final net.minecraft.core.Registry<Biome> biomeRegistry;
Context(ServerLevel level, ChunkPos center, int testRadius) {
this.level = level;
this.chunkSource = level.getChunkSource();
this.center = center;
this.loadRadius = testRadius + REQUIRED_LOAD_RADIUS;
this.loadDiameter = 2 * loadRadius + 1;
this.neighborDiameter = 2 * REQUIRED_LOAD_RADIUS + 1;
this.centerIndex = neighborDiameter * neighborDiameter / 2;
this.executor = MoreExecutors.directExecutor();
this.generator = chunkSource.getGenerator();
this.lightEngine = chunkSource.getLightEngine();
this.templates = level.getStructureManager();
this.poiManager = chunkSource.getPoiManager();
this.noopPromotion = chunk -> CompletableFuture.completedFuture(Either.left(chunk));
this.biomeRegistry = level.registryAccess().registryOrThrow(Registries.BIOME);
chunkSource.addRegionTicket(BENCHMARK_TICKET, center, loadRadius, Unit.INSTANCE);
realChunks = new ChunkAccess[loadDiameter * loadDiameter];
for (int dz = -loadRadius; dz <= loadRadius; dz++) {
for (int dx = -loadRadius; dx <= loadRadius; dx++) {
LevelChunk real = level.getChunk(center.x + dx, center.z + dz);
realChunks[(dz + loadRadius) * loadDiameter + (dx + loadRadius)] =
new ImposterProtoChunk(real, false);
}
}
}
ProtoChunk newProtoChunk(ChunkPos pos) {
return new ProtoChunk(pos, UpgradeData.EMPTY, level, biomeRegistry, null);
}
ChunkAccess[] buildNeighborArray(ChunkPos testPos) {
int count = neighborDiameter * neighborDiameter;
ChunkAccess[] array = new ChunkAccess[count];
int baseX = (testPos.x - REQUIRED_LOAD_RADIUS) - (center.x - loadRadius);
int baseZ = (testPos.z - REQUIRED_LOAD_RADIUS) - (center.z - loadRadius);
for (int dz = 0; dz < neighborDiameter; dz++) {
System.arraycopy(realChunks, (baseZ + dz) * loadDiameter + baseX,
array, dz * neighborDiameter, neighborDiameter);
}
return array;
}
void cleanup() {
chunkSource.removeRegionTicket(BENCHMARK_TICKET, center, loadRadius, Unit.INSTANCE);
}
}
}

View File

@ -3,14 +3,58 @@ package org.embeddedt.modernfix.command;
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.packs.resources.Resource;
import net.minecraft.server.packs.resources.ResourceManager;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.duck.IProfilingServerFunctionManager; import org.embeddedt.modernfix.duck.IProfilingServerFunctionManager;
import org.embeddedt.modernfix.structure.CachingStructureManager;
import java.io.InputStream;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static net.minecraft.commands.Commands.literal; import static net.minecraft.commands.Commands.literal;
public class ModernFixCommands { public class ModernFixCommands {
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) { public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(literal("modernfix") dispatcher.register(literal("modernfix")
.then(literal("upgradeStructures")
.requires(source -> source.hasPermission(3))
.executes(context -> {
ServerLevel level = context.getSource().getLevel();
if(level == null) {
context.getSource().sendFailure(Component.literal("Couldn't find server level"));
return 0;
}
ResourceManager manager = level.getServer().resources.resourceManager();
Map<ResourceLocation, Resource> structures = manager.listResources("structures", p -> p.getPath().endsWith(".nbt"));
int upgradedNum = 0;
Pattern pathPattern = Pattern.compile("^structures/(.*)\\.nbt$");
for(Map.Entry<ResourceLocation, Resource> entry : structures.entrySet()) {
upgradedNum++;
ResourceLocation found = entry.getKey();
Matcher matcher = pathPattern.matcher(found.getPath());
if(!matcher.matches())
continue;
ResourceLocation structureLocation = new ResourceLocation(found.getNamespace(), matcher.group(1));
try(InputStream resource = entry.getValue().open()) {
CachingStructureManager.readStructureTag(structureLocation, level.getServer().getFixerUpper(), resource);
Component msg = Component.literal("checked " + structureLocation + " (" + upgradedNum + "/" + structures.size() + ")");
context.getSource().sendSuccess(() -> msg, false);
} catch(Throwable e) {
ModernFix.LOGGER.error("Couldn't upgrade structure " + found, e);
context.getSource().sendFailure(Component.literal("error reading " + structureLocation + " (" + upgradedNum + "/" + structures.size() + ")"));
}
}
context.getSource().sendSuccess(() -> Component.literal("All structures upgraded"), false);
return 1;
}))
.then(literal("mcfunctions").requires(source -> source.hasPermission(3)) .then(literal("mcfunctions").requires(source -> source.hasPermission(3))
.executes(context -> { .executes(context -> {
ServerLevel level = context.getSource().getLevel(); ServerLevel level = context.getSource().getLevel();

View File

@ -0,0 +1,56 @@
package org.embeddedt.modernfix.common.mixin.bugfix.buffer_builder_leak;
import com.mojang.blaze3d.vertex.BufferBuilder;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.render.UnsafeBufferHelper;
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.callback.CallbackInfo;
import java.nio.ByteBuffer;
@Mixin(value = BufferBuilder.class, priority = 1500)
@ClientOnlyMixin
public class BufferBuilderMixin {
@Shadow private ByteBuffer buffer;
private static boolean leakReported = false;
private boolean mfix$shouldFree = true;
@Dynamic
@Inject(method = "flywheel$injectForRender", at = @At("RETURN"), remap = false, require = 0)
private void preventFree(CallbackInfo ci) {
mfix$shouldFree = false;
}
/**
* Ensure UnsafeBufferHelper is classloaded early, to avoid Forge's event transformer showing an error in the log.
*/
@Inject(method = "<clinit>", at = @At(value = "RETURN"))
private static void initUnsafeBufferHelper(CallbackInfo ci) {
UnsafeBufferHelper.init();
}
@Override
protected void finalize() throws Throwable {
try {
ByteBuffer buf = buffer;
// can be null if a mod already tried to free the buffer
if(buf != null && mfix$shouldFree) {
if(!leakReported) {
leakReported = true;
ModernFix.LOGGER.warn("One or more BufferBuilders have been leaked, ModernFix will attempt to correct this.");
}
UnsafeBufferHelper.free(buf);
buffer = null;
}
} finally {
super.finalize();
}
}
}

View File

@ -0,0 +1,27 @@
package org.embeddedt.modernfix.common.mixin.bugfix.buffer_builder_leak;
import com.mojang.blaze3d.vertex.BufferBuilder;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import net.minecraft.client.renderer.RenderBuffers;
import net.minecraft.client.renderer.RenderType;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
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.CallbackInfo;
@Mixin(RenderBuffers.class)
@ClientOnlyMixin
public class RenderBuffersMixin {
/**
* @author embeddedt
* @reason put() may be called for multiple instances of the same render type (e.g. signSheet and hangingSignSheet
* in 1.20.1). This leaks the previous BufferBuilder if one is already in the map.
*/
@Inject(method = "put", at = @At("HEAD"), cancellable = true)
private static void mfix$preventBufferLeak(Object2ObjectLinkedOpenHashMap<RenderType, BufferBuilder> mapBuilders, RenderType renderType, CallbackInfo ci) {
if (mapBuilders.containsKey(renderType)) {
ci.cancel();
}
}
}

View File

@ -1,28 +1,50 @@
package org.embeddedt.modernfix.common.mixin.bugfix.chunk_deadlock; package org.embeddedt.modernfix.common.mixin.bugfix.chunk_deadlock;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local; import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.datafixers.util.Either;
import net.minecraft.CrashReport; import net.minecraft.CrashReport;
import net.minecraft.ReportedException; import net.minecraft.ReportedException;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.util.thread.BlockableEventLoop;
import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatusTasks; import net.minecraftforge.fml.util.ObfuscationReflectionHelper;
import net.minecraft.world.level.chunk.status.WorldGenContext; import org.embeddedt.modernfix.ModernFix;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.lang.reflect.Field;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.function.Supplier; import java.util.function.Function;
@Mixin(ChunkStatusTasks.class) @Mixin(ChunkMap.class)
public abstract class ChunkMapLoadMixin { public abstract class ChunkMapLoadMixin {
@Shadow
@Nullable
protected abstract ChunkHolder getVisibleChunkIfPresent(long l);
@Shadow
@Final
private BlockableEventLoop<Runnable> mainThreadExecutor;
@Unique @Unique
private static final ThreadLocal<CompletableFuture<ChunkAccess>> MFIX_SURROGATE_FUTURE = new ThreadLocal<>(); private static final ThreadLocal<CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>> MFIX_SURROGATE_FUTURE = new ThreadLocal<>();
@Unique
private final ConcurrentLinkedQueue<Throwable> mfix$promotionExceptions = new ConcurrentLinkedQueue<>();
/** /**
* @author embeddedt * @author embeddedt
@ -37,24 +59,25 @@ public abstract class ChunkMapLoadMixin {
* <p>This is a cleaner version of a similar trick used in ModernFix versions for 1.16, which deferred specifically * <p>This is a cleaner version of a similar trick used in ModernFix versions for 1.16, which deferred specifically
* entity addition to happen outside the futures. * entity addition to happen outside the futures.
*/ */
@Redirect(method = "full", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;supplyAsync(Ljava/util/function/Supplier;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;", ordinal = 0)) @Redirect(method = "protoChunkToFullChunk", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenApplyAsync(Ljava/util/function/Function;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;", ordinal = 0))
private static CompletableFuture<ChunkAccess> createSurrogateFuture(Supplier<ChunkAccess> supplier, Executor executor, private CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> createSurrogateFuture(
@Local(ordinal = 0, argsOnly = true) WorldGenContext worldGenContext) { CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> previousFuture,
var surrogate = new CompletableFuture<ChunkAccess>(); Function<? super Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>, ? extends Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> fn,
Executor executor) {
var surrogate = new CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>();
// Unlike vanilla, we execute the promotion lambda in mainThreadExecutor, rather than within the context // Unlike vanilla, we execute the promotion lambda in mainThreadExecutor, rather than within the context
// of the task sorter. Doing this avoids deadlocking the sorter if a blocking chunk load is attempted // of the task sorter. Doing this avoids deadlocking the sorter if a blocking chunk load is attempted
// during chunk promotion. We still initially compose the future through the sorter's executor to stop promotion // during chunk promotion. We still initially compose the future through the sorter's executor to stop promotion
// from running earlier than it would in vanilla. // from running earlier than it would in vanilla.
var mainThreadExecutor = ((ServerChunkCacheAccessor) worldGenContext.level().getChunkSource()).mfix$getMainThreadProcessor(); previousFuture.thenComposeAsync(CompletableFuture::completedFuture, executor).thenApplyAsync(either -> {
CompletableFuture.runAsync(() -> {}, executor).thenApplyAsync($ -> {
// running on thread that executes lambda body // running on thread that executes lambda body
MFIX_SURROGATE_FUTURE.set(surrogate); MFIX_SURROGATE_FUTURE.set(surrogate);
try { try {
return supplier.get(); return fn.apply(either);
} finally { } finally {
MFIX_SURROGATE_FUTURE.remove(); MFIX_SURROGATE_FUTURE.remove();
} }
}, mainThreadExecutor).whenComplete((either, throwable) -> { }, this.mainThreadExecutor).whenComplete((either, throwable) -> {
if (throwable != null) { if (throwable != null) {
if (!surrogate.isDone()) { if (!surrogate.isDone()) {
surrogate.completeExceptionally(throwable); surrogate.completeExceptionally(throwable);
@ -62,8 +85,7 @@ public abstract class ChunkMapLoadMixin {
// The chunk has already become visible at FULL status, so we // The chunk has already become visible at FULL status, so we
// track the exception ourselves and manually rethrow it at the right point // track the exception ourselves and manually rethrow it at the right point
// to trigger a server crash // to trigger a server crash
var exc = new ReportedException(CrashReport.forThrowable(throwable, "Exception during promotion of chunk to FULL status")); this.mfix$promotionExceptions.add(throwable);
MinecraftServer.setFatalException(exc);
} }
} else { } else {
surrogate.complete(either); surrogate.complete(either);
@ -78,11 +100,64 @@ public abstract class ChunkMapLoadMixin {
* @reason Complete the surrogate future as soon as basic promotion is done, and before we start loading entities * @reason Complete the surrogate future as soon as basic promotion is done, and before we start loading entities
* & block entities. This allows EntityJoinLevelEvent to read the current chunk. * & block entities. This allows EntityJoinLevelEvent to read the current chunk.
*/ */
@Inject(method = "lambda$full$2", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunk;runPostLoad()V")) @Inject(method = "lambda$protoChunkToFullChunk$34", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunk;runPostLoad()V"))
private static void completeSurrogateFuture(CallbackInfoReturnable<ChunkAccess> cir, @Local(ordinal = 0) LevelChunk levelChunk) { private void completeSurrogateFuture(ChunkHolder holder, ChunkAccess p_214856_, CallbackInfoReturnable<ChunkAccess> cir,
@Local(ordinal = 0) LevelChunk levelChunk) {
var future = MFIX_SURROGATE_FUTURE.get(); var future = MFIX_SURROGATE_FUTURE.get();
if (future != null) { if (future != null) {
future.complete(levelChunk); future.complete(Either.left(levelChunk));
}
}
@Inject(method = "tick()V", at = @At("HEAD"))
private void reportDeferredPromotionException(CallbackInfo ci) {
var throwable = this.mfix$promotionExceptions.poll();
if (throwable == null) {
return;
}
if (throwable instanceof ReportedException e) {
throw e;
} else {
throw new ReportedException(CrashReport.forThrowable(throwable, "Exception during promotion of chunk to FULL status"));
}
}
// we also preserve the legacy currentlyLoading field to keep Forge parity
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 = "lambda$protoChunkToFullChunk$34", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunk;runPostLoad()V"))
private void setCurrentLoadingThenPostLoad(LevelChunk chunk, Operation<Void> 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);
} }
} }
} }

View File

@ -1,7 +1,6 @@
package org.embeddedt.modernfix.common.mixin.bugfix.chunk_deadlock; package org.embeddedt.modernfix.common.mixin.bugfix.chunk_deadlock;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import net.minecraft.core.Holder;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.gameevent.GameEvent; import net.minecraft.world.level.gameevent.GameEvent;
@ -18,8 +17,8 @@ public class EntityMixin {
* tries to raytrace blocks. To fix this, we skip firing the sculk event if the chunk the entity is within is not * tries to raytrace blocks. To fix this, we skip firing the sculk event if the chunk the entity is within is not
* loaded. * loaded.
*/ */
@WrapWithCondition(method = "addPassenger", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;gameEvent(Lnet/minecraft/core/Holder;Lnet/minecraft/world/entity/Entity;)V")) @WrapWithCondition(method = "addPassenger", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;gameEvent(Lnet/minecraft/world/level/gameevent/GameEvent;Lnet/minecraft/world/entity/Entity;)V"))
private boolean onlyAddIfSelfChunkLoaded(Entity instance, Holder<GameEvent> gameEvent, Entity entity) { private boolean onlyAddIfSelfChunkLoaded(Entity instance, GameEvent event, Entity entity) {
var chunkPos = instance.chunkPosition(); var chunkPos = instance.chunkPosition();
if (instance.level() instanceof ServerLevel serverLevel && serverLevel.getChunkSource().getChunkNow(chunkPos.x, chunkPos.z) == null) { if (instance.level() instanceof ServerLevel serverLevel && serverLevel.getChunkSource().getChunkNow(chunkPos.x, chunkPos.z) == null) {
ModernFix.LOGGER.warn("Skipped emitting ENTITY_MOUNT game event for entity {} as it would cause deadlock", instance.toString()); ModernFix.LOGGER.warn("Skipped emitting ENTITY_MOUNT game event for entity {} as it would cause deadlock", instance.toString());

View File

@ -1,11 +0,0 @@
package org.embeddedt.modernfix.common.mixin.bugfix.chunk_deadlock;
import net.minecraft.server.level.ServerChunkCache;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(ServerChunkCache.class)
public interface ServerChunkCacheAccessor {
@Accessor("mainThreadProcessor")
ServerChunkCache.MainThreadExecutor mfix$getMainThreadProcessor();
}

View File

@ -0,0 +1,57 @@
package org.embeddedt.modernfix.common.mixin.bugfix.chunk_deadlock;
import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ServerChunkCache;
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.chunk.LevelChunk;
import net.minecraftforge.fml.util.ObfuscationReflectionHelper;
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 org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
@Mixin(value = ServerChunkCache.class, priority = 1100)
public abstract class ServerChunkCache_CurrentLoadingMixin {
@Shadow @Nullable protected abstract ChunkHolder getVisibleChunkIfPresent(long l);
private static final MethodHandle CURRENTLY_LOADING;
static {
try {
Field currentlyLoadingField = ObfuscationReflectionHelper.findField(ChunkHolder.class, "currentlyLoading");
currentlyLoadingField.setAccessible(true);
CURRENTLY_LOADING = MethodHandles.lookup().unreflectGetter(currentlyLoadingField);
} catch(Exception e) {
throw new RuntimeException("Failed to get currentlyLoading field", e);
}
}
/**
* Check the currentlyLoading field before going to the future chain, as was done in 1.16. In 1.18 upstream seems
* to have only applied this to getChunkNow().
*/
@Inject(method = "getChunk", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerChunkCache;getChunkFutureMainThread(IILnet/minecraft/world/level/chunk/ChunkStatus;Z)Ljava/util/concurrent/CompletableFuture;"), cancellable = true, require = 0)
private void checkCurrentlyLoading(int chunkX, int chunkZ, ChunkStatus requiredStatus, boolean load, CallbackInfoReturnable<ChunkAccess> cir) {
long i = ChunkPos.asLong(chunkX, chunkZ);
ChunkHolder holder = this.getVisibleChunkIfPresent(i);
if(holder != null) {
LevelChunk c;
try {
c = (LevelChunk)CURRENTLY_LOADING.invokeExact(holder);
} catch(Throwable e) {
e.printStackTrace();
c = null;
}
if(c != null)
cir.setReturnValue(c);
}
}
}

View File

@ -0,0 +1,31 @@
package org.embeddedt.modernfix.common.mixin.bugfix.concurrency;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.tags.TagKey;
import net.minecraftforge.registries.tags.ITag;
import org.jetbrains.annotations.NotNull;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import java.util.Map;
@Mixin(targets = {"net/minecraftforge/registries/ForgeRegistryTagManager"})
public class ForgeRegistryTagManagerMixin<V> {
@Shadow private volatile Map<TagKey<V>, ITag<V>> tags;
/**
* @author embeddedt (issue found by Uncandango)
* @reason vanilla does not use the correct double-checked locking paradigm, which leads to race conditions
*/
@WrapMethod(method = "getTag", remap = false)
private ITag<V> getTagSafe(@NotNull TagKey<V> name, Operation<ITag<V>> original) {
ITag<V> tag = this.tags.get(name);
if (tag == null) {
synchronized (this) {
tag = original.call(name);
}
}
return tag;
}
}

View File

@ -0,0 +1,40 @@
package org.embeddedt.modernfix.common.mixin.bugfix.concurrency;
import net.minecraft.core.HolderSet;
import net.minecraft.core.MappedRegistry;
import net.minecraft.tags.TagKey;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import java.util.IdentityHashMap;
import java.util.Map;
@Mixin(value = MappedRegistry.class, priority = 500)
public abstract class MappedRegistryMixin<T> {
@Shadow private volatile Map<TagKey<T>, HolderSet.Named<T>> tags;
@Shadow protected abstract HolderSet.Named<T> createTag(TagKey<T> key);
/**
* @author embeddedt (issue found by Uncandango)
* @reason vanilla does not use the correct double-checked locking paradigm, which leads to race conditions
*/
@Overwrite
public HolderSet.Named<T> getOrCreateTag(TagKey<T> key) {
HolderSet.Named<T> named = this.tags.get(key);
if (named == null) {
// synchronize and check again - this is the bugfix
synchronized (this) {
named = this.tags.get(key);
if (named == null) {
named = this.createTag(key);
Map<TagKey<T>, HolderSet.Named<T>> map = new IdentityHashMap<>(this.tags);
map.put(key, named);
this.tags = map;
}
}
}
return named;
}
}

View File

@ -0,0 +1,39 @@
package org.embeddedt.modernfix.common.mixin.bugfix.concurrency;
import net.minecraft.core.HolderSet;
import net.minecraft.tags.TagKey;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import java.util.IdentityHashMap;
import java.util.Map;
@Mixin(targets = {"net/minecraftforge/registries/NamespacedWrapper"}, priority = 500)
public abstract class NamespacedWrapperMixin<T> {
@Shadow(aliases = {"tags"}) private volatile Map<TagKey<T>, HolderSet.Named<T>> tags;
@Shadow(aliases = {"createTag"}) protected abstract HolderSet.Named<T> m_211067_(TagKey<T> key);
/**
* @author embeddedt (issue found by Uncandango)
* @reason vanilla does not use the correct double-checked locking paradigm, which leads to race conditions
*/
@Overwrite
public HolderSet.Named<T> getOrCreateTag(TagKey<T> key) {
HolderSet.Named<T> named = this.tags.get(key);
if (named == null) {
// synchronize and check again - this is the bugfix
synchronized (this) {
named = this.tags.get(key);
if (named == null) {
named = this.m_211067_(key);
Map<TagKey<T>, HolderSet.Named<T>> map = new IdentityHashMap<>(this.tags);
map.put(key, named);
this.tags = map;
}
}
}
return named;
}
}

View File

@ -6,9 +6,13 @@ import net.minecraft.client.Minecraft;
import net.minecraft.server.packs.PackType; import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.resources.PreparableReloadListener; import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.server.packs.resources.ReloadableResourceManager; import net.minecraft.server.packs.resources.ReloadableResourceManager;
import net.minecraftforge.fml.ModContainer;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.ModLoadingStage;
import net.minecraftforge.registries.ForgeRegistries;
import org.embeddedt.modernfix.ModernFix; import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin; import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.neoforge.init.ModernFixForge; import org.embeddedt.modernfix.forge.init.ModernFixForge;
import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;

View File

@ -2,9 +2,9 @@ package org.embeddedt.modernfix.common.mixin.bugfix.entity_pose_stack;
import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.renderer.entity.LivingEntityRenderer; import net.minecraft.client.renderer.entity.LivingEntityRenderer;
import net.neoforged.neoforge.client.event.RenderLivingEvent; import net.minecraftforge.client.event.RenderLivingEvent;
import net.neoforged.bus.api.Event; import net.minecraftforge.eventbus.api.Event;
import net.neoforged.bus.api.IEventBus; import net.minecraftforge.eventbus.api.IEventBus;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin; import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
@ -13,17 +13,18 @@ import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(LivingEntityRenderer.class) @Mixin(LivingEntityRenderer.class)
@ClientOnlyMixin @ClientOnlyMixin
public class LivingEntityRendererMixin { public class LivingEntityRendererMixin {
@Redirect(method = "render(Lnet/minecraft/world/entity/LivingEntity;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V", at = @At(value = "INVOKE", target = "Lnet/neoforged/bus/api/IEventBus;post(Lnet/neoforged/bus/api/Event;)Lnet/neoforged/bus/api/Event;", ordinal = 0)) @Redirect(method = "render(Lnet/minecraft/world/entity/LivingEntity;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/eventbus/api/IEventBus;post(Lnet/minecraftforge/eventbus/api/Event;)Z", ordinal = 0))
private Event fireCheckingPoseStack(IEventBus instance, Event event) { private boolean fireCheckingPoseStack(IEventBus instance, Event event) {
PoseStack stack = ((RenderLivingEvent)event).getPoseStack(); PoseStack stack = ((RenderLivingEvent)event).getPoseStack();
int size = ((PoseStackAccessor)stack).getPoseStack().size(); int size = ((PoseStackAccessor)stack).getPoseStack().size();
instance.post(event); if (instance.post(event)) {
if (((RenderLivingEvent.Pre)event).isCanceled()) {
// Pop the stack if someone pushed it in the event // Pop the stack if someone pushed it in the event
while (((PoseStackAccessor)stack).getPoseStack().size() > size) { while (((PoseStackAccessor)stack).getPoseStack().size() > size) {
stack.popPose(); stack.popPose();
} }
return true;
} else {
return false;
} }
return event;
} }
} }

View File

@ -2,9 +2,9 @@ package org.embeddedt.modernfix.common.mixin.bugfix.entity_pose_stack;
import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.renderer.entity.player.PlayerRenderer; import net.minecraft.client.renderer.entity.player.PlayerRenderer;
import net.neoforged.bus.api.Event; import net.minecraftforge.client.event.RenderPlayerEvent;
import net.neoforged.bus.api.IEventBus; import net.minecraftforge.eventbus.api.Event;
import net.neoforged.neoforge.client.event.RenderPlayerEvent; import net.minecraftforge.eventbus.api.IEventBus;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin; import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
@ -13,17 +13,18 @@ import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(PlayerRenderer.class) @Mixin(PlayerRenderer.class)
@ClientOnlyMixin @ClientOnlyMixin
public class PlayerRendererMixin { public class PlayerRendererMixin {
@Redirect(method = "render(Lnet/minecraft/client/player/AbstractClientPlayer;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V", at = @At(value = "INVOKE", target = "Lnet/neoforged/bus/api/IEventBus;post(Lnet/neoforged/bus/api/Event;)Lnet/neoforged/bus/api/Event;", ordinal = 0)) @Redirect(method = "render(Lnet/minecraft/client/player/AbstractClientPlayer;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/eventbus/api/IEventBus;post(Lnet/minecraftforge/eventbus/api/Event;)Z", ordinal = 0))
private Event fireCheckingPoseStack(IEventBus instance, Event event) { private boolean fireCheckingPoseStack(IEventBus instance, Event event) {
PoseStack stack = ((RenderPlayerEvent)event).getPoseStack(); PoseStack stack = ((RenderPlayerEvent)event).getPoseStack();
int size = ((PoseStackAccessor)stack).getPoseStack().size(); int size = ((PoseStackAccessor)stack).getPoseStack().size();
instance.post(event); if (instance.post(event)) {
if (((RenderPlayerEvent.Pre)event).isCanceled()) {
// Pop the stack if someone pushed it in the event // Pop the stack if someone pushed it in the event
while (((PoseStackAccessor)stack).getPoseStack().size() > size) { while (((PoseStackAccessor)stack).getPoseStack().size() > size) {
stack.popPose(); stack.popPose();
} }
return true;
} else {
return false;
} }
return event;
} }
} }

View File

@ -0,0 +1,36 @@
package org.embeddedt.modernfix.common.mixin.bugfix.forge_vehicle_packets;
import net.minecraft.network.protocol.game.ServerboundMoveVehiclePacket;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.minecraft.world.phys.Vec3;
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.Redirect;
@Mixin(ServerGamePacketListenerImpl.class)
public class ServerGamePacketListenerImplMixin {
@Shadow public ServerPlayer player;
@Redirect(method = "handleMoveVehicle", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;absMoveTo(DDDFF)V"), require = 0)
private void movePlayerUsingPositionRider(ServerPlayer player, double x, double y, double z, float yRot, float xRot, ServerboundMoveVehiclePacket packet) {
if(player == this.player) {
// use positionRider
Vec3 oldPos = this.player.position();
yRot = this.player.getYRot();
xRot = this.player.getXRot();
float yHeadRot = this.player.getYHeadRot();
this.player.getRootVehicle().positionRider(this.player);
// keep old rotation
this.player.setYRot(yRot);
this.player.setXRot(xRot);
this.player.setYHeadRot(yHeadRot);
// save old position
this.player.xo = oldPos.x;
this.player.yo = oldPos.y;
this.player.zo = oldPos.z;
} else
player.absMoveTo(x, y, z, yRot, xRot);
}
}

View File

@ -0,0 +1,55 @@
package org.embeddedt.modernfix.common.mixin.bugfix.model_data_manager_cme;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.ChunkPos;
import net.minecraftforge.client.model.data.ModelDataManager;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
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.ModifyArg;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
/**
* Fix several concurrency issues in the default ModelDataManager.
*/
@Mixin(ModelDataManager.class)
@ClientOnlyMixin
public abstract class ModelDataManagerMixin {
@Shadow protected abstract void refreshAt(ChunkPos chunk);
@Shadow @Final private Map<ChunkPos, Set<BlockPos>> needModelDataRefresh;
/**
* Make the set of positions to refresh a real concurrent hash set rather than relying on synchronizedSet,
* because the returned iterator won't be thread-safe otherwise. See https://github.com/AppliedEnergistics/Applied-Energistics-2/issues/7511
*/
@ModifyArg(method = "requestRefresh", at = @At(value = "INVOKE", target = "Ljava/util/Map;computeIfAbsent(Ljava/lang/Object;Ljava/util/function/Function;)Ljava/lang/Object;", ordinal = 0), index = 1, remap = false)
private Function<ChunkPos, Set<BlockPos>> changeTypeOfSetUsed(Function<ChunkPos, Set<BlockPos>> mappingFunction) {
return pos -> ConcurrentHashMap.newKeySet();
}
@Redirect(method = "getAt(Lnet/minecraft/world/level/ChunkPos;)Ljava/util/Map;", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/client/model/data/ModelDataManager;refreshAt(Lnet/minecraft/world/level/ChunkPos;)V"), remap = false)
private void onlyRefreshOnMainThread(ModelDataManager instance, ChunkPos pos) {
// Only refresh model data on the main thread. This prevents calling getBlockEntity from worker threads
// which could cause weird CMEs or other behavior.
// Avoid the loop if no model data needs to be refreshed, to prevent unnecessary allocation.
if(Minecraft.getInstance().isSameThread() && !needModelDataRefresh.isEmpty()) {
// Refresh the given chunk, and all its neighbors. This is less efficient than the default code
// but we have no choice since we need to not do refreshing on workers, and blocks might
// try to access model data in neighboring chunks.
for(int x = -1; x <= 1; x++) {
for(int z = -1; z <= 1; z++) {
refreshAt(new ChunkPos(pos.x + x, pos.z + z));
}
}
}
}
}

View File

@ -0,0 +1,52 @@
package org.embeddedt.modernfix.common.mixin.bugfix.paper_chunk_patches;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.mojang.datafixers.util.Either;
import net.minecraft.server.level.*;
import net.minecraft.util.thread.BlockableEventLoop;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkStatus;
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.ModifyArg;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
@Mixin(ChunkMap.class)
public abstract class ChunkMapMixin {
@Shadow @Final private BlockableEventLoop<Runnable> mainThreadExecutor;
/* 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;
}
/**
* @author embeddedt
* @reason 1.17+ uses getNow to check if the parent future is ready, and calls scheduleChunkGeneration as soon as
* it is found to not be ready. In the latter scenario, a massive number of extra CompletableFutures are allocated
* even if they are not actually necessary if the future is waited for. To prevent this, if the parent future
* is not done, we wait for it to complete and then retry schedule(). This will either detect an adequate
* status and return a loading future, or re-enter this injector with the parent future completed, in which case
* we proceed to schedule generation as originally requested.
*/
@WrapOperation(method = "schedule", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ChunkMap;scheduleChunkGeneration(Lnet/minecraft/server/level/ChunkHolder;Lnet/minecraft/world/level/chunk/ChunkStatus;)Ljava/util/concurrent/CompletableFuture;"))
private CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> mfix$avoidSchedulingGenerationPrematurely(ChunkMap map, ChunkHolder holder, ChunkStatus status, Operation<CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>> original) {
if (!status.hasLoadDependencies()) {
var parentFuture = holder.getOrScheduleFuture(status.getParent(), map);
if (!parentFuture.isDone()) {
return parentFuture.thenComposeAsync(
either -> map.schedule(holder, status),
this.mainThreadExecutor
);
}
}
return original.call(map, holder, status);
}
}

View File

@ -1,9 +1,15 @@
package org.embeddedt.modernfix.common.mixin.bugfix.recipe_book_type_desync; package org.embeddedt.modernfix.common.mixin.bugfix.recipe_book_type_desync;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.stats.RecipeBookSettings; import net.minecraft.stats.RecipeBookSettings;
import net.minecraft.world.inventory.RecipeBookType; import net.minecraft.world.inventory.RecipeBookType;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin; import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.forge.packet.NetworkUtils;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
@ -30,14 +36,12 @@ public class RecipeBookSettingsMixin {
} }
mfix$maxVanillaOrdinal = ord; mfix$maxVanillaOrdinal = ord;
} }
/*
@Redirect(method = "read(Lnet/minecraft/network/FriendlyByteBuf;)Lnet/minecraft/stats/RecipeBookSettings;", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/FriendlyByteBuf;readBoolean()Z")) @Redirect(method = "read(Lnet/minecraft/network/FriendlyByteBuf;)Lnet/minecraft/stats/RecipeBookSettings;", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/FriendlyByteBuf;readBoolean()Z"))
private static boolean useDefaultBooleanIfVanilla(FriendlyByteBuf buf, @Local(ordinal = 0) RecipeBookType type) { private static boolean useDefaultBooleanIfVanilla(FriendlyByteBuf buf, @Local(ordinal = 0) RecipeBookType type) {
if(type.ordinal() >= (mfix$maxVanillaOrdinal + 1)) { if(type.ordinal() >= (mfix$maxVanillaOrdinal + 1) && NetworkUtils.isCurrentlyVanilla) {
ModernFix.LOGGER.warn("Not reading recipe book data for type '{}' as we are using vanilla connection", type.name()); ModernFix.LOGGER.warn("Not reading recipe book data for type '{}' as we are using vanilla connection", type.name());
return false; // skip actually reading buffer return false; // skip actually reading buffer
} }
return buf.readBoolean(); return buf.readBoolean();
} }
*/
} }

View File

@ -0,0 +1,27 @@
package org.embeddedt.modernfix.common.mixin.bugfix.registry_ops_cme;
import net.minecraft.core.Registry;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
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.Optional;
import java.util.concurrent.ConcurrentHashMap;
@Mixin(targets = {"net/minecraft/resources/RegistryOps$1"})
public class RegistryOpsMemoizedMixin {
@Shadow @Final @Mutable
private Map<ResourceKey<? extends Registry<?>>, Optional<? extends RegistryOps.RegistryInfo<?>>> lookups;
@Inject(method = "<init>", at = @At("RETURN"))
private void useConcurrentMap(RegistryOps.RegistryInfoLookup registryInfoLookup, CallbackInfo ci) {
this.lookups = new ConcurrentHashMap<>(this.lookups);
}
}

View File

@ -0,0 +1,14 @@
package org.embeddedt.modernfix.common.mixin.bugfix.removed_dimensions;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
@Mixin(LevelStorageSource.class)
public class LevelStorageSourceMixin {
@ModifyArg(method = "*", at = @At(value = "INVOKE", target = "Lcom/mojang/serialization/DataResult;getOrThrow(ZLjava/util/function/Consumer;)Ljava/lang/Object;", ordinal = 0), index = 0)
private static boolean alwaysAllowPartialDimensions(boolean flag) {
return true;
}
}

View File

@ -24,7 +24,7 @@ public class MinecraftMixin {
/** /**
* To mitigate the effect of leaked client worlds, clear most of the data structures that waste memory. * To mitigate the effect of leaked client worlds, clear most of the data structures that waste memory.
*/ */
@Inject(method = "disconnect(Lnet/minecraft/client/gui/screens/Screen;Z)V", at = @At(value = "FIELD", opcode = Opcodes.PUTFIELD, target = "Lnet/minecraft/client/Minecraft;level:Lnet/minecraft/client/multiplayer/ClientLevel;")) @Inject(method = "clearLevel(Lnet/minecraft/client/gui/screens/Screen;)V", at = @At(value = "FIELD", opcode = Opcodes.PUTFIELD, target = "Lnet/minecraft/client/Minecraft;level:Lnet/minecraft/client/multiplayer/ClientLevel;"))
private void clearLevelDataForLeaks(CallbackInfo ci) { private void clearLevelDataForLeaks(CallbackInfo ci) {
if(this.level != null) { if(this.level != null) {
try { try {

View File

@ -1,8 +1,10 @@
package org.embeddedt.modernfix.common.mixin.core; package org.embeddedt.modernfix.common.mixin.core;
import net.minecraft.server.Bootstrap; import net.minecraft.server.Bootstrap;
import net.minecraftforge.network.NetworkConstants;
import org.embeddedt.modernfix.forge.classloading.ManifestCompactor; import org.embeddedt.modernfix.forge.classloading.ManifestCompactor;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.embeddedt.modernfix.forge.load.ModWorkManagerQueue;
import org.embeddedt.modernfix.util.TimeFormatter; import org.embeddedt.modernfix.util.TimeFormatter;
import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
@ -23,7 +25,14 @@ public class BootstrapMixin {
private static void doModernFixBootstrap(CallbackInfo ci) { private static void doModernFixBootstrap(CallbackInfo ci) {
if(!isBootstrapped) { if(!isBootstrapped) {
LOGGER.info("ModernFix reached bootstrap stage ({} after launch)", TimeFormatter.formatNanos(ManagementFactory.getRuntimeMXBean().getUptime() * 1000L * 1000L)); LOGGER.info("ModernFix reached bootstrap stage ({} after launch)", TimeFormatter.formatNanos(ManagementFactory.getRuntimeMXBean().getUptime() * 1000L * 1000L));
ModWorkManagerQueue.replace();
ManifestCompactor.compactManifests(); ManifestCompactor.compactManifests();
} }
} }
/* for https://github.com/MinecraftForge/MinecraftForge/issues/9505 */
@Inject(method = "bootStrap", at = @At("RETURN"))
private static void doClassloadHack(CallbackInfo ci) {
NetworkConstants.init();
}
} }

View File

@ -0,0 +1,31 @@
package org.embeddedt.modernfix.common.mixin.core;
import net.minecraftforge.forgespi.language.IModInfo;
import net.minecraftforge.logging.CrashReportAnalyser;
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;
import java.util.Map;
@Mixin(CrashReportAnalyser.class)
public class CrashReportAnalyserMixin {
@Shadow @Final private static Map<IModInfo, String[]> SUSPECTED_MODS;
/**
* @author embeddedt
* @reason Remove ModernFix from the list of suspected mods when a crash happens. Otherwise, we get blamed
* for "registry object not present" crashes if users don't interpret the crash before reporting
* it.
*
* It seems unlikely ModernFix will simultaneously cause a crash while it's not obvious it caused it.
*/
@Inject(method = "buildSuspectedModsSection", at = @At("HEAD"), require = 0, remap = false)
private static void removeOurselvesFromSuspectedMods(StringBuilder stringBuilder, CallbackInfo ci) {
SUSPECTED_MODS.keySet().removeIf(iModInfo -> iModInfo.getModId().equals("modernfix"));
}
}

View File

@ -1,7 +1,7 @@
package org.embeddedt.modernfix.common.mixin.core; package org.embeddedt.modernfix.common.mixin.core;
import net.neoforged.neoforge.registries.GameData; import net.minecraftforge.registries.GameData;
import org.embeddedt.modernfix.neoforge.init.ModernFixForge; import org.embeddedt.modernfix.forge.init.ModernFixForge;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;

View File

@ -4,7 +4,7 @@ import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.Util; import net.minecraft.Util;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import org.embeddedt.modernfix.duck.ITimeTrackingServer; import org.embeddedt.modernfix.duck.ITimeTrackingServer;
import org.embeddedt.modernfix.neoforge.load.MinecraftServerReloadTracker; import org.embeddedt.modernfix.forge.load.MinecraftServerReloadTracker;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;

View File

@ -0,0 +1,22 @@
package org.embeddedt.modernfix.common.mixin.core;
import net.minecraft.network.Connection;
import net.minecraftforge.network.NetworkHooks;
import org.embeddedt.modernfix.forge.packet.NetworkUtils;
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(NetworkHooks.class)
public abstract class NetworkHooksMixin {
@Shadow public static boolean isVanillaConnection(Connection manager) {
throw new AssertionError();
}
@Inject(method = "handleClientLoginSuccess", at = @At("RETURN"), remap = false)
private static void setVanillaGlobalFlag(Connection manager, CallbackInfo ci) {
NetworkUtils.isCurrentlyVanilla = isVanillaConnection(manager);
}
}

View File

@ -3,7 +3,7 @@ package org.embeddedt.modernfix.common.mixin.core;
import com.llamalad7.mixinextras.injector.ModifyReturnValue; import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import com.llamalad7.mixinextras.sugar.Local; import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.server.WorldLoader; import net.minecraft.server.WorldLoader;
import org.embeddedt.modernfix.neoforge.load.MinecraftServerReloadTracker; import org.embeddedt.modernfix.forge.load.MinecraftServerReloadTracker;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;

View File

@ -0,0 +1,16 @@
package org.embeddedt.modernfix.common.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", remap = false))
private static void noDump(ForgeRegistry<?> reg, ResourceLocation id) {
}
}

View File

@ -1,9 +1,9 @@
package org.embeddedt.modernfix.common.mixin.feature.branding; package org.embeddedt.modernfix.common.mixin.feature.branding;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import net.neoforged.fml.ModContainer; import net.minecraftforge.internal.BrandingControl;
import net.neoforged.fml.ModList; import net.minecraftforge.fml.ModContainer;
import net.neoforged.neoforge.internal.BrandingControl; import net.minecraftforge.fml.ModList;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
@ -14,7 +14,7 @@ import java.util.Optional;
@Mixin(value = BrandingControl.class, remap = false, priority = 1100) @Mixin(value = BrandingControl.class, remap = false, priority = 1100)
public class BrandingControlMixin { public class BrandingControlMixin {
@Inject(method = "computeBranding", at = @At(value = "INVOKE", target = "Lnet/neoforged/fml/ModList;get()Lnet/neoforged/fml/ModList;"), locals = LocalCapture.CAPTURE_FAILHARD, require = 0) @Inject(method = "computeBranding", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/fml/ModList;get()Lnet/minecraftforge/fml/ModList;"), locals = LocalCapture.CAPTURE_FAILHARD, require = 0)
private static void addModernFixBranding(CallbackInfo ci, ImmutableList.Builder<String> builder) { private static void addModernFixBranding(CallbackInfo ci, ImmutableList.Builder<String> builder) {
Optional<? extends ModContainer> mfContainer = ModList.get().getModContainerById("modernfix"); Optional<? extends ModContainer> mfContainer = ModList.get().getModContainerById("modernfix");
if(mfContainer.isPresent()) if(mfContainer.isPresent())

View File

@ -1,6 +1,6 @@
package org.embeddedt.modernfix.common.mixin.feature.cause_lag_by_disabling_threads; package org.embeddedt.modernfix.common.mixin.feature.cause_lag_by_disabling_threads;
import net.minecraft.client.renderer.chunk.SectionRenderDispatcher; import net.minecraft.client.renderer.chunk.ChunkRenderDispatcher;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin; import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
@ -11,7 +11,7 @@ import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Mixin(SectionRenderDispatcher.class) @Mixin(ChunkRenderDispatcher.class)
@ClientOnlyMixin @ClientOnlyMixin
public class ChunkRenderDispatcherMixin { public class ChunkRenderDispatcherMixin {
private static final Executor MFIX_CHUNK_BUILD_EXECUTOR = new ThreadPoolExecutor(1, computeNumThreads(), 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); private static final Executor MFIX_CHUNK_BUILD_EXECUTOR = new ThreadPoolExecutor(1, computeNumThreads(), 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

View File

@ -5,8 +5,7 @@ import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.Share; import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalRef; import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandFunction;
import net.minecraft.commands.functions.CommandFunction;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.ServerFunctionManager; import net.minecraft.server.ServerFunctionManager;
import org.embeddedt.modernfix.duck.IProfilingServerFunctionManager; import org.embeddedt.modernfix.duck.IProfilingServerFunctionManager;
@ -30,22 +29,22 @@ public class ServerFunctionManagerMixin implements IProfilingServerFunctionManag
private final Map<ResourceLocation, Stopwatch> mfix$functionWatches = new Object2ObjectOpenHashMap<>(); private final Map<ResourceLocation, Stopwatch> mfix$functionWatches = new Object2ObjectOpenHashMap<>();
@Inject(method = "executeTagFunctions", at = @At("HEAD")) @Inject(method = "executeTagFunctions", at = @At("HEAD"))
private void resetWatches(Collection<CommandFunction<CommandSourceStack>> functionObjects, ResourceLocation identifier, CallbackInfo ci) { private void resetWatches(Collection<CommandFunction> functionObjects, ResourceLocation identifier, CallbackInfo ci) {
mfix$functionWatches.values().forEach(Stopwatch::reset); mfix$functionWatches.values().forEach(Stopwatch::reset);
} }
@Inject(method = "executeTagFunctions", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/ServerFunctionManager;execute(Lnet/minecraft/commands/functions/CommandFunction;Lnet/minecraft/commands/CommandSourceStack;)V")) @Inject(method = "executeTagFunctions", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/ServerFunctionManager;execute(Lnet/minecraft/commands/CommandFunction;Lnet/minecraft/commands/CommandSourceStack;)I"))
private void startWatch(Collection<CommandFunction<CommandSourceStack>> functionObjects, ResourceLocation identifier, CallbackInfo ci, @Local(ordinal = 0) CommandFunction<CommandSourceStack> function, @Share("stopwatch") LocalRef<Stopwatch> watchRef) { private void startWatch(Collection<CommandFunction> functionObjects, ResourceLocation identifier, CallbackInfo ci, @Local(ordinal = 0) CommandFunction function, @Share("stopwatch") LocalRef<Stopwatch> watchRef) {
watchRef.set(null); watchRef.set(null);
if (identifier == TICK_FUNCTION_TAG) { if (identifier == TICK_FUNCTION_TAG) {
var watch = mfix$functionWatches.computeIfAbsent(function.id(), i -> Stopwatch.createUnstarted()); var watch = mfix$functionWatches.computeIfAbsent(function.getId(), i -> Stopwatch.createUnstarted());
watch.start(); watch.start();
watchRef.set(watch); watchRef.set(watch);
} }
} }
@Inject(method = "executeTagFunctions", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/ServerFunctionManager;execute(Lnet/minecraft/commands/functions/CommandFunction;Lnet/minecraft/commands/CommandSourceStack;)V", shift = At.Shift.AFTER)) @Inject(method = "executeTagFunctions", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/ServerFunctionManager;execute(Lnet/minecraft/commands/CommandFunction;Lnet/minecraft/commands/CommandSourceStack;)I", shift = At.Shift.AFTER))
private void stopWatch(Collection<CommandFunction<CommandSourceStack>> functionObjects, ResourceLocation identifier, CallbackInfo ci, @Share("stopwatch") LocalRef<Stopwatch> watchRef) { private void stopWatch(Collection<CommandFunction> functionObjects, ResourceLocation identifier, CallbackInfo ci, @Share("stopwatch") LocalRef<Stopwatch> watchRef) {
var watch = watchRef.get(); var watch = watchRef.get();
if (watch != null && watch.isRunning()) { if (watch != null && watch.isRunning()) {
watch.stop(); watch.stop();
@ -53,7 +52,7 @@ public class ServerFunctionManagerMixin implements IProfilingServerFunctionManag
} }
@Inject(method = "executeTagFunctions", at = @At("RETURN")) @Inject(method = "executeTagFunctions", at = @At("RETURN"))
private void pruneUnusedWatches(Collection<CommandFunction<CommandSourceStack>> functionObjects, ResourceLocation identifier, CallbackInfo ci) { private void pruneUnusedWatches(Collection<CommandFunction> functionObjects, ResourceLocation identifier, CallbackInfo ci) {
mfix$functionWatches.values().removeIf(watch -> watch.elapsed().isZero()); mfix$functionWatches.values().removeIf(watch -> watch.elapsed().isZero());
} }

View File

@ -5,7 +5,7 @@ import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
@Mixin(targets = "net/neoforged/neoforge/event/AddReloadListenerEvent$WrappedStateAwareListener") @Mixin(targets = "net/minecraftforge/event/AddReloadListenerEvent$WrappedStateAwareListener")
public abstract class AddReloadListenerEventWrapperMixin implements PreparableReloadListener { public abstract class AddReloadListenerEventWrapperMixin implements PreparableReloadListener {
@Shadow @Final private PreparableReloadListener wrapped; @Shadow @Final private PreparableReloadListener wrapped;

View File

@ -1,46 +1,52 @@
package org.embeddedt.modernfix.common.mixin.feature.registry_event_progress; package org.embeddedt.modernfix.common.mixin.feature.registry_event_progress;
import net.neoforged.bus.api.Event; import net.minecraftforge.eventbus.api.Event;
import net.neoforged.bus.api.EventPriority; import net.minecraftforge.fml.ModList;
import net.neoforged.fml.ModList; import net.minecraftforge.fml.ModLoader;
import net.neoforged.fml.ModLoader; import net.minecraftforge.fml.ModLoadingContext;
import net.neoforged.fml.ModLoadingContext; import net.minecraftforge.fml.StartupMessageManager;
import net.neoforged.fml.event.IModBusEvent; import net.minecraftforge.fml.event.IModBusEvent;
import net.neoforged.fml.loading.progress.StartupNotificationManager; import net.minecraftforge.registries.GameData;
import net.neoforged.neoforge.registries.GameData; import net.minecraftforge.registries.RegisterEvent;
import net.neoforged.neoforge.registries.RegisterEvent;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin; import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.forge.util.AsyncLoadingScreen;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; 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.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(value = GameData.class, remap = false) @Mixin(value = GameData.class, remap = false)
@ClientOnlyMixin @ClientOnlyMixin
public class GameDataMixin { public class GameDataMixin {
@Redirect(method = "postRegisterEvents", at = @At(value = "INVOKE", target = "Lnet/neoforged/fml/ModLoader;postEventWrapContainerInModOrder(Lnet/neoforged/bus/api/Event;)V")) private static AsyncLoadingScreen mfix$asyncScreen;
private static <T extends Event & IModBusEvent> void postWithProgressBar(T event) {
if(ModLoader.hasErrors()) {
return;
}
RegisterEvent registryEvent = (RegisterEvent)event;
// We control phases ourselves so we can make a separate progress bar for each phase.
String registryName = registryEvent.getRegistryKey().location().toString();
for(EventPriority phase : EventPriority.values()) {
// FIXME need to use prepend rather than append for it to be visible for now
var pb = StartupNotificationManager.prependProgressBar(registryName, ModList.get().size());
try {
ModList.get().forEachModInOrder(mc -> {
ModLoadingContext.get().setActiveContainer(mc);
pb.label(pb.name() + " - " + mc.getModInfo().getDisplayName());
pb.increment();
mc.acceptEvent(phase, event);
ModLoadingContext.get().setActiveContainer(null);
});
} finally {
pb.complete();
}
}
@Inject(method = "postRegisterEvents", at = @At(value = "INVOKE", target = "Ljava/util/Set;iterator()Ljava/util/Iterator;", ordinal = 0))
private static void createAsyncScreen(CallbackInfo ci) {
mfix$asyncScreen = new AsyncLoadingScreen();
}
@Inject(method = "postRegisterEvents", at = @At(value = "INVOKE", target = "Ljava/lang/RuntimeException;getSuppressed()[Ljava/lang/Throwable;", ordinal = 0))
private static void closeAsyncScreen(CallbackInfo ci) {
mfix$asyncScreen.close();
mfix$asyncScreen = null;
}
@Redirect(method = "postRegisterEvents", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/fml/ModLoader;postEventWrapContainerInModOrder(Lnet/minecraftforge/eventbus/api/Event;)V"))
private static <T extends Event & IModBusEvent> void swapThreadAndPost(ModLoader loader, T event) {
RegisterEvent registryEvent = (RegisterEvent)event;
var pb = StartupMessageManager.addProgressBar(registryEvent.getRegistryKey().location().toString(), ModList.get().size());
try {
loader.postEventWithWrapInModOrder(event, (mc, e) -> {
ModLoadingContext.get().setActiveContainer(mc);
pb.label(pb.name() + " - " + mc.getModInfo().getDisplayName());
pb.increment();
}, (mc, e) -> {
ModLoadingContext.get().setActiveContainer(null);
});
} finally {
pb.complete();
}
} }
} }

View File

@ -1,11 +1,10 @@
package org.embeddedt.modernfix.common.mixin.perf.attribute_supplier_dedup; package org.embeddedt.modernfix.common.mixin.perf.attribute_supplier_dedup;
import net.minecraft.core.Holder;
import net.minecraft.world.entity.ai.attributes.Attribute; import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeInstance; import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier; import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
import org.embeddedt.modernfix.entity.AttributeInstanceTemplates; import org.embeddedt.modernfix.entity.AttributeInstanceTemplates;
import org.embeddedt.modernfix.neoforge.init.ModernFixForge; import org.embeddedt.modernfix.forge.init.ModernFixForge;
import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
@ -19,7 +18,7 @@ import java.util.Map;
public class AttributeSupplierBuilderMixin { public class AttributeSupplierBuilderMixin {
@Shadow @Shadow
@Final @Final
private Map<Holder<Attribute>, AttributeInstance> builder; private Map<Attribute, AttributeInstance> builder;
/** /**
* @author embeddedt * @author embeddedt

View File

@ -1,7 +1,6 @@
package org.embeddedt.modernfix.common.mixin.perf.attribute_supplier_dedup; package org.embeddedt.modernfix.common.mixin.perf.attribute_supplier_dedup;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.core.Holder;
import net.minecraft.world.entity.ai.attributes.Attribute; import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeInstance; import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier; import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
@ -20,7 +19,7 @@ public class AttributeSupplierMixin {
@Shadow @Shadow
@Final @Final
@Mutable @Mutable
private Map<Holder<Attribute>, AttributeInstance> instances; private Map<Attribute, AttributeInstance> instances;
/** /**
* @author embeddedt * @author embeddedt
@ -28,7 +27,7 @@ public class AttributeSupplierMixin {
* care about insertion order in this context * care about insertion order in this context
*/ */
@Inject(method = "<init>", at = @At("RETURN")) @Inject(method = "<init>", at = @At("RETURN"))
private void useCompactJavaMap(Map<Holder<Attribute>, AttributeInstance> instances, CallbackInfo ci) { private void useCompactJavaMap(Map<Attribute, AttributeInstance> instances, CallbackInfo ci) {
this.instances = new Object2ObjectOpenHashMap<>(this.instances); this.instances = new Object2ObjectOpenHashMap<>(this.instances);
} }
} }

View File

@ -0,0 +1,52 @@
package org.embeddedt.modernfix.common.mixin.perf.blast_search_trees;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;
import net.minecraft.client.searchtree.SearchRegistry;
import net.minecraft.world.item.ItemStack;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.platform.ModernFixPlatformHooks;
import org.embeddedt.modernfix.searchtree.RecipeBookSearchTree;
import org.embeddedt.modernfix.searchtree.SearchTreeProviderRegistry;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWErrorCallback;
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;
import java.util.List;
@Mixin(Minecraft.class)
@ClientOnlyMixin
public abstract class MinecraftMixin {
@Shadow @Final private SearchRegistry searchRegistry;
@Shadow public abstract <T> void populateSearchTree(SearchRegistry.Key<T> key, List<T> list);
@Inject(method = "createSearchTrees", at = @At("HEAD"), cancellable = true)
private void replaceSearchTrees(CallbackInfo ci) {
SearchTreeProviderRegistry.Provider provider = SearchTreeProviderRegistry.getSearchTreeProvider();
if(provider == null)
return;
ModernFix.LOGGER.info("Replacing search trees with '{}' provider", provider.getName());
SearchRegistry.TreeBuilderSupplier<ItemStack> nameSupplier = list -> provider.getSearchTree(false);
SearchRegistry.TreeBuilderSupplier<ItemStack> tagSupplier = list -> provider.getSearchTree(true);
this.searchRegistry.register(SearchRegistry.CREATIVE_NAMES, nameSupplier);
this.searchRegistry.register(SearchRegistry.CREATIVE_TAGS, tagSupplier);
this.searchRegistry.register(SearchRegistry.RECIPE_COLLECTIONS, list -> new RecipeBookSearchTree(provider.getSearchTree(false), list));
ModernFixPlatformHooks.INSTANCE.registerCreativeSearchTrees(this.searchRegistry, nameSupplier, tagSupplier, this::populateSearchTree);
// grab components for all key mappings in order to prevent them from being loaded off-thread later
// this populates the LazyLoadedValues
// we also need to suppress GLFW errors to prevent crashes if a key is missing
GLFWErrorCallback oldCb = GLFW.glfwSetErrorCallback(null);
for(KeyMapping mapping : KeyMapping.ALL.values()) {
mapping.getTranslatedKeyMessage();
}
GLFW.glfwSetErrorCallback(oldCb);
ci.cancel();
}
}

View File

@ -3,6 +3,7 @@ package org.embeddedt.modernfix.common.mixin.perf.cache_profile_texture_url;
import com.google.common.cache.Cache; import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheBuilder;
import com.mojang.authlib.minecraft.MinecraftProfileTexture; import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import net.minecraft.client.resources.SkinManager;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin; import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.Unique;
@ -12,7 +13,7 @@ import org.spongepowered.asm.mixin.injection.Redirect;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Mixin(targets = {"net/minecraft/client/resources/SkinManager$TextureCache" }) @Mixin(SkinManager.class)
@ClientOnlyMixin @ClientOnlyMixin
public class SkinManagerMixin { public class SkinManagerMixin {
@Unique @Unique
@ -21,7 +22,7 @@ public class SkinManagerMixin {
.concurrencyLevel(1) .concurrencyLevel(1)
.build(); .build();
@Redirect(method = { "getOrLoad", "registerTexture" }, @Redirect(method = "registerTexture(Lcom/mojang/authlib/minecraft/MinecraftProfileTexture;Lcom/mojang/authlib/minecraft/MinecraftProfileTexture$Type;Lnet/minecraft/client/resources/SkinManager$SkinTextureCallback;)Lnet/minecraft/resources/ResourceLocation;",
at = @At(value = "INVOKE", target = "Lcom/mojang/authlib/minecraft/MinecraftProfileTexture;getHash()Ljava/lang/String;", remap = false)) at = @At(value = "INVOKE", target = "Lcom/mojang/authlib/minecraft/MinecraftProfileTexture;getHash()Ljava/lang/String;", remap = false))
private String useCachedHash(MinecraftProfileTexture texture) { private String useCachedHash(MinecraftProfileTexture texture) {
// avoid lambda allocation for common case // avoid lambda allocation for common case

View File

@ -106,7 +106,7 @@ public class ChunkGeneratorMixin implements IChunkGenerator {
private String mfix$makeCacheKey(ConcentricRingsStructurePlacement placement) { private String mfix$makeCacheKey(ConcentricRingsStructurePlacement placement) {
RegistryOps<Tag> ops = RegistryOps.create(NbtOps.INSTANCE, this.mfix$server.registryAccess()); RegistryOps<Tag> ops = RegistryOps.create(NbtOps.INSTANCE, this.mfix$server.registryAccess());
String placementKey = ConcentricRingsStructurePlacement.CODEC.codec().encodeStart(ops, placement) String placementKey = ConcentricRingsStructurePlacement.CODEC.encodeStart(ops, placement)
.result().map(Tag::toString).orElse(null); .result().map(Tag::toString).orElse(null);
String biomeSourceKey = BiomeSource.CODEC.encodeStart(ops, this.biomeSource) String biomeSourceKey = BiomeSource.CODEC.encodeStart(ops, this.biomeSource)
.result().map(Tag::toString).orElse(null); .result().map(Tag::toString).orElse(null);
@ -155,7 +155,7 @@ public class ChunkGeneratorMixin implements IChunkGenerator {
return new HashMap<>(); return new HashMap<>();
} }
try { try {
CompoundTag root = NbtIo.readCompressed(file, NbtAccounter.unlimitedHeap()); CompoundTag root = NbtIo.readCompressed(file.toFile());
Map<String, List<ChunkPos>> result = new HashMap<>(); Map<String, List<ChunkPos>> result = new HashMap<>();
for (String key : root.getAllKeys()) { for (String key : root.getAllKeys()) {
if (root.contains(key, Tag.TAG_INT_ARRAY)) { if (root.contains(key, Tag.TAG_INT_ARRAY)) {
@ -190,7 +190,7 @@ public class ChunkGeneratorMixin implements IChunkGenerator {
} }
Path file = mfix$dimensionPath.resolve(CACHE_FILENAME); Path file = mfix$dimensionPath.resolve(CACHE_FILENAME);
try { try {
NbtIo.writeCompressed(root, file); NbtIo.writeCompressed(root, file.toFile());
} catch (Exception e) { } catch (Exception e) {
ModernFix.LOGGER.warn("Failed to write stronghold cache", e); ModernFix.LOGGER.warn("Failed to write stronghold cache", e);
} }

View File

@ -0,0 +1,46 @@
package org.embeddedt.modernfix.common.mixin.perf.cache_upgraded_structures;
import com.mojang.datafixers.DataFixer;
import net.minecraft.core.HolderGetter;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.structure.CachingStructureManager;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
@Mixin(StructureTemplateManager.class)
public class StructureManagerMixin {
@Shadow @Final private DataFixer fixerUpper;
@Shadow private ResourceManager resourceManager;
@Shadow @Final private HolderGetter<Block> blockLookup;
/**
* @author embeddedt
* @reason use our own manager to avoid needless DFU updates
*/
@Overwrite
private Optional<StructureTemplate> loadFromResource(ResourceLocation id) {
ResourceLocation arg = new ResourceLocation(id.getNamespace(), "structures/" + id.getPath() + ".nbt");
try(InputStream stream = this.resourceManager.open(arg)) {
return Optional.of(CachingStructureManager.readStructure(id, this.fixerUpper, stream, this.blockLookup));
} catch(FileNotFoundException e) {
return Optional.empty();
} catch(IOException e) {
ModernFix.LOGGER.error("Can't read structure", e);
return Optional.empty();
}
}
}

View File

@ -1,29 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.capability_list_compaction;
import com.llamalad7.mixinextras.sugar.Local;
import net.neoforged.neoforge.capabilities.BaseCapability;
import net.neoforged.neoforge.capabilities.CapabilityHooks;
import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent;
import org.embeddedt.modernfix.neoforge.caps.CapProviderGetter;
import org.embeddedt.modernfix.neoforge.caps.ITrackingCapEvent;
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.CallbackInfo;
@Mixin(value = CapabilityHooks.class, remap = false)
public class CapabilityHooksMixin {
// Must inject as late as possible to run after mixins that add their own capabilities
// (e.g. https://github.com/SuperMartijn642/Entangled/blob/37f2489d8badc3f52401088d8a6e25d2a63a045c/src/main/java/com/supermartijn642/entangled/mixin/neoforge/CapabilityHooksMixin.java)
@Inject(method = "init", at = @At(value = "RETURN"))
private static void deduplicateCaps(CallbackInfo ci, @Local(ordinal = 0) RegisterCapabilitiesEvent event) {
if(event instanceof ITrackingCapEvent) {
//var stopwatch = Stopwatch.createStarted();
for(BaseCapability<?, ?> cap : ((ITrackingCapEvent)event).mfix$getTrackedCaps()) {
CapProviderGetter.deduplicateCap(cap);
}
//stopwatch.stop();
//ModernFix.LOGGER.info("Deduplicated capability lists in {}", stopwatch);
}
}
}

View File

@ -1,51 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.capability_list_compaction;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.level.ItemLike;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.neoforged.neoforge.capabilities.BaseCapability;
import net.neoforged.neoforge.capabilities.BlockCapability;
import net.neoforged.neoforge.capabilities.EntityCapability;
import net.neoforged.neoforge.capabilities.IBlockCapabilityProvider;
import net.neoforged.neoforge.capabilities.ICapabilityProvider;
import net.neoforged.neoforge.capabilities.ItemCapability;
import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent;
import org.embeddedt.modernfix.neoforge.caps.ITrackingCapEvent;
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.CallbackInfo;
import java.util.HashSet;
import java.util.Set;
@Mixin(value = RegisterCapabilitiesEvent.class, remap = false)
public class RegisterCapabilitiesEventMixin implements ITrackingCapEvent {
private final Set<BaseCapability<?, ?>> mfix$trackedCapabilities = new HashSet<>();
@Inject(method = "registerBlock", at = @At("HEAD"))
private void trackBlockCap(BlockCapability<?, ?> capability, IBlockCapabilityProvider<?, ?> provider, Block[] blocks, CallbackInfo ci) {
mfix$trackedCapabilities.add(capability);
}
@Inject(method = "registerBlockEntity", at = @At("HEAD"))
private void trackBlockEntityCap(BlockCapability<?, ?> capability, BlockEntityType<?> type, ICapabilityProvider<?, ?, ?> provider, CallbackInfo ci) {
mfix$trackedCapabilities.add(capability);
}
@Inject(method = "registerItem", at = @At("HEAD"))
private void trackItemCap(ItemCapability<?, ?> capability, ICapabilityProvider<?, ?, ?> provider, ItemLike[] items, CallbackInfo ci) {
mfix$trackedCapabilities.add(capability);
}
@Inject(method = "registerEntity", at = @At("HEAD"))
private void trackEntityCap(EntityCapability<?, ?> capability, EntityType<?> type, ICapabilityProvider<?, ?, ?> provider, CallbackInfo ci) {
mfix$trackedCapabilities.add(capability);
}
@Override
public Set<BaseCapability<?, ?>> mfix$getTrackedCaps() {
return mfix$trackedCapabilities;
}
}

View File

@ -1,7 +1,9 @@
package org.embeddedt.modernfix.common.mixin.perf.chunk_meshing; package org.embeddedt.modernfix.common.mixin.perf.chunk_meshing;
import net.minecraft.client.renderer.chunk.SectionCompiler; import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.renderer.chunk.RenderChunkRegion;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.state.BlockState;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin; import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.annotation.RequiresMod; import org.embeddedt.modernfix.annotation.RequiresMod;
import org.embeddedt.modernfix.util.blockpos.SectionBlockPosIterator; import org.embeddedt.modernfix.util.blockpos.SectionBlockPosIterator;
@ -9,7 +11,7 @@ import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(value = SectionCompiler.class, priority = 2000) @Mixin(targets = { "net/minecraft/client/renderer/chunk/ChunkRenderDispatcher$RenderChunk$RebuildTask"}, priority = 2000)
@ClientOnlyMixin @ClientOnlyMixin
@RequiresMod("!fluidlogged") @RequiresMod("!fluidlogged")
public class RebuildTaskMixin { public class RebuildTaskMixin {
@ -21,4 +23,13 @@ public class RebuildTaskMixin {
private Iterable<BlockPos> fastBetweenClosed(BlockPos firstPos, BlockPos secondPos) { private Iterable<BlockPos> fastBetweenClosed(BlockPos firstPos, BlockPos secondPos) {
return () -> new SectionBlockPosIterator(firstPos); return () -> new SectionBlockPosIterator(firstPos);
} }
/**
* @author embeddedt
* @reason RenderChunkRegion.getBlockState is expensive, avoid calling it multiple times for the same position
*/
@Redirect(method = "compile", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/chunk/RenderChunkRegion;getBlockState(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;", ordinal = 1), require = 0)
private BlockState useExistingBlockState(RenderChunkRegion instance, BlockPos pos, @Local(ordinal = 0) BlockState state) {
return state;
}
} }

View File

@ -1,8 +1,7 @@
package org.embeddedt.modernfix.common.mixin.perf.compact_mojang_registries; package org.embeddedt.modernfix.common.mixin.perf.compact_mojang_registries;
import com.mojang.serialization.Lifecycle;
import net.minecraft.core.MappedRegistry; import net.minecraft.core.MappedRegistry;
import net.minecraft.core.RegistrationInfo;
import net.minecraft.resources.ResourceKey;
import org.embeddedt.modernfix.annotation.IgnoreOutsideDev; import org.embeddedt.modernfix.annotation.IgnoreOutsideDev;
import org.embeddedt.modernfix.registry.LifecycleMap; import org.embeddedt.modernfix.registry.LifecycleMap;
import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Final;
@ -21,10 +20,10 @@ public abstract class MappedRegistryMixin<T> {
@Shadow @Shadow
@Final @Final
@Mutable @Mutable
private Map<ResourceKey<T>, RegistrationInfo> registrationInfos; private Map<T, Lifecycle> lifecycles;
@Inject(method = "<init>", at = @At("RETURN")) @Inject(method = "<init>", at = @At("RETURN"))
private void replaceStorage(CallbackInfo ci) { private void replaceStorage(CallbackInfo ci) {
this.registrationInfos = new LifecycleMap<>(); this.lifecycles = new LifecycleMap<>();
} }
} }

View File

@ -12,7 +12,7 @@ import java.util.concurrent.ExecutorService;
@Mixin(Minecraft.class) @Mixin(Minecraft.class)
@ClientOnlyMixin @ClientOnlyMixin
public class MinecraftMixin { public class MinecraftMixin {
@Redirect(method = { "<init>", "reloadResourcePacks(ZLnet/minecraft/client/Minecraft$GameLoadCookie;)Ljava/util/concurrent/CompletableFuture;" }, at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;backgroundExecutor()Ljava/util/concurrent/ExecutorService;", ordinal = 0)) @Redirect(method = { "<init>", "reloadResourcePacks(Z)Ljava/util/concurrent/CompletableFuture;" }, at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;backgroundExecutor()Ljava/util/concurrent/ExecutorService;", ordinal = 0))
private ExecutorService getResourceReloadExecutor() { private ExecutorService getResourceReloadExecutor() {
return ModernFix.resourceReloadExecutor(); return ModernFix.resourceReloadExecutor();
} }

View File

@ -10,7 +10,7 @@ import java.util.concurrent.Executor;
@Mixin(MinecraftServer.class) @Mixin(MinecraftServer.class)
public class MinecraftServerMixin { public class MinecraftServerMixin {
@ModifyArg(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/ReloadableServerResources;loadResources(Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/core/LayeredRegistryAccess;Lnet/minecraft/world/flag/FeatureFlagSet;Lnet/minecraft/commands/Commands$CommandSelection;ILjava/util/concurrent/Executor;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"), index = 5) @ModifyArg(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/ReloadableServerResources;loadResources(Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/core/RegistryAccess$Frozen;Lnet/minecraft/world/flag/FeatureFlagSet;Lnet/minecraft/commands/Commands$CommandSelection;ILjava/util/concurrent/Executor;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"), index = 5)
private Executor getReloadExecutor(Executor asyncExecutor) { private Executor getReloadExecutor(Executor asyncExecutor) {
return ModernFix.resourceReloadExecutor(); return ModernFix.resourceReloadExecutor();
} }

View File

@ -25,7 +25,7 @@ import java.util.Map;
*/ */
@Mixin(WallBlock.class) @Mixin(WallBlock.class)
public abstract class WallBlockMixin extends Block { public abstract class WallBlockMixin extends Block {
private static Map<ImmutableList<Float>, Pair<Map<Map<Property<?>, Comparable<?>>, VoxelShape>, StateDefinition<Block, BlockState>>> CACHE_BY_SHAPE_VALS = new HashMap<>(); private static Map<ImmutableList<Float>, Pair<Map<ImmutableMap<Property<?>, Comparable<?>>, VoxelShape>, StateDefinition<Block, BlockState>>> CACHE_BY_SHAPE_VALS = new HashMap<>();
public WallBlockMixin(Properties properties) { public WallBlockMixin(Properties properties) {
super(properties); super(properties);
@ -34,7 +34,7 @@ public abstract class WallBlockMixin extends Block {
@Inject(method = "makeShapes", at = @At("HEAD"), cancellable = true) @Inject(method = "makeShapes", at = @At("HEAD"), cancellable = true)
private synchronized void useCachedShapeMap(float f1, float f2, float f3, float f4, float f5, float f6, CallbackInfoReturnable<Map<BlockState, VoxelShape>> cir) { private synchronized void useCachedShapeMap(float f1, float f2, float f3, float f4, float f5, float f6, CallbackInfoReturnable<Map<BlockState, VoxelShape>> cir) {
ImmutableList<Float> key = ImmutableList.of(f1, f2, f3, f4, f5, f6); ImmutableList<Float> key = ImmutableList.of(f1, f2, f3, f4, f5, f6);
Pair<Map<Map<Property<?>, Comparable<?>>, VoxelShape>, StateDefinition<Block, BlockState>> cache = CACHE_BY_SHAPE_VALS.get(key); Pair<Map<ImmutableMap<Property<?>, Comparable<?>>, VoxelShape>, StateDefinition<Block, BlockState>> cache = CACHE_BY_SHAPE_VALS.get(key);
// require the properties to be identical // require the properties to be identical
if(cache == null || !cache.getSecond().getProperties().equals(this.stateDefinition.getProperties())) if(cache == null || !cache.getSecond().getProperties().equals(this.stateDefinition.getProperties()))
return; return;
@ -55,7 +55,7 @@ public abstract class WallBlockMixin extends Block {
return; return;
ImmutableList<Float> key = ImmutableList.of(f1, f2, f3, f4, f5, f6); ImmutableList<Float> key = ImmutableList.of(f1, f2, f3, f4, f5, f6);
if(!CACHE_BY_SHAPE_VALS.containsKey(key)) { if(!CACHE_BY_SHAPE_VALS.containsKey(key)) {
Map<Map<Property<?>, Comparable<?>>, VoxelShape> cacheByProperties = new HashMap<>(); Map<ImmutableMap<Property<?>, Comparable<?>>, VoxelShape> cacheByProperties = new HashMap<>();
Map<BlockState, VoxelShape> shapeMap = cir.getReturnValue(); Map<BlockState, VoxelShape> shapeMap = cir.getReturnValue();
for(Map.Entry<BlockState, VoxelShape> entry : shapeMap.entrySet()) { for(Map.Entry<BlockState, VoxelShape> entry : shapeMap.entrySet()) {
cacheByProperties.put(entry.getKey().getValues(), entry.getValue()); cacheByProperties.put(entry.getKey().getValues(), entry.getValue());

View File

@ -0,0 +1,19 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_dfu;
import com.mojang.datafixers.DSL;
import com.mojang.datafixers.types.Type;
import net.minecraft.world.level.block.entity.BlockEntityType;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Prevent fetchChoiceType calls from loading DFU early. Vanilla doesn't need the return values here.
*/
@Mixin(BlockEntityType.class)
public class BlockEntityTypeMixin {
@Redirect(method = "register", at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;fetchChoiceType(Lcom/mojang/datafixers/DSL$TypeReference;Ljava/lang/String;)Lcom/mojang/datafixers/types/Type;"))
private static Type<?> skipSchemaCheck(DSL.TypeReference ref, String s) {
return null;
}
}

View File

@ -1,17 +1,33 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_dfu; package org.embeddedt.modernfix.common.mixin.perf.dynamic_dfu;
import com.llamalad7.mixinextras.injector.ModifyReturnValue; import com.mojang.datafixers.DSL;
import com.mojang.datafixers.DataFixerBuilder; import com.mojang.datafixers.DataFixer;
import net.minecraft.util.datafix.DataFixers; import net.minecraft.util.datafix.DataFixers;
import org.embeddedt.modernfix.dfu.DFUBlaster; import org.embeddedt.modernfix.dfu.LazyDataFixer;
import org.spongepowered.asm.mixin.Mixin; 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.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Set;
@Mixin(DataFixers.class) @Mixin(DataFixers.class)
public class DataFixersMixin { public abstract class DataFixersMixin {
@ModifyReturnValue(method = "createFixerUpper", at = @At("RETURN")) @Shadow protected static DataFixer createFixerUpper(Set<DSL.TypeReference> set) {
private static DataFixerBuilder.Result setupMapBlasting(DataFixerBuilder.Result original) { throw new AssertionError();
DFUBlaster.blastMaps(); }
return original;
private static LazyDataFixer lazyDataFixer;
/**
* Avoid classloading the DFU logic until we actually need it.
*/
@Inject(method = "createFixerUpper", at = @At("HEAD"), cancellable = true)
private static void createLazyFixerUpper(Set<DSL.TypeReference> set, CallbackInfoReturnable<DataFixer> cir) {
if(lazyDataFixer == null) {
lazyDataFixer = new LazyDataFixer(() -> createFixerUpper(set));
cir.setReturnValue(lazyDataFixer);
}
} }
} }

View File

@ -0,0 +1,19 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_dfu;
import com.mojang.datafixers.DSL;
import com.mojang.datafixers.types.Type;
import net.minecraft.world.entity.EntityType;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Prevent fetchChoiceType calls from loading DFU early. Vanilla doesn't need the return values here.
*/
@Mixin(EntityType.Builder.class)
public class EntityTypeBuilderMixin {
@Redirect(method = "build", at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;fetchChoiceType(Lcom/mojang/datafixers/DSL$TypeReference;Ljava/lang/String;)Lcom/mojang/datafixers/types/Type;"))
private Type<?> skipSchemaCheck(DSL.TypeReference ref, String s) {
return null;
}
}

View File

@ -29,7 +29,7 @@ public class BlockModelShaperMixin {
@Inject(method = { "<init>", "replaceCache" }, at = @At("RETURN")) @Inject(method = { "<init>", "replaceCache" }, at = @At("RETURN"))
private void replaceModelMap(CallbackInfo ci) { private void replaceModelMap(CallbackInfo ci) {
// replace the backing map for mods which will access it // replace the backing map for mods which will access it
this.modelByStateCache = new DynamicOverridableMap<>(BlockState.class, state -> modelManager.getModel(ModelLocationCache.get(state))); this.modelByStateCache = new DynamicOverridableMap<>(state -> modelManager.getModel(ModelLocationCache.get(state)));
// Clear the cached models on blockstate objects // Clear the cached models on blockstate objects
for(Block block : BuiltInRegistries.BLOCK) { for(Block block : BuiltInRegistries.BLOCK) {
for(BlockState state : block.getStateDefinition().getPossibleStates()) { for(BlockState state : block.getStateDefinition().getPossibleStates()) {

View File

@ -1,117 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableList;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import it.unimi.dsi.fastutil.Pair;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntMaps;
import it.unimi.dsi.fastutil.objects.ReferenceObjectImmutablePair;
import net.minecraft.client.color.block.BlockColors;
import net.minecraft.client.renderer.block.model.BlockModelDefinition;
import net.minecraft.client.resources.model.BlockStateModelLoader;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.DefaultedRegistry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.profiling.ProfilerFiller;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.IBlockStateModelLoader;
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 java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
@Mixin(BlockStateModelLoader.class)
@ClientOnlyMixin
public abstract class BlockStateModelLoaderMixin implements IBlockStateModelLoader {
@Shadow protected abstract void loadBlockStateDefinitions(ResourceLocation resourceLocation, StateDefinition<Block, BlockState> stateDefinition);
@Shadow @Mutable @Final private Object2IntMap<BlockState> modelGroups;
private ImmutableList<BlockState> filteredStates;
@Inject(method = "<init>", at = @At("RETURN"))
private void makeModelGroupsSynchronized(Map map, ProfilerFiller profilerFiller, UnbakedModel unbakedModel, BlockColors blockColors, BiConsumer biConsumer, CallbackInfo ci) {
this.modelGroups = Object2IntMaps.synchronize(this.modelGroups);
}
@Override
public void loadSpecificBlock(ModelResourceLocation location) {
var optionalBlock = BuiltInRegistries.BLOCK.getOptional(location.id());
if(optionalBlock.isPresent()) {
// embeddedt note - filtering is currently disabled as it's quite inefficient to do vs. just loading
// the extra models and letting LRU deal with it
/*
try {
// Only filter states if we are in a world and not in the loading overlay
filteredStates = (Minecraft.getInstance().getOverlay() == null && Minecraft.getInstance().level != null) ? ModelBakeryHelpers.getBlockStatesForMRL(optionalBlock.get().getStateDefinition(), location) : null;
} catch(RuntimeException e) {
ModernFix.LOGGER.error("Exception filtering states on {}", location, e);
filteredStates = null;
}
*/
try {
this.loadBlockStateDefinitions(location.id(), optionalBlock.get().getStateDefinition());
} finally {
filteredStates = null;
}
}
}
@Redirect(method = "loadAllBlockStates", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/DefaultedRegistry;iterator()Ljava/util/Iterator;"))
private Iterator<?> skipIteratingBlocks(DefaultedRegistry instance) {
return Collections.emptyIterator();
}
@Redirect(method = "loadBlockStateDefinitions", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/StateDefinition;getPossibleStates()Lcom/google/common/collect/ImmutableList;"))
private ImmutableList<BlockState> getFilteredStates(StateDefinition<Block, BlockState> instance) {
return this.filteredStates != null ? this.filteredStates : instance.getPossibleStates();
}
// Add some caching around key hot paths
private final Cache<ReferenceObjectImmutablePair<BlockStateModelLoader.LoadedJson, ResourceLocation>, BlockModelDefinition> cachedBlockModelDefs = CacheBuilder.newBuilder()
.maximumSize(100)
.build();
private static final Cache<Pair<StateDefinition<Block, BlockState>, String>, Predicate<BlockState>> cachedBlockStatePredicates = CacheBuilder.newBuilder()
.maximumSize(100)
.build();
@WrapOperation(method = "loadBlockStateDefinitions", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/BlockStateModelLoader$LoadedJson;parse(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/renderer/block/model/BlockModelDefinition$Context;)Lnet/minecraft/client/renderer/block/model/BlockModelDefinition;"))
private BlockModelDefinition avoidMultipleParses(BlockStateModelLoader.LoadedJson instance, ResourceLocation blockStateId, BlockModelDefinition.Context context, Operation<BlockModelDefinition> original) {
try {
return cachedBlockModelDefs.get(ReferenceObjectImmutablePair.of(instance, blockStateId), () -> original.call(instance, blockStateId, context));
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
@WrapMethod(method = "predicate")
private static Predicate<BlockState> memoizePredicate(StateDefinition<Block, BlockState> stateDefentition, String properties, Operation<Predicate<BlockState>> original) {
try {
return cachedBlockStatePredicates.get(Pair.of(stateDefentition, properties), () -> original.call(stateDefentition, properties));
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -3,16 +3,17 @@ package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.google.common.base.Stopwatch; import com.google.common.base.Stopwatch;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.client.resources.model.BakedModel; import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.neoforged.bus.api.Event; import net.minecraftforge.client.ForgeHooksClient;
import net.neoforged.fml.ModContainer; import net.minecraftforge.client.event.ModelEvent;
import net.neoforged.fml.ModList; import net.minecraftforge.eventbus.api.Event;
import net.neoforged.fml.ModLoader; import net.minecraftforge.fml.ModContainer;
import net.neoforged.fml.util.ObfuscationReflectionHelper; import net.minecraftforge.fml.ModList;
import net.neoforged.neoforge.client.ClientHooks; import net.minecraftforge.fml.ModLoader;
import net.neoforged.neoforge.client.event.ModelEvent; import net.minecraftforge.fml.util.ObfuscationReflectionHelper;
import org.embeddedt.modernfix.ModernFix; import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.neoforge.dynresources.ModelBakeEventHelper; import org.embeddedt.modernfix.dynamicresources.DynamicBakedModelProvider;
import org.embeddedt.modernfix.forge.dynresources.ModelBakeEventHelper;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.Redirect;
@ -23,14 +24,14 @@ import java.util.Comparator;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Mixin(ClientHooks.class) @Mixin(ForgeHooksClient.class)
public class ForgeHooksClientMixin { public class ForgeHooksClientMixin {
/** /**
* Generate a more realistic keySet that contains every item and block model location, to help with mod compat. * Generate a more realistic keySet that contains every item and block model location, to help with mod compat.
*/ */
@Redirect(method = "onModifyBakingResult", at = @At(value = "INVOKE", target = "Lnet/neoforged/fml/ModLoader;postEvent(Lnet/neoforged/bus/api/Event;)V"), remap = false) @Redirect(method = "onModifyBakingResult", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/fml/ModLoader;postEvent(Lnet/minecraftforge/eventbus/api/Event;)V"), remap = false)
private static void postNamespacedKeySetEvent(Event event) { private static void postNamespacedKeySetEvent(ModLoader loader, Event event) {
if(ModLoader.hasErrors()) if(!ModLoader.isLoadingStateValid())
return; return;
ModelEvent.ModifyBakingResult bakeEvent = ((ModelEvent.ModifyBakingResult)event); ModelEvent.ModifyBakingResult bakeEvent = ((ModelEvent.ModifyBakingResult)event);
Stopwatch globalTimer = Stopwatch.createStarted(); Stopwatch globalTimer = Stopwatch.createStarted();
@ -41,8 +42,8 @@ public class ForgeHooksClientMixin {
Map<String, Stopwatch> times = new Object2ObjectOpenHashMap<>(); Map<String, Stopwatch> times = new Object2ObjectOpenHashMap<>();
times.put("modernfix", selfTimer); times.put("modernfix", selfTimer);
ModList.get().forEachModContainer((id, mc) -> { ModList.get().forEachModContainer((id, mc) -> {
Map<ModelResourceLocation, BakedModel> newRegistry = helper.wrapRegistry(id); Map<ResourceLocation, BakedModel> newRegistry = helper.wrapRegistry(id);
ModelEvent.ModifyBakingResult postedEvent = new ModelEvent.ModifyBakingResult(newRegistry, bakeEvent.getTextureGetter(), bakeEvent.getModelBakery()); ModelEvent.ModifyBakingResult postedEvent = new ModelEvent.ModifyBakingResult(newRegistry, bakeEvent.getModelBakery());
Stopwatch timer = times.computeIfAbsent(id, $ -> Stopwatch.createUnstarted()); Stopwatch timer = times.computeIfAbsent(id, $ -> Stopwatch.createUnstarted());
timer.start(); timer.start();
try { try {
@ -62,5 +63,8 @@ public class ForgeHooksClientMixin {
ModernFix.LOGGER.warn(" {}: {}", entry.getKey(), entry.getValue().toString()); ModernFix.LOGGER.warn(" {}: {}", entry.getKey(), entry.getValue().toString());
}); });
} }
if (bakeEvent.getModels() instanceof DynamicBakedModelProvider dynamicProvider) {
dynamicProvider.dumpStats();
}
} }
} }

View File

@ -4,9 +4,11 @@ import net.minecraft.client.renderer.ItemModelShaper;
import net.minecraft.client.resources.model.BakedModel; import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager; import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ModelResourceLocation; import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.core.Holder;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import net.neoforged.neoforge.client.model.RegistryAwareItemModelShaper; import net.minecraftforge.client.model.ForgeItemModelShaper;
import net.minecraftforge.registries.ForgeRegistries;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin; import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.dynamicresources.DynamicModelCache; import org.embeddedt.modernfix.dynamicresources.DynamicModelCache;
import org.embeddedt.modernfix.dynamicresources.ModelLocationCache; import org.embeddedt.modernfix.dynamicresources.ModelLocationCache;
@ -19,20 +21,20 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Mixin(RegistryAwareItemModelShaper.class) @Mixin(ForgeItemModelShaper.class)
@ClientOnlyMixin @ClientOnlyMixin
public abstract class ItemModelMesherForgeMixin extends ItemModelShaper { public abstract class ItemModelMesherForgeMixin extends ItemModelShaper {
@Shadow(remap = false) @Final @Mutable private Map<Item, ModelResourceLocation> locations; @Shadow(remap = false) @Final @Mutable private Map<Holder.Reference<Item>, ModelResourceLocation> locations;
private Map<Item, ModelResourceLocation> overrideLocations; private Map<Holder.Reference<Item>, ModelResourceLocation> overrideLocations;
private final DynamicModelCache<Item> mfix$modelCache = new DynamicModelCache<>(k -> this.mfix$getModelSlow((Item)k), true); private final DynamicModelCache<Holder.Reference<Item>> mfix$modelCache = new DynamicModelCache<>(k -> this.mfix$getModelSlow((Holder.Reference<Item>)k), true);
public ItemModelMesherForgeMixin(ModelManager arg) { public ItemModelMesherForgeMixin(ModelManager arg) {
super(arg); super(arg);
} }
private static final ModelResourceLocation SENTINEL = new ModelResourceLocation(ResourceLocation.fromNamespaceAndPath("modernfix", "sentinel"), "sentinel"); private static final ModelResourceLocation SENTINEL = new ModelResourceLocation(new ResourceLocation("modernfix", "sentinel"), "sentinel");
@Inject(method = "<init>", at = @At("RETURN")) @Inject(method = "<init>", at = @At("RETURN"))
private void replaceLocationMap(CallbackInfo ci) { private void replaceLocationMap(CallbackInfo ci) {
@ -42,16 +44,16 @@ public abstract class ItemModelMesherForgeMixin extends ItemModelShaper {
} }
@Unique @Unique
private ModelResourceLocation mfix$getLocationForge(Item item) { private ModelResourceLocation mfix$getLocationForge(Holder.Reference<Item> item) {
ModelResourceLocation map = overrideLocations.getOrDefault(item, SENTINEL); ModelResourceLocation map = overrideLocations.getOrDefault(item, SENTINEL);
if(map == SENTINEL) { if(map == SENTINEL) {
/* generate the appropriate location from our cache */ /* generate the appropriate location from our cache */
map = ModelLocationCache.get(item); map = ModelLocationCache.get(item.get());
} }
return map; return map;
} }
private BakedModel mfix$getModelSlow(Item key) { private BakedModel mfix$getModelSlow(Holder.Reference<Item> key) {
ModelResourceLocation map = mfix$getLocationForge(key); ModelResourceLocation map = mfix$getLocationForge(key);
return map == null ? null : getModelManager().getModel(map); return map == null ? null : getModelManager().getModel(map);
} }
@ -64,7 +66,7 @@ public abstract class ItemModelMesherForgeMixin extends ItemModelShaper {
@Overwrite @Overwrite
@Override @Override
public BakedModel getItemModel(Item item) { public BakedModel getItemModel(Item item) {
return this.mfix$modelCache.get(item); return this.mfix$modelCache.get(ForgeRegistries.ITEMS.getDelegateOrThrow(item));
} }
/** /**
@ -75,7 +77,7 @@ public abstract class ItemModelMesherForgeMixin extends ItemModelShaper {
@Overwrite @Overwrite
@Override @Override
public void register(Item item, ModelResourceLocation location) { public void register(Item item, ModelResourceLocation location) {
overrideLocations.put(item, location); overrideLocations.put(ForgeRegistries.ITEMS.getDelegateOrThrow(item), location);
} }
/** /**

View File

@ -33,7 +33,7 @@ public abstract class ItemModelShaperMixin {
super(); super();
} }
private static final ModelResourceLocation SENTINEL_VANILLA = new ModelResourceLocation(ResourceLocation.fromNamespaceAndPath("modernfix", "sentinel"), "sentinel"); private static final ModelResourceLocation SENTINEL_VANILLA = new ModelResourceLocation(new ResourceLocation("modernfix", "sentinel"), "sentinel");
private final DynamicModelCache<Item> mfix$itemModelCache = new DynamicModelCache<>(k -> this.mfix$getModelForItem((Item)k), true); private final DynamicModelCache<Item> mfix$itemModelCache = new DynamicModelCache<>(k -> this.mfix$getModelForItem((Item)k), true);

View File

@ -0,0 +1,34 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.renderer.block.model.ItemOverrides;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.client.resources.model.ModelState;
import net.minecraft.resources.ResourceLocation;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.IExtendedModelBaker;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import java.util.function.Function;
@Mixin(ItemOverrides.class)
@ClientOnlyMixin
public class ItemOverridesForgeMixin {
/**
* @author embeddedt
* @reason servers insist on generating invalid item overrides that have missing models
*/
@WrapOperation(method = "bakeModel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelBaker;bake(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/resources/model/ModelState;Ljava/util/function/Function;)Lnet/minecraft/client/resources/model/BakedModel;"), remap = false)
private BakedModel bake(ModelBaker instance, ResourceLocation resourceLocation, ModelState modelState, Function<ResourceLocation, TextureAtlasSprite> spriteGetter, Operation<BakedModel> original) {
boolean prevState = ((IExtendedModelBaker)instance).throwOnMissingModel(false);
try {
return original.call(instance, resourceLocation, modelState, spriteGetter);
} finally {
((IExtendedModelBaker)instance).throwOnMissingModel(prevState);
}
}
}

View File

@ -0,0 +1,26 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.renderer.block.model.ItemOverrides;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.resources.ResourceLocation;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.IExtendedModelBaker;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(ItemOverrides.class)
@ClientOnlyMixin
public class ItemOverridesMixin {
@WrapOperation(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelBaker;getModel(Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/client/resources/model/UnbakedModel;"))
private UnbakedModel preventThrowForMissing(ModelBaker instance, ResourceLocation resourceLocation, Operation<UnbakedModel> original) {
boolean prevState = ((IExtendedModelBaker)instance).throwOnMissingModel(false);
try {
return original.call(instance, resourceLocation);
} finally {
((IExtendedModelBaker)instance).throwOnMissingModel(prevState);
}
}
}

View File

@ -1,22 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.model.ModelManager;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.IExtendedModelManager;
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(Minecraft.class)
@ClientOnlyMixin
public abstract class MinecraftMixin_ModelTicking {
@Shadow public abstract ModelManager getModelManager();
@Inject(method = "tick", at = @At(value = "RETURN"))
private void tickModels(CallbackInfo ci) {
((IExtendedModelManager)this.getModelManager()).mfix$tick();
}
}

View File

@ -1,85 +1,102 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources; package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.llamalad7.mixinextras.injector.ModifyReturnValue; import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.BakedModel; import net.minecraft.client.resources.model.*;
import net.minecraft.client.resources.model.Material;
import net.minecraft.client.resources.model.ModelBakery;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.client.resources.model.ModelState;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import org.embeddedt.modernfix.ModernFix; import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.ModernFixClient;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin; import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.api.entrypoint.ModernFixClientIntegration;
import org.embeddedt.modernfix.duck.IExtendedModelBaker;
import org.embeddedt.modernfix.duck.IExtendedModelBakery; import org.embeddedt.modernfix.duck.IExtendedModelBakery;
import org.embeddedt.modernfix.dynamicresources.ModelMissingException;
import org.embeddedt.modernfix.forge.dynresources.IModelBakerImpl;
import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At; 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.function.Function; import java.util.function.Function;
@Mixin(ModelBakery.ModelBakerImpl.class) @Mixin(value = ModelBakery.ModelBakerImpl.class, priority = 600)
@ClientOnlyMixin @ClientOnlyMixin
public abstract class ModelBakerImplMixin { public abstract class ModelBakerImplMixin implements IModelBakerImpl, IExtendedModelBaker {
@Shadow public abstract UnbakedModel getModel(ResourceLocation location); private static final boolean debugDynamicModelLoading = Boolean.getBoolean("modernfix.debugDynamicModelLoading");
@Shadow(aliases = {"this$0","f_243927_"}) @Final private ModelBakery field_40571;
@Shadow(aliases = {"this$0"}) @Final private ModelBakery field_40571; private boolean mfix$ignoreCache = false;
@Unique
private int mfix$getDepth = 0;
/** @Shadow @Final private Function<Material, TextureAtlasSprite> modelTextureGetter;
* @author embeddedt
* @reason force parent resolution to happen before model gets baked @Override
*/ public void mfix$ignoreCache() {
@ModifyReturnValue(method = "getModel", at = @At("RETURN")) mfix$ignoreCache = true;
private UnbakedModel resolveParents(UnbakedModel model) { }
mfix$getDepth++;
if(mfix$getDepth == 1) { private boolean throwIfMissing;
try {
model.resolveParents(this::getModel); @Override
} catch(Exception e) { public boolean throwOnMissingModel(boolean flag) {
ModernFix.LOGGER.warn("Exception encountered resolving parents", e); boolean old = throwIfMissing;
throwIfMissing = flag;
return old;
}
@Inject(method = "getModel", at = @At("HEAD"), cancellable = true)
private void obtainModel(ResourceLocation arg, CallbackInfoReturnable<UnbakedModel> cir) {
if(debugDynamicModelLoading)
ModernFix.LOGGER.info("Baking {}", arg);
IExtendedModelBakery extendedBakery = (IExtendedModelBakery)this.field_40571;
if(arg instanceof ModelResourceLocation && arg != ModelBakery.MISSING_MODEL_LOCATION) {
// synchronized because we use topLevelModels
synchronized (this.field_40571) {
this.field_40571.loadTopLevel((ModelResourceLocation)arg);
cir.setReturnValue(this.field_40571.topLevelModels.getOrDefault(arg, extendedBakery.mfix$getUnbakedMissingModel()));
// avoid leaks
this.field_40571.topLevelModels.clear();
}
} else
cir.setReturnValue(this.field_40571.getModel(arg));
UnbakedModel toReplace = cir.getReturnValue();
if(true) {
for(ModernFixClientIntegration integration : ModernFixClient.CLIENT_INTEGRATIONS) {
try {
toReplace = integration.onUnbakedModelPreBake(arg, toReplace, this.field_40571);
} catch(RuntimeException e) {
ModernFix.LOGGER.error("Exception firing model pre-bake event for {}", arg, e);
}
} }
} }
mfix$getDepth--; cir.setReturnValue(toReplace);
cir.getReturnValue().resolveParents(this.field_40571::getModel);
if(cir.getReturnValue() == extendedBakery.mfix$getUnbakedMissingModel()) {
if(arg != ModelBakery.MISSING_MODEL_LOCATION) {
if(debugDynamicModelLoading)
ModernFix.LOGGER.warn("Model {} not present", arg);
if(throwIfMissing)
throw new ModelMissingException();
}
}
}
@ModifyExpressionValue(method = "bake(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/resources/model/ModelState;Ljava/util/function/Function;)Lnet/minecraft/client/resources/model/BakedModel;", at = @At(value = "INVOKE", target = "Ljava/util/Map;get(Ljava/lang/Object;)Ljava/lang/Object;", ordinal = 0), remap = false)
private Object ignoreCacheIfRequested(Object o) {
return mfix$ignoreCache ? null : o;
}
@WrapOperation(method = "bake(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/resources/model/ModelState;Ljava/util/function/Function;)Lnet/minecraft/client/resources/model/BakedModel;", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/UnbakedModel;bake(Lnet/minecraft/client/resources/model/ModelBaker;Ljava/util/function/Function;Lnet/minecraft/client/resources/model/ModelState;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/client/resources/model/BakedModel;"))
private BakedModel callBakedModelIntegration(UnbakedModel unbakedModel, ModelBaker baker, Function<Material, TextureAtlasSprite> spriteGetter, ModelState state, ResourceLocation location, Operation<BakedModel> operation) {
BakedModel model = operation.call(unbakedModel, baker, spriteGetter, state, location);
for(ModernFixClientIntegration integration : ModernFixClient.CLIENT_INTEGRATIONS) {
model = integration.onBakedModelLoad(location, unbakedModel, model, state, this.field_40571, spriteGetter);
}
return model; return model;
} }
@WrapMethod(method = "bake(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/resources/model/ModelState;)Lnet/minecraft/client/resources/model/BakedModel;")
private BakedModel mfix$lockWhenBaking(ResourceLocation location, ModelState transform, Operation<BakedModel> original) {
var lock = ((IExtendedModelBakery)this.field_40571).mfix$getLock();
lock.lock();
try {
return original.call(location, transform);
} finally {
lock.unlock();
}
}
/**
* @author embeddedt
* @reason Handle dynamic model loading
*/
@Overwrite(remap = false)
public UnbakedModel getTopLevelModel(ModelResourceLocation location) {
IExtendedModelBakery bakery = (IExtendedModelBakery)this.field_40571;
UnbakedModel model = bakery.mfix$loadUnbakedModelDynamic(location);
return model == bakery.mfix$getMissingModel() ? null : model;
}
@WrapMethod(method = "bake(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/resources/model/ModelState;Ljava/util/function/Function;)Lnet/minecraft/client/resources/model/BakedModel;", remap = false)
private BakedModel mfix$lockWhenBaking(ResourceLocation location, ModelState transform, Function<Material, TextureAtlasSprite> textureGetter, Operation<BakedModel> original) {
var lock = ((IExtendedModelBakery)this.field_40571).mfix$getLock();
lock.lock();
try {
return original.call(location, transform, textureGetter);
} finally {
lock.unlock();
}
}
} }

View File

@ -1,274 +1,412 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources; package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalCause;
import com.google.common.cache.RemovalNotification;
import com.google.common.collect.ForwardingMap;
import com.google.common.collect.ImmutableList;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import net.minecraft.client.Minecraft;
import net.minecraft.client.color.block.BlockColors; import net.minecraft.client.color.block.BlockColors;
import net.minecraft.client.resources.model.BakedModel; import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.BlockModelRotation; import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.BlockStateModelLoader; import net.minecraft.client.resources.model.*;
import net.minecraft.client.resources.model.ModelBakery;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.DefaultedRegistry;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import org.embeddedt.modernfix.ModernFix; import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.ModernFixClient; import org.embeddedt.modernfix.ModernFixClient;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.api.entrypoint.ModernFixClientIntegration; import org.embeddedt.modernfix.api.entrypoint.ModernFixClientIntegration;
import org.embeddedt.modernfix.duck.IBlockStateModelLoader; import org.embeddedt.modernfix.duck.IExtendedModelBaker;
import org.embeddedt.modernfix.dynamicresources.DynamicBakedModelProvider;
import org.embeddedt.modernfix.duck.IExtendedModelBakery; import org.embeddedt.modernfix.duck.IExtendedModelBakery;
import org.embeddedt.modernfix.util.DynamicOverridableMap; import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.util.LRUMap; import org.embeddedt.modernfix.dynamicresources.ModelBakeryHelpers;
import org.objectweb.asm.Opcodes;
import org.slf4j.Logger;
import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable; import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.*;
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.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Collections; import java.io.IOException;
import java.util.Map; import java.util.*;
import java.util.Objects; import java.util.concurrent.ConcurrentMap;
import java.util.Set; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiConsumer;
import java.util.function.BiFunction;
@Mixin(ModelBakery.class) /* low priority so that our injectors are added after other mods' */
@Mixin(value = ModelBakery.class, priority = 1100)
@ClientOnlyMixin @ClientOnlyMixin
public abstract class ModelBakeryMixin implements IExtendedModelBakery { public abstract class ModelBakeryMixin implements IExtendedModelBakery {
@Unique
private BlockStateModelLoader dynamicLoader;
@Unique private static final boolean debugDynamicModelLoading = Boolean.getBoolean("modernfix.debugDynamicModelLoading");
private final ReentrantLock modelBakeryLock = new ReentrantLock();
@Unique @Shadow @Final @Mutable public Map<ResourceLocation, UnbakedModel> unbakedCache;
private ModelBakery.TextureGetter textureGetter;
@Unique @Shadow @Final public static ModelResourceLocation MISSING_MODEL_LOCATION;
private BakedModel bakedMissingModel;
@Shadow abstract UnbakedModel getModel(ResourceLocation resourceLocation); @Shadow protected abstract BlockModel loadBlockModel(ResourceLocation location) throws IOException;
@Shadow @Final private UnbakedModel missingModel; @Shadow @Final private Set<ResourceLocation> loadingStack;
@Unique @Shadow protected abstract void loadModel(ResourceLocation blockstateLocation) throws Exception;
private static final boolean DEBUG_MODEL_LOADS = Boolean.getBoolean("modernfix.debugDynamicModelLoading");
/** @Shadow @Final @Mutable
* Bake a model using the provided texture getter and location. The model is stored in {@link ModelBakeryMixin#bakedTopLevelModels}. private Map<ResourceLocation, BakedModel> bakedTopLevelModels;
*/
@Shadow(aliases = "lambda$bakeModels$6") protected abstract void method_61072(ModelBakery.TextureGetter getter, ModelResourceLocation location, UnbakedModel model);
@Shadow @Mutable @Final private Map<ModelResourceLocation, BakedModel> bakedTopLevelModels; @Shadow @Final @Mutable private Map<ModelBakery.BakedCacheKey, BakedModel> bakedCache;
@Shadow @Mutable @Final public Map<ModelResourceLocation, UnbakedModel> topLevelModels;
@Shadow @Mutable @Final private Map<ResourceLocation, UnbakedModel> unbakedCache;
@Shadow @Mutable @Final public Map<ModelBakery.BakedCacheKey, BakedModel> bakedCache;
@Shadow protected abstract void loadItemModelAndDependencies(ResourceLocation resourceLocation); @Shadow @Final @Mutable private BlockColors blockColors;
@Shadow @Final private static Logger LOGGER;
@Shadow
public abstract void loadTopLevel(ModelResourceLocation modelResourceLocation);
@Shadow public abstract UnbakedModel getModel(ResourceLocation resourceLocation);
private Cache<ModelBakery.BakedCacheKey, BakedModel> loadedBakedModels;
private Cache<ResourceLocation, UnbakedModel> loadedModels;
private HashMap<ResourceLocation, UnbakedModel> smallLoadingCache = new HashMap<>();
private boolean ignoreModelLoad;
// disable fabric recursion
@SuppressWarnings("unused")
private boolean fabric_enableGetOrLoadModelGuard;
@Shadow @Final public static ModelResourceLocation MISSING_MODEL_VARIANT; @Redirect(method = "<init>", at = @At(value = "FIELD", opcode = Opcodes.PUTFIELD, target = "Lnet/minecraft/client/resources/model/ModelBakery;blockColors:Lnet/minecraft/client/color/block/BlockColors;"))
private void replaceTopLevelBakedModels(ModelBakery bakery, BlockColors val) {
@Shadow protected abstract void registerModelAndLoadDependencies(ModelResourceLocation modelLocation, UnbakedModel model); fabric_enableGetOrLoadModelGuard = false;
this.blockColors = val;
private final Map<ModelResourceLocation, BakedModel> mfix$emulatedBakedRegistry = new DynamicOverridableMap<>(ModelResourceLocation.class, this::loadBakedModelDynamic); this.loadedBakedModels = CacheBuilder.newBuilder()
.expireAfterAccess(ModelBakeryHelpers.MAX_MODEL_LIFETIME_SECS, TimeUnit.SECONDS)
@Override .maximumSize(ModelBakeryHelpers.MAX_BAKED_MODEL_COUNT)
public UnbakedModel mfix$loadUnbakedModelDynamic(ModelResourceLocation location) { .concurrencyLevel(8)
if(location.equals(MISSING_MODEL_VARIANT)) { .removalListener(this::onModelRemoved)
return missingModel; .softValues()
} .build();
modelBakeryLock.lock(); this.loadedModels = CacheBuilder.newBuilder()
try { .expireAfterAccess(ModelBakeryHelpers.MAX_MODEL_LIFETIME_SECS, TimeUnit.SECONDS)
UnbakedModel existing = this.topLevelModels.get(location); .maximumSize(ModelBakeryHelpers.MAX_UNBAKED_MODEL_COUNT)
if (existing != null) { .concurrencyLevel(8)
return existing; .removalListener(this::onModelRemoved)
.softValues()
.build();
this.bakedCache = loadedBakedModels.asMap();
ConcurrentMap<ResourceLocation, UnbakedModel> unbakedCacheBackingMap = loadedModels.asMap();
this.unbakedCache = new ForwardingMap<ResourceLocation, UnbakedModel>() {
@Override
protected Map<ResourceLocation, UnbakedModel> delegate() {
return unbakedCacheBackingMap;
} }
if(DEBUG_MODEL_LOADS) {
ModernFix.LOGGER.info("Loading model {}", location); @Override
public UnbakedModel put(ResourceLocation key, UnbakedModel value) {
smallLoadingCache.put(key, value);
return super.put(key, value);
} }
if(location.variant().equals("inventory")) { };
this.loadItemModelAndDependencies(location.id()); this.bakedTopLevelModels = new DynamicBakedModelProvider((ModelBakery)(Object)this, bakedCache);
} else if (location.variant().equals("fabric_resource") || location.variant().equals("standalone")) {
UnbakedModel unbakedModel = this.getModel(location.id());
this.registerModelAndLoadDependencies(location, unbakedModel);
} else {
((IBlockStateModelLoader)dynamicLoader).loadSpecificBlock(location);
}
return this.topLevelModels.getOrDefault(location, this.missingModel);
} finally {
modelBakeryLock.unlock();
}
} }
@WrapMethod(method = "getModel") @ModifyExpressionValue(method = "<init>", at = @At(value = "FIELD", opcode = Opcodes.GETSTATIC, ordinal = 0, target = "Lnet/minecraft/client/resources/model/ModelBakery;STATIC_DEFINITIONS:Ljava/util/Map;"))
private UnbakedModel mfix$lockWhenGettingModel(ResourceLocation modelLocation, Operation<UnbakedModel> original) { private Map<ResourceLocation, StateDefinition<Block, BlockState>> ignoreFutureModelLoads(Map<ResourceLocation, StateDefinition<Block, BlockState>> original) {
modelBakeryLock.lock(); this.ignoreModelLoad = true;
try {
return original.call(modelLocation);
} finally {
modelBakeryLock.unlock();
}
}
@Override
public UnbakedModel mfix$getMissingModel() {
return missingModel;
}
@Unique
private BakedModel loadBakedModelDynamic(ModelResourceLocation location) {
if(location.equals(MISSING_MODEL_VARIANT)) {
return bakedMissingModel;
}
BakedModel model;
modelBakeryLock.lock();
try {
model = bakedTopLevelModels.get(location);
if(model == null) {
UnbakedModel prototype = mfix$loadUnbakedModelDynamic(location);
if(prototype == missingModel) {
model = bakedMissingModel;
} else {
prototype.resolveParents(this::getModel);
if(DEBUG_MODEL_LOADS) {
ModernFix.LOGGER.info("Baking model {}", location);
}
this.method_61072(this.textureGetter, location, prototype);
model = bakedTopLevelModels.remove(location);
if(model == null) {
ModernFix.LOGGER.error("Failed to load model " + location);
model = bakedMissingModel;
}
for(ModernFixClientIntegration integration : ModernFixClient.CLIENT_INTEGRATIONS) {
try {
model = integration.onBakedModelLoad(location, prototype, model, BlockModelRotation.X0_Y0, (ModelBakery)(Object)this, this.textureGetter);
} catch (RuntimeException e) {
ModernFix.LOGGER.error("Exception encountered running dynamic resources integration", e);
}
}
}
}
} finally {
modelBakeryLock.unlock();
}
return model;
}
@ModifyExpressionValue(method = "<init>", at = @At(value = "CONSTANT", args = "stringValue=missing_model"))
private String replaceBackingMaps(String original) {
this.unbakedCache = new LRUMap<>(this.unbakedCache);
this.bakedCache = new LRUMap<>(this.bakedCache);
this.topLevelModels = new LRUMap<>(this.topLevelModels);
this.bakedTopLevelModels = new LRUMap<>(this.bakedTopLevelModels);
return original; return original;
} }
@WrapOperation(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/BlockStateModelLoader;loadAllBlockStates()V")) private <K, V> void onModelRemoved(RemovalNotification<K, V> notification) {
private void noInitialBlockStateLoad(BlockStateModelLoader instance, Operation<Void> original) { if(!debugDynamicModelLoading)
dynamicLoader = instance; return;
original.call(instance); // If the entry was replaced (happens because of the Minecraft model loading code structure), or
} // was explicitly removed, we don't really care.
var reason = notification.getCause();
@Redirect(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/DefaultedRegistry;keySet()Ljava/util/Set;")) if (reason == RemovalCause.REPLACED || reason == RemovalCause.EXPLICIT) {
private Set<?> skipLoadingItems(DefaultedRegistry instance) {
return Collections.emptySet();
}
@Inject(method = "bakeModels", at = @At("HEAD"))
private void storeTextureGetterAndBakeMissing(ModelBakery.TextureGetter textureGetter, CallbackInfo ci) {
this.textureGetter = textureGetter;
this.method_61072(textureGetter, MISSING_MODEL_VARIANT, Objects.requireNonNull(this.topLevelModels.get(MISSING_MODEL_VARIANT)));
this.bakedMissingModel = this.bakedTopLevelModels.get(MISSING_MODEL_VARIANT);
}
private boolean inInitialLoad = true;
@Inject(method = "bakeModels", at = @At("RETURN"))
private void onInitialBakeFinish(ModelBakery.TextureGetter textureGetter, CallbackInfo ci) {
var permanentMRLs = new ObjectOpenHashSet<>(this.bakedTopLevelModels.keySet());
((LRUMap<ModelResourceLocation, BakedModel>)this.bakedTopLevelModels).setPermanentEntries(permanentMRLs);
ModernFix.LOGGER.info("Dynamic model bakery initial baking finished, with {} permanent top level baked models", this.bakedTopLevelModels.size());
}
@Inject(method = "<init>", at = @At("RETURN"))
private void onInitialLoadFinish(BlockColors blockColors, ProfilerFiller profilerFiller, Map map, Map map2, CallbackInfo ci) {
var permanentMRLs = new ObjectOpenHashSet<>(this.topLevelModels.keySet());
((LRUMap<ModelResourceLocation, UnbakedModel>)this.topLevelModels).setPermanentEntries(permanentMRLs);
ModernFix.LOGGER.info("Dynamic model bakery loading finished, with {} permanent top level models", this.topLevelModels.size());
}
@Unique
private int tickCount;
@Unique
private static final int MAXIMUM_CACHE_SIZE = 1000;
private void runCleanup() {
try {
((LRUMap<?, ?>)this.unbakedCache).dropEntriesToMeetSize(MAXIMUM_CACHE_SIZE);
} catch(RuntimeException e) {
throw new IllegalStateException("Exception dropping entries in unbaked cache", e);
}
try {
((LRUMap<?, ?>)this.bakedCache).dropEntriesToMeetSize(MAXIMUM_CACHE_SIZE);
} catch(RuntimeException e) {
throw new IllegalStateException("Exception dropping entries in baked cache", e);
}
try {
((LRUMap<?, ?>)this.topLevelModels).dropEntriesToMeetSize(MAXIMUM_CACHE_SIZE);
} catch(RuntimeException e) {
throw new IllegalStateException("Exception dropping entries in top level models", e);
}
try {
((LRUMap<?, ?>)this.bakedTopLevelModels).dropEntriesToMeetSize(MAXIMUM_CACHE_SIZE);
} catch(RuntimeException e) {
throw new IllegalStateException("Exception dropping entries in baked top level models", e);
}
}
@Override
public void mfix$finishLoading() {
inInitialLoad = false;
}
@Override
public void mfix$tick() {
if(inInitialLoad) {
return; return;
} }
tickCount++; Object k = notification.getKey();
if((tickCount % 200) == 0) { if(k == null)
if(modelBakeryLock.tryLock()) { return;
try { ResourceLocation rl;
runCleanup(); boolean baked = false;
} finally { if(k instanceof ResourceLocation) {
modelBakeryLock.unlock(); rl = (ResourceLocation)k;
} } else {
} rl = ((ModelBakery.BakedCacheKey)k).id();
baked = true;
} }
/* can fire when a model is replaced */
if(!baked && this.loadedModels.getIfPresent(rl) != null)
return;
ModernFix.LOGGER.warn("Evicted {} model {}", baked ? "baked" : "unbaked", rl);
}
private UnbakedModel missingModel;
private Set<ResourceLocation> blockStateFiles;
private Set<ResourceLocation> modelFiles;
@ModifyArg(method = "<init>", at = @At(value = "INVOKE", target = "Ljava/util/Map;put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", ordinal = 0), index = 1)
private Object captureMissingModel(Object model) {
this.missingModel = (UnbakedModel)model;
this.blockStateFiles = new HashSet<>();
this.modelFiles = new HashSet<>();
return this.missingModel;
} }
/** /**
* @author embeddedt * @author embeddedt
* @reason We provide a fake baked registry to the rest of Minecraft, that dynamically loads models. * @reason don't actually load most models
*/ */
@Overwrite @Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelBakery;loadTopLevel(Lnet/minecraft/client/resources/model/ModelResourceLocation;)V"))
public Map<ModelResourceLocation, BakedModel> getBakedTopLevelModels() { private void addTopLevelFile(ModelBakery bakery, ModelResourceLocation location) {
return this.mfix$emulatedBakedRegistry; if(location == MISSING_MODEL_LOCATION || !this.ignoreModelLoad) {
loadTopLevel(location);
}
}
@Redirect(method = "<init>", at = @At(value = "INVOKE", target = "Ljava/util/Map;forEach(Ljava/util/function/BiConsumer;)V", ordinal = 0))
private void fetchStaticDefinitions(Map<ResourceLocation, StateDefinition<Block, BlockState>> map, BiConsumer<ResourceLocation, StateDefinition<Block, BlockState>> func) {
/* no-op */
}
@Redirect(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/StateDefinition;getPossibleStates()Lcom/google/common/collect/ImmutableList;", ordinal = 0))
private ImmutableList<BlockState> fetchBlocks(StateDefinition<Block, BlockState> def) {
/* no-op */
return ImmutableList.of();
}
/**
* @author embeddedt
* @reason Prevent the models provided by RegisterAdditional from being tracked, otherwise the unbaked models will
* be loaded, baked, and added to permanent overrides unnecessarily.
* We still need to fire the event, because there is a decent chance mods rely on it to set up state for their
* model loaders, but we can ignore the return value.
*/
@WrapOperation(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/client/ForgeHooksClient;onRegisterAdditionalModels(Ljava/util/Set;)V"))
private void preventLoadOfAdditionalModels(Set<ResourceLocation> additionalModels, Operation<Void> original) {
original.call(additionalModels);
// Immediately clear the set
additionalModels.clear();
}
/**
* Make a copy of the top-level model list to avoid CME if more models get loaded here.
*/
@Redirect(method = "<init>", at = @At(value = "INVOKE", target = "Ljava/util/Map;values()Ljava/util/Collection;", ordinal = 0))
private Collection<?> copyTopLevelModelList(Map<?, ?> map) {
return new ArrayList<>(map.values());
}
private BiFunction<ResourceLocation, Material, TextureAtlasSprite> textureGetter;
@Inject(method = "bakeModels", at = @At("HEAD"))
private void captureGetter(BiFunction<ResourceLocation, Material, TextureAtlasSprite> getter, CallbackInfo ci) {
this.ignoreModelLoad = false;
textureGetter = getter;
DynamicBakedModelProvider.currentInstance = (DynamicBakedModelProvider)this.bakedTopLevelModels;
}
@Redirect(method = "bakeModels", at = @At(value = "INVOKE", target = "Ljava/util/Map;keySet()Ljava/util/Set;"))
private Set<ResourceLocation> skipBake(Map<ResourceLocation, UnbakedModel> instance) {
Set<ResourceLocation> modelSet = new HashSet<>(instance.keySet());
if(modelSet.size() > 0)
ModernFix.LOGGER.info("Early baking {} models", modelSet.size());
return modelSet;
}
/**
* Use the already loaded missing model instead of the cache entry (which will probably get evicted).
*/
@Redirect(method = "loadModel", at = @At(value = "INVOKE", target = "Ljava/util/Map;get(Ljava/lang/Object;)Ljava/lang/Object;", ordinal = 1))
private Object getMissingModel(Map map, Object rl) {
if(rl == MISSING_MODEL_LOCATION && map == unbakedCache)
return missingModel;
return unbakedCache.get(rl);
}
@ModifyVariable(method = "cacheAndQueueDependencies", at = @At("HEAD"), argsOnly = true)
private UnbakedModel fireUnbakedEvent(UnbakedModel model, ResourceLocation location) {
for(ModernFixClientIntegration integration : ModernFixClient.CLIENT_INTEGRATIONS) {
try {
model = integration.onUnbakedModelLoad(location, model, (ModelBakery)(Object)this);
} catch(RuntimeException e) {
ModernFix.LOGGER.error("Exception firing model load event for {}", location, e);
}
}
return model;
}
@Inject(method = "cacheAndQueueDependencies", at = @At("RETURN"))
private void addToSmallLoadingCache(ResourceLocation location, UnbakedModel model, CallbackInfo ci) {
smallLoadingCache.put(location, model);
}
private int mfix$nestedLoads = 0;
/**
* @author embeddedt
* @reason synchronize
*/
@Inject(method = "getModel", at = @At("HEAD"), cancellable = true)
public void getOrLoadModelDynamic(ResourceLocation modelLocation, CallbackInfoReturnable<UnbakedModel> cir) {
if(modelLocation.equals(MISSING_MODEL_LOCATION)) {
cir.setReturnValue(missingModel);
return;
}
UnbakedModel existing = this.unbakedCache.get(modelLocation);
if (existing != null) {
cir.setReturnValue(existing);
} else {
synchronized(this) {
// CIT Resewn adds dependencies to loadingStack outside of getModel(), so we must silently
// ignore existing things in the stack for the outermost model
if (mfix$nestedLoads > 0 && this.loadingStack.contains(modelLocation)) {
throw new IllegalStateException("Circular reference while loading " + modelLocation);
} else {
this.loadingStack.add(modelLocation);
UnbakedModel iunbakedmodel = missingModel;
while(!this.loadingStack.isEmpty()) {
ResourceLocation resourcelocation = this.loadingStack.iterator().next();
mfix$nestedLoads++;
try {
existing = this.unbakedCache.get(resourcelocation);
if (existing == null) {
if(debugDynamicModelLoading)
LOGGER.info("Loading {}", resourcelocation);
this.loadModel(resourcelocation);
} else
smallLoadingCache.put(resourcelocation, existing);
} catch (ModelBakery.BlockStateDefinitionException var9) {
LOGGER.warn(var9.getMessage());
this.unbakedCache.put(resourcelocation, iunbakedmodel);
smallLoadingCache.put(resourcelocation, iunbakedmodel);
} catch (Exception var10) {
LOGGER.warn("Unable to load model: '{}' referenced from: {}: {}", resourcelocation, modelLocation, var10);
this.unbakedCache.put(resourcelocation, iunbakedmodel);
smallLoadingCache.put(resourcelocation, iunbakedmodel);
} finally {
mfix$nestedLoads--;
this.loadingStack.remove(resourcelocation);
}
}
// We have to get the result from the temporary cache used for a model load
// As in pathological cases (e.g. Pedestals on 1.19) unbakedCache can lose
// the model immediately
UnbakedModel result = smallLoadingCache.getOrDefault(modelLocation, iunbakedmodel);
try {
// required as some mods (e.g. EBE) call bake directly on the returned model, without resolving parents themselves
result.resolveParents(this::getModel);
} catch(RuntimeException ignored) {}
// We are done with loading, so clear this cache to allow GC of any unneeded models
if(mfix$nestedLoads == 0)
smallLoadingCache.clear();
cir.setReturnValue(result);
}
}
}
}
@Redirect(method = "loadModel", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/StateDefinition;getPossibleStates()Lcom/google/common/collect/ImmutableList;"))
private ImmutableList<BlockState> loadOnlyRelevantBlockState(StateDefinition<Block, BlockState> stateDefinition, ResourceLocation location) {
var allStates = stateDefinition.getPossibleStates();
if(!(location instanceof ModelResourceLocation mrl)) {
return allStates;
}
// Load a batch of models at once in certain initialization phases to speed up the loading process.
// This is disabled when in-game as it will cause stutters when blocks are placed.
boolean shouldLoadBatch = (Minecraft.getInstance().getOverlay() != null || Minecraft.getInstance().level == null);
int batchSize = ModelBakeryHelpers.MAX_UNBAKED_MODEL_COUNT - 1000;
// If loading a batch and all the states are smaller than the max batch size, just use them
// This is hoisted above the computation of desiredStates for performance reasons
if (shouldLoadBatch && allStates.size() <= batchSize) {
return allStates;
}
var desiredStates = ModelBakeryHelpers.getBlockStatesForMRL(stateDefinition, mrl);
// If not loading a batch, load only the desired states
if (!shouldLoadBatch) {
return desiredStates;
}
// At this point we want to load a batch if possible, but loading every state is too much. If desiredStates
// is a single state (should almost always be the case), then we choose a sublist starting from it and extending
// batchSize entries (or less if the list ends). If it's multiple states, a single sublist may not include
// everything, so we bail.
if (desiredStates.size() != 1) {
return desiredStates;
}
var desiredState = desiredStates.get(0);
int indexInAllStates = allStates.indexOf(desiredState);
if (indexInAllStates == -1) {
return desiredStates;
}
return allStates.subList(indexInAllStates, Math.min(indexInAllStates + batchSize, allStates.size()));
} }
@Override @Override
public ReentrantLock mfix$getLock() { public BakedModel bakeDefault(ResourceLocation modelLocation, ModelState state) {
return this.modelBakeryLock; ModelBakery.BakedCacheKey key = new ModelBakery.BakedCacheKey(modelLocation, BlockModelRotation.X0_Y0.getRotation(), BlockModelRotation.X0_Y0.isUvLocked());
BakedModel m = loadedBakedModels.getIfPresent(key);
if(m != null)
return m;
ModelBakery self = (ModelBakery) (Object) this;
ModelBaker theBaker = self.new ModelBakerImpl(textureGetter, modelLocation);
((IExtendedModelBaker)theBaker).throwOnMissingModel(true);
synchronized(this) {
// We intentionally use the 2-arg overload for better mixin compatibility, because we use the baker's default
// texture getter anyway.
//noinspection deprecation
m = theBaker.bake(modelLocation, state);
}
if(m != null)
loadedBakedModels.put(key, m);
return m;
}
@Override
public ImmutableList<BlockState> getBlockStatesForMRL(StateDefinition<Block, BlockState> stateDefinition, ModelResourceLocation location) {
return loadOnlyRelevantBlockState(stateDefinition, location);
}
private BakedModel bakedMissingModel = null;
public void setBakedMissingModel(BakedModel m) {
bakedMissingModel = m;
}
public BakedModel getBakedMissingModel() {
return bakedMissingModel;
}
public UnbakedModel mfix$getUnbakedMissingModel() {
return missingModel;
}
@Override
public void mfix$clearModels() {
loadedModels.invalidateAll();
loadedBakedModels.invalidateAll();
} }
} }

View File

@ -4,34 +4,27 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.llamalad7.mixinextras.sugar.Local; import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.renderer.block.model.BlockModel; import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.AtlasSet;
import net.minecraft.client.resources.model.BakedModel; import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.BlockStateModelLoader;
import net.minecraft.client.resources.model.ModelBakery; import net.minecraft.client.resources.model.ModelBakery;
import net.minecraft.client.resources.model.ModelManager; import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.Resource; import net.minecraft.server.packs.resources.Resource;
import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.util.GsonHelper; import net.minecraft.util.GsonHelper;
import net.minecraft.util.profiling.ProfilerFiller;
import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.StateDefinition;
import org.embeddedt.modernfix.ModernFix; import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin; import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.IExtendedModelBakery;
import org.embeddedt.modernfix.duck.IExtendedModelManager;
import org.embeddedt.modernfix.util.CacheUtil; import org.embeddedt.modernfix.util.CacheUtil;
import org.embeddedt.modernfix.util.LambdaMap;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Coerce;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
@ -48,12 +41,9 @@ import java.util.stream.Collectors;
@Mixin(ModelManager.class) @Mixin(ModelManager.class)
@ClientOnlyMixin @ClientOnlyMixin
public class ModelManagerMixin implements IExtendedModelManager { public class ModelManagerMixin {
@Shadow private Map<ResourceLocation, BakedModel> bakedRegistry; @Shadow private Map<ResourceLocation, BakedModel> bakedRegistry;
@Unique
private Runnable tickHandler = () -> {};
@Inject(method = "<init>", at = @At("RETURN")) @Inject(method = "<init>", at = @At("RETURN"))
private void injectDummyBakedRegistry(CallbackInfo ci) { private void injectDummyBakedRegistry(CallbackInfo ci) {
if(this.bakedRegistry == null) { if(this.bakedRegistry == null) {
@ -71,10 +61,9 @@ public class ModelManagerMixin implements IExtendedModelManager {
} }
@Redirect(method = "reload", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelManager;loadBlockStates(Lnet/minecraft/server/packs/resources/ResourceManager;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;")) @Redirect(method = "reload", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelManager;loadBlockStates(Lnet/minecraft/server/packs/resources/ResourceManager;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"))
private CompletableFuture<Map<ResourceLocation, List<BlockStateModelLoader.LoadedJson>>> deferBlockStateLoad(ResourceManager manager, Executor executor) { private CompletableFuture<Map<ResourceLocation, List<ModelBakery.LoadedJson>>> deferBlockStateLoad(ResourceManager manager, Executor executor) {
var cache = CacheUtil.<ResourceLocation, List<BlockStateModelLoader.LoadedJson>>simpleCacheForLambda(location -> loadSingleBlockState(manager, location), 100L); var cache = CacheUtil.<ResourceLocation, List<ModelBakery.LoadedJson>>simpleCacheForLambda(location -> loadSingleBlockState(manager, location), 100L);
var blockStateKeys = Set.copyOf(BlockStateModelLoader.BLOCKSTATE_LISTER.listMatchingResourceStacks(manager).keySet()); return CompletableFuture.completedFuture(new LambdaMap<>(location -> cache.getUnchecked(location)));
return CompletableFuture.completedFuture(Maps.asMap(blockStateKeys, location -> cache.getUnchecked(location)));
} }
@Redirect(method = "loadModels", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/StateDefinition;getPossibleStates()Lcom/google/common/collect/ImmutableList;")) @Redirect(method = "loadModels", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/StateDefinition;getPossibleStates()Lcom/google/common/collect/ImmutableList;"))
@ -95,29 +84,14 @@ public class ModelManagerMixin implements IExtendedModelManager {
}).orElse(null); }).orElse(null);
} }
private List<BlockStateModelLoader.LoadedJson> loadSingleBlockState(ResourceManager manager, ResourceLocation location) { private List<ModelBakery.LoadedJson> loadSingleBlockState(ResourceManager manager, ResourceLocation location) {
return manager.getResourceStack(location).stream().map(resource -> { return manager.getResourceStack(location).stream().map(resource -> {
try (BufferedReader reader = resource.openAsReader()) { try (BufferedReader reader = resource.openAsReader()) {
return new BlockStateModelLoader.LoadedJson(resource.sourcePackId(), GsonHelper.parse(reader)); return new ModelBakery.LoadedJson(resource.sourcePackId(), GsonHelper.parse(reader));
} catch(IOException e) { } catch(IOException e) {
ModernFix.LOGGER.error("Couldn't load blockstate", e); ModernFix.LOGGER.error("Couldn't load blockstate", e);
return null; return null;
} }
}).filter(Objects::nonNull).collect(Collectors.toList()); }).filter(Objects::nonNull).collect(Collectors.toList());
} }
@Inject(method = "loadModels", at = @At("RETURN"))
private void storeTicker(ProfilerFiller profilerFiller, Map<ResourceLocation, AtlasSet.StitchResult> map, ModelBakery modelBakery, CallbackInfoReturnable<?> cir) {
tickHandler = ((IExtendedModelBakery)modelBakery)::mfix$tick;
}
@Inject(method = "apply", at = @At("RETURN"))
private void freezeBakery(@Coerce Object reloadState, ProfilerFiller profilerFiller, CallbackInfo ci, @Local(ordinal = 0) ModelBakery bakery) {
((IExtendedModelBakery)bakery).mfix$finishLoading();
}
@Override
public void mfix$tick() {
tickHandler.run();
}
} }

View File

@ -3,12 +3,15 @@ package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources.ctm;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.mojang.datafixers.util.Pair; import com.mojang.datafixers.util.Pair;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.*; import net.minecraft.client.resources.model.*;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import org.embeddedt.modernfix.ModernFixClient; import org.embeddedt.modernfix.ModernFixClient;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin; import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.annotation.RequiresMod; import org.embeddedt.modernfix.annotation.RequiresMod;
import org.embeddedt.modernfix.api.entrypoint.ModernFixClientIntegration; import org.embeddedt.modernfix.api.entrypoint.ModernFixClientIntegration;
import org.embeddedt.modernfix.forge.dynresources.IModelBakerImpl;
import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
@ -26,6 +29,7 @@ import team.chisel.ctm.client.util.TextureMetadataHandler;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.function.Function;
@Mixin(TextureMetadataHandler.class) @Mixin(TextureMetadataHandler.class)
@RequiresMod("ctm") @RequiresMod("ctm")
@ -41,18 +45,18 @@ public abstract class TextureMetadataHandlerMixin implements ModernFixClientInte
ModernFixClient.CLIENT_INTEGRATIONS.add(this); ModernFixClient.CLIENT_INTEGRATIONS.add(this);
} }
@Inject(method = { "onModelBake(Lnet/neoforged/neoforge/client/event/ModelEvent$BakingCompleted;)V", "onModelBake(Lnet/neoforged/neoforge/client/event/ModelEvent$ModifyBakingResult;)V" }, at = @At("HEAD"), cancellable = true, remap = false) @Inject(method = { "onModelBake(Lnet/minecraftforge/client/event/ModelEvent$ModifyBakingResult;)V", "onModelBake(Lnet/minecraftforge/client/event/ModelEvent$BakingCompleted;)V" }, at = @At("HEAD"), cancellable = true, remap = false)
private void noIteration(CallbackInfo ci) { private void noIteration(CallbackInfo ci) {
ci.cancel(); ci.cancel();
} }
@Override @Override
public BakedModel onBakedModelLoad(ModelResourceLocation mrl, UnbakedModel rootModel, BakedModel baked, ModelState state, ModelBakery bakery, ModelBakery.TextureGetter getter) { public BakedModel onBakedModelLoad(ResourceLocation rl, UnbakedModel rootModel, BakedModel baked, ModelState state, ModelBakery bakery) {
if (!(baked instanceof AbstractCTMBakedModel) && !baked.isCustomRenderer()) { if (rl instanceof ModelResourceLocation && !(baked instanceof AbstractCTMBakedModel) && !baked.isCustomRenderer()) {
Deque<ResourceLocation> dependencies = new ArrayDeque<>(); Deque<ResourceLocation> dependencies = new ArrayDeque<>();
Set<ResourceLocation> seenModels = new HashSet<>(); Set<ResourceLocation> seenModels = new HashSet<>();
dependencies.push(mrl.id()); dependencies.push(rl);
seenModels.add(mrl.id()); seenModels.add(rl);
boolean shouldWrap = false; boolean shouldWrap = false;
Set<Pair<String, String>> errors = new HashSet<>(); Set<Pair<String, String>> errors = new HashSet<>();
// Breadth-first loop through dependencies, exiting as soon as a CTM texture is found, and skipping duplicates/cycles // Breadth-first loop through dependencies, exiting as soon as a CTM texture is found, and skipping duplicates/cycles
@ -60,7 +64,7 @@ public abstract class TextureMetadataHandlerMixin implements ModernFixClientInte
ResourceLocation dep = dependencies.pop(); ResourceLocation dep = dependencies.pop();
UnbakedModel model; UnbakedModel model;
try { try {
model = dep == mrl.id() ? rootModel : bakery.getModel(dep); model = dep == rl ? rootModel : bakery.getModel(dep);
} catch (Exception e) { } catch (Exception e) {
continue; continue;
} }
@ -88,25 +92,32 @@ public abstract class TextureMetadataHandlerMixin implements ModernFixClientInte
if (shouldWrap) { if (shouldWrap) {
try { try {
baked = wrap(rootModel, baked); baked = wrap(rootModel, baked);
handleInit(mrl, baked, bakery, getter); handleInit(rl, baked, bakery);
dependencies.clear(); dependencies.clear();
} catch (IOException e) { } catch (IOException e) {
CTM.logger.error("Could not wrap model " + mrl + ". Aborting...", e); CTM.logger.error("Could not wrap model " + rl + ". Aborting...", e);
} }
} }
} }
return baked; return baked;
} }
private void handleInit(ModelResourceLocation key, BakedModel wrappedModel, ModelBakery bakery, ModelBakery.TextureGetter spriteGetter) { private void handleInit(ResourceLocation key, BakedModel wrappedModel, ModelBakery bakery) {
if(wrappedModel instanceof AbstractCTMBakedModel baked) { if(wrappedModel instanceof AbstractCTMBakedModel baked) {
IModelCTM var10 = baked.getModel(); IModelCTM var10 = baked.getModel();
if (var10 instanceof ModelCTM ctmModel) { if (var10 instanceof ModelCTM ctmModel) {
if (!ctmModel.isInitialized()) { if (!ctmModel.isInitialized()) {
// Clear the baked cache as upstream CTM does // Clear the baked cache as upstream CTM does
((CTMModelBakeryAccessor)bakery).mfix$getBakedCache().clear(); ((CTMModelBakeryAccessor)bakery).mfix$getBakedCache().clear();
ModelBakery.ModelBakerImpl baker = bakery.new ModelBakerImpl(spriteGetter, key); Function<Material, TextureAtlasSprite> spriteGetter = (m) -> {
ctmModel.bake(baker, m -> spriteGetter.get(key, m), BlockModelRotation.X0_Y0); return Minecraft.getInstance().getModelManager().getAtlas(m.atlasLocation()).getSprite(m.texture());
};
ModelBakery.ModelBakerImpl baker = bakery.new ModelBakerImpl(($, m) -> {
return spriteGetter.apply(m);
}, key);
// bypass bakedCache so that dependent models get re-baked and thus retrieve their sprites again
((IModelBakerImpl)baker).mfix$ignoreCache();
ctmModel.bake(baker, spriteGetter, BlockModelRotation.X0_Y0, key);
} }
} }
} }

View File

@ -2,9 +2,11 @@ package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources.ldlib;
import com.lowdragmc.lowdraglib.LDLib; import com.lowdragmc.lowdraglib.LDLib;
import com.lowdragmc.lowdraglib.client.ClientProxy; import com.lowdragmc.lowdraglib.client.ClientProxy;
import com.lowdragmc.lowdraglib.client.model.custommodel.CustomBakedModel; import com.lowdragmc.lowdraglib.client.forge.ClientProxyImpl;
import com.lowdragmc.lowdraglib.client.model.custommodel.LDLMetadataSection; import com.lowdragmc.lowdraglib.client.model.custommodel.LDLMetadataSection;
import com.lowdragmc.lowdraglib.client.model.forge.CustomBakedModelImpl;
import com.lowdragmc.lowdraglib.client.model.forge.LDLRendererModel; import com.lowdragmc.lowdraglib.client.model.forge.LDLRendererModel;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.BakedModel; import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.Material; import net.minecraft.client.resources.model.Material;
import net.minecraft.client.resources.model.ModelBakery; import net.minecraft.client.resources.model.ModelBakery;
@ -27,8 +29,9 @@ import java.util.Deque;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
@Mixin(ClientProxy.class) @Mixin(ClientProxyImpl.class)
@ClientOnlyMixin @ClientOnlyMixin
@RequiresMod("ldlib") @RequiresMod("ldlib")
public abstract class ClientProxyImplMixin implements ModernFixClientIntegration { public abstract class ClientProxyImplMixin implements ModernFixClientIntegration {
@ -43,11 +46,11 @@ public abstract class ClientProxyImplMixin implements ModernFixClientIntegration
} }
@Override @Override
public BakedModel onBakedModelLoad(ModelResourceLocation mrl, UnbakedModel rootModel, BakedModel baked, ModelState state, ModelBakery bakery, ModelBakery.TextureGetter textureGetter) { public BakedModel onBakedModelLoad(ResourceLocation rl, UnbakedModel rootModel, BakedModel baked, ModelState state, ModelBakery bakery, Function<Material, TextureAtlasSprite> textureGetter) {
if (baked == null) { if (baked == null) {
return null; return null;
} }
if (rootModel != null) { if (rl instanceof ModelResourceLocation && rootModel != null) {
if (baked instanceof LDLRendererModel) { if (baked instanceof LDLRendererModel) {
return baked; return baked;
} }
@ -56,10 +59,9 @@ public abstract class ClientProxyImplMixin implements ModernFixClientIntegration
} }
Deque<ResourceLocation> dependencies = new ArrayDeque<>(); Deque<ResourceLocation> dependencies = new ArrayDeque<>();
Set<ResourceLocation> seenModels = new HashSet<>(); Set<ResourceLocation> seenModels = new HashSet<>();
ResourceLocation rl = mrl.id();
dependencies.push(rl); dependencies.push(rl);
seenModels.add(rl); seenModels.add(rl);
boolean shouldWrap = ClientProxy.WRAPPED_MODELS.getOrDefault(mrl, false); boolean shouldWrap = ClientProxy.WRAPPED_MODELS.getOrDefault(rl, false);
// Breadth-first loop through dependencies, exiting as soon as a CTM texture is found, and skipping duplicates/cycles // Breadth-first loop through dependencies, exiting as soon as a CTM texture is found, and skipping duplicates/cycles
while (!shouldWrap && !dependencies.isEmpty()) { while (!shouldWrap && !dependencies.isEmpty()) {
ResourceLocation dep = dependencies.pop(); ResourceLocation dep = dependencies.pop();
@ -90,9 +92,9 @@ public abstract class ClientProxyImplMixin implements ModernFixClientIntegration
LDLib.LOGGER.error("Error loading baked dependency {} for baked {}. Skipping...", dep, rl, e); LDLib.LOGGER.error("Error loading baked dependency {} for baked {}. Skipping...", dep, rl, e);
} }
} }
ClientProxy.WRAPPED_MODELS.put(mrl, shouldWrap); ClientProxy.WRAPPED_MODELS.put(rl, shouldWrap);
if (shouldWrap) { if (shouldWrap) {
return new CustomBakedModel<>(baked); return new CustomBakedModelImpl(baked);
} }
} }
return baked; return baked;

View File

@ -0,0 +1,64 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources.supermartijncore;
import com.supermartijn642.core.registry.ClientRegistrationHandler;
import com.supermartijn642.core.util.Pair;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelBakery;
import net.minecraft.client.resources.model.ModelState;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.resources.ResourceLocation;
import org.embeddedt.modernfix.ModernFixClient;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.annotation.RequiresMod;
import org.embeddedt.modernfix.api.entrypoint.ModernFixClientIntegration;
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.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
@Mixin(ClientRegistrationHandler.class)
@RequiresMod("supermartijn642corelib")
@ClientOnlyMixin
public class ClientRegistrationHandlerMixin {
@Shadow(remap = false) @Final private List<Pair<Supplier<Stream<ResourceLocation>>, Function<BakedModel, BakedModel>>> modelOverwrites;
private Map<ResourceLocation, Function<BakedModel, BakedModel>> modelOverwritesByLocation = new Object2ObjectOpenHashMap<>();
@Redirect(method = "handleModelBakeEvent", at = @At(value = "FIELD", target = "Lcom/supermartijn642/core/registry/ClientRegistrationHandler;modelOverwrites:Ljava/util/List;"), remap = false)
private List<?> skipModelOverwrites(ClientRegistrationHandler h) {
modelOverwritesByLocation.clear();
for(Pair<Supplier<Stream<ResourceLocation>>, Function<BakedModel, BakedModel>> pair : this.modelOverwrites) {
Stream<ResourceLocation> locationStream = pair.left().get();
Function<BakedModel, BakedModel> swapper = pair.right();
locationStream.forEach(l -> {
modelOverwritesByLocation.put(l, swapper);
});
}
return Collections.emptyList();
}
@Inject(method = "<init>", at = @At("RETURN"))
private void registerDynBake(String modid, CallbackInfo ci) {
ModernFixClient.CLIENT_INTEGRATIONS.add(new ModernFixClientIntegration() {
@Override
public BakedModel onBakedModelLoad(ResourceLocation location, UnbakedModel baseModel, BakedModel originalModel, ModelState state, ModelBakery bakery) {
Function<BakedModel, BakedModel> replacer = modelOverwritesByLocation.get(location);
if(replacer != null)
return replacer.apply(originalModel);
else
return originalModel;
}
});
}
}

View File

@ -1,21 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.encoder_cache_leak;
import net.minecraft.client.multiplayer.ClientConfigurationPacketListenerImpl;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
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.CallbackInfo;
@Mixin(ClientConfigurationPacketListenerImpl.class)
@ClientOnlyMixin
public class ClientConfigurationPacketListenerImplMixin {
/**
* @author embeddedt
* @reason Reset the encoder cache after configuration finishes as the registries are now changing.
*/
@Inject(method = "handleConfigurationFinished", at = @At("RETURN"))
private void resetEncoderCache(CallbackInfo ci) {
((EncoderCacheAccessor)DataComponentsAccessor.mfix$getCache()).mfix$getCache().invalidateAll();
}
}

View File

@ -1,14 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.encoder_cache_leak;
import net.minecraft.core.component.DataComponents;
import net.minecraft.util.EncoderCache;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(DataComponents.class)
public interface DataComponentsAccessor {
@Accessor("ENCODER_CACHE")
static EncoderCache mfix$getCache() {
throw new AssertionError();
}
}

View File

@ -1,12 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.encoder_cache_leak;
import com.google.common.cache.LoadingCache;
import net.minecraft.util.EncoderCache;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(EncoderCache.class)
public interface EncoderCacheAccessor {
@Accessor("cache")
LoadingCache<?, ?> mfix$getCache();
}

View File

@ -1,25 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.encoder_cache_leak;
import net.minecraft.client.Minecraft;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
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.CallbackInfo;
@Mixin(Minecraft.class)
@ClientOnlyMixin
public class MinecraftMixin {
/**
* @author embeddedt
* @reason Make sure the encoder cache is cleared when the client disconnects, as it retains strong references
* to registries.
*/
@Inject(method = {
"disconnect(Lnet/minecraft/client/gui/screens/Screen;Z)V",
"clearClientLevel(Lnet/minecraft/client/gui/screens/Screen;)V"
}, at = @At("RETURN"))
private void clearEncoderCache(CallbackInfo ci) {
((EncoderCacheAccessor)DataComponentsAccessor.mfix$getCache()).mfix$getCache().invalidateAll();
}
}

View File

@ -1,27 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.encoder_cache_leak;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import net.minecraft.core.component.DataComponents;
import net.minecraft.server.ReloadableServerResources;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import java.util.concurrent.CompletableFuture;
@Mixin(ReloadableServerResources.class)
public class ReloadableServerResourcesMixin {
/**
* @author embeddedt
* @reason Some mods (e.g. KubeJS) may provide a custom DynamicOps instance during resource reload. This instance
* can end up being strongly retained by an EncoderCache.Key entry even after the reload finishes. The simplest
* fix is to invalidate all entries of the encoder cache after a server-side resource reload, which should not break
* mods, as the cache is not guaranteed to persist entries for any length of time due to using both a maximum size
* & soft values.
*/
@ModifyReturnValue(method = "loadResources", at = @At("RETURN"))
private static CompletableFuture<ReloadableServerResources> resetEncoderCache(CompletableFuture<ReloadableServerResources> future) {
return future.whenComplete((r, t) -> {
((EncoderCacheAccessor)DataComponentsAccessor.mfix$getCache()).mfix$getCache().invalidateAll();
});
}
}

View File

@ -0,0 +1,43 @@
package org.embeddedt.modernfix.common.mixin.perf.fast_forge_dummies;
import com.mojang.serialization.Lifecycle;
import net.minecraft.core.Holder;
import net.minecraft.core.MappedRegistry;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import org.objectweb.asm.Opcodes;
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.CallbackInfoReturnable;
import java.util.Map;
@Mixin(targets = { "net/minecraftforge/registries/NamespacedWrapper" })
public abstract class NamespacedHolderHelperMixin<T> extends MappedRegistry<T> {
@Shadow(remap = false) private Map<ResourceLocation, Holder.Reference<T>> holdersByName;
public NamespacedHolderHelperMixin(ResourceKey<? extends Registry<T>> arg, Lifecycle lifecycle) {
super(arg, lifecycle);
}
@Inject(method = "freeze", at = @At(value = "FIELD", opcode = Opcodes.GETFIELD, target = "Lnet/minecraftforge/registries/NamespacedWrapper;holdersByName:Ljava/util/Map;", remap = false), cancellable = true)
private void fastDummyCheck(CallbackInfoReturnable<Registry<T>> cir) {
// Quickly iterate without making any streams, etc. to see if everything is fine
// Use the slow path (by returning without cancelling) when there is an error
for(Holder.Reference<T> ref : this.holdersByName.values()) {
if(!ref.isBound())
return;
}
if (this.unregisteredIntrusiveHolders != null) {
for(Holder.Reference<T> ref : this.unregisteredIntrusiveHolders.values()) {
if(ref.getType() == Holder.Reference.Type.INTRUSIVE && !ref.isBound())
return;
}
}
// Skip the creation of streams
cir.setReturnValue(this);
}
}

View File

@ -0,0 +1,37 @@
package org.embeddedt.modernfix.common.mixin.perf.fast_registry_validation;
import net.minecraftforge.registries.ForgeRegistry;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
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.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.*;
@Mixin(value = ForgeRegistry.class, remap = false)
public class ForgeRegistryMixin<V> {
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;
}
@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) {
}
}

View File

@ -0,0 +1,42 @@
package org.embeddedt.modernfix.common.mixin.perf.faster_capabilities;
import net.minecraft.core.Direction;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.CapabilityDispatcher;
import net.minecraftforge.common.capabilities.CapabilityProvider;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
import net.minecraftforge.common.util.LazyOptional;
import org.embeddedt.modernfix.forge.capability.CapabilityProviderDispatcherGenerator;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
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.List;
import java.util.Map;
@Mixin(CapabilityDispatcher.class)
public class CapabilityDispatcherMixin {
@Shadow
@Final
private ICapabilityProvider[] caps;
private ICapabilityProvider mfix$turboDispatcher;
@Inject(method = "<init>(Ljava/util/Map;Ljava/util/List;Lnet/minecraftforge/common/capabilities/ICapabilityProvider;)V", at = @At("RETURN"))
private void createTurboDispatcher(Map list, List listeners, ICapabilityProvider parent, CallbackInfo ci) {
this.mfix$turboDispatcher = CapabilityProviderDispatcherGenerator.getOrGenerateDispatcher(this.caps);
}
/**
* @author embeddedt
* @reason use ASM-generated dispatcher
*/
@Overwrite(remap = false)
public <T> LazyOptional<T> getCapability(Capability<T> cap, @Nullable Direction side) {
return this.mfix$turboDispatcher.getCapability(cap, side);
}
}

View File

@ -0,0 +1,59 @@
package org.embeddedt.modernfix.common.mixin.perf.faster_capabilities.bytecode_analysis;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
import net.minecraftforge.event.AttachCapabilitiesEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.eventbus.api.EventPriority;
import org.embeddedt.modernfix.duck.IBatchingCapEvent;
import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalysisResult;
import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalyzer;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
@Mixin(AttachCapabilitiesEvent.class)
public abstract class AttachCapabilitiesEventMixin extends Event implements IBatchingCapEvent {
@Shadow @Final
private Map<ResourceLocation, ICapabilityProvider> caps;
@Unique
private final Map<ResourceLocation, EventPriority> mfix$phaseMap = new HashMap<>();
/**
* @author embeddedt
* @reason record the current dispatch phase so we can sort within phase groups later
*/
@WrapMethod(method = "addCapability", remap = false)
private void mfix$trackPhase(ResourceLocation key, ICapabilityProvider cap, Operation<Void> original) {
original.call(key, cap);
mfix$phaseMap.put(key, this.getPhase());
}
@Override
public void mfix$sortCaps() {
if (caps.size() < 2) {
return;
}
var entries = new ArrayList<>(caps.entrySet());
entries.sort(Comparator.comparingInt(e -> {
EventPriority phase = mfix$phaseMap.getOrDefault(e.getKey(), EventPriority.NORMAL);
var result = CapabilityAnalyzer.analyze(e.getValue().getClass());
// Primary: preserve phase ordering (HIGHEST=0 .. LOWEST=4)
// Secondary: Known/AlwaysEmpty before Indeterminate within each phase
int capKey = result instanceof CapabilityAnalysisResult.Indeterminate ? 1 : 0;
return phase.ordinal() * 2 + capKey;
}));
caps.clear();
entries.forEach(e -> caps.put(e.getKey(), e.getValue()));
mfix$phaseMap.clear();
}
}

View File

@ -0,0 +1,20 @@
package org.embeddedt.modernfix.common.mixin.perf.faster_capabilities.bytecode_analysis;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraftforge.event.ForgeEventFactory;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.eventbus.api.IEventBus;
import org.embeddedt.modernfix.duck.IBatchingCapEvent;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(value = ForgeEventFactory.class, remap = false)
public class ForgeEventFactoryMixin {
@WrapOperation(method = "gatherCapabilities(Lnet/minecraftforge/event/AttachCapabilitiesEvent;Lnet/minecraftforge/common/capabilities/ICapabilityProvider;)Lnet/minecraftforge/common/capabilities/CapabilityDispatcher;", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/eventbus/api/IEventBus;post(Lnet/minecraftforge/eventbus/api/Event;)Z"))
private static boolean modernfix$sortAfterPost(IEventBus instance, Event event, Operation<Boolean> original) {
boolean result = original.call(instance, event);
((IBatchingCapEvent) event).mfix$sortCaps();
return result;
}
}

View File

@ -0,0 +1,45 @@
package org.embeddedt.modernfix.common.mixin.perf.faster_ingredients;
import cofh.lib.util.crafting.IngredientWithCount;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.Ingredient;
import org.embeddedt.modernfix.annotation.RequiresMod;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import java.util.stream.Stream;
@Mixin(IngredientWithCount.class)
@RequiresMod("cofh_core")
public class CofhIngredientWithCountMixin extends Ingredient {
@Shadow @Final private Ingredient wrappedIngredient;
@Shadow @Final private int count;
@Unique
private ItemStack[] mfix$itemStacksWithAdjustedCount;
protected CofhIngredientWithCountMixin(Stream<? extends Value> values) {
super(values);
}
/**
* @author embeddedt
* @reason reimplement in a way that doesn't rely on the itemStacks implementation detail of the wrapped ingredient
*/
@Overwrite
public ItemStack[] getItems() {
if (this.mfix$itemStacksWithAdjustedCount == null) {
ItemStack[] originalItems = this.wrappedIngredient.getItems();
var newItems = new ItemStack[originalItems.length];
for (int i = 0; i < originalItems.length; i++) {
newItems[i] = originalItems[i].copy();
newItems[i].setCount(this.count);
}
this.mfix$itemStacksWithAdjustedCount = newItems;
}
return this.mfix$itemStacksWithAdjustedCount;
}
}

View File

@ -0,0 +1,23 @@
package org.embeddedt.modernfix.common.mixin.perf.faster_ingredients;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraftforge.common.ForgeHooks;
import org.embeddedt.modernfix.forge.recipe.ExtendedIngredient;
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;
@Mixin(value = ForgeHooks.class, priority = 900)
public class ForgeHooksMixin {
/**
* @author embeddedt
* @reason Exploding the stack list is entirely unnecessary to compute this
*/
@Inject(method = "hasNoElements", at = @At("HEAD"), cancellable = true, remap = false)
private static void modernfix$fastHasNoElements(Ingredient ingredient, CallbackInfoReturnable<Boolean> cir) {
if (ingredient.isVanilla()) {
cir.setReturnValue(((ExtendedIngredient)ingredient).mfix$hasNoElements());
}
}
}

View File

@ -5,12 +5,10 @@ import it.unimi.dsi.fastutil.ints.IntComparators;
import it.unimi.dsi.fastutil.ints.IntList; import it.unimi.dsi.fastutil.ints.IntList;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.ItemStackLinkedSet;
import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.item.crafting.Ingredient;
import net.neoforged.neoforge.common.crafting.ICustomIngredient; import org.embeddedt.modernfix.forge.load.MinecraftServerReloadTracker;
import org.embeddedt.modernfix.neoforge.load.MinecraftServerReloadTracker; import org.embeddedt.modernfix.forge.recipe.ExtendedIngredient;
import org.embeddedt.modernfix.neoforge.recipe.ExtendedIngredient; import org.embeddedt.modernfix.forge.recipe.IngredientItemStacksSoftReference;
import org.embeddedt.modernfix.neoforge.recipe.IngredientItemStacksSoftReference;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
@ -19,13 +17,16 @@ import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.stream.Collectors;
@Mixin(value = Ingredient.class, priority = 700) @Mixin(value = Ingredient.class, priority = 700)
public abstract class IngredientMixin implements ExtendedIngredient { public abstract class IngredientMixin implements ExtendedIngredient {
@Shadow
public abstract boolean isVanilla();
@Shadow @Final @Shadow @Final
private Ingredient.Value[] values; private Ingredient.Value[] values;
@ -33,15 +34,6 @@ public abstract class IngredientMixin implements ExtendedIngredient {
@Shadow @Nullable private ItemStack[] itemStacks; @Shadow @Nullable private ItemStack[] itemStacks;
@Shadow public abstract boolean isCustom();
@Shadow private ICustomIngredient customIngredient;
@Unique
private boolean isVanilla() {
return !this.isCustom();
}
private volatile IngredientItemStacksSoftReference mfix$cachedItemStacks; private volatile IngredientItemStacksSoftReference mfix$cachedItemStacks;
/** /**
@ -70,19 +62,13 @@ public abstract class IngredientMixin implements ExtendedIngredient {
@Inject(method = "test(Lnet/minecraft/world/item/ItemStack;)Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/crafting/Ingredient;getItems()[Lnet/minecraft/world/item/ItemStack;"), cancellable = true) @Inject(method = "test(Lnet/minecraft/world/item/ItemStack;)Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/crafting/Ingredient;getItems()[Lnet/minecraft/world/item/ItemStack;"), cancellable = true)
private void modernfix$fasterTagIngredientTest(ItemStack stack, CallbackInfoReturnable<Boolean> cir) { private void modernfix$fasterTagIngredientTest(ItemStack stack, CallbackInfoReturnable<Boolean> cir) {
if (this.isVanilla() && this.values.length == 1 && this.values[0] instanceof Ingredient.TagValue tagValue && mfix$areTagsAvailable()) { if (this.isVanilla() && this.values.length == 1 && this.values[0] instanceof Ingredient.TagValue tagValue && mfix$areTagsAvailable()) {
cir.setReturnValue(stack.getItemHolder().is(tagValue.tag())); cir.setReturnValue(stack.getItemHolder().is(tagValue.tag));
} }
} }
/** @Override
* @author embeddedt public boolean mfix$hasNoElements() {
* @reason exploding the stack list is unnecessary return !this.containsItems();
*/
@Inject(method = "hasNoItems", at = @At("HEAD"), cancellable = true, remap = false)
public void hasNoItems(CallbackInfoReturnable<Boolean> cir) {
if (this.isVanilla()) {
cir.setReturnValue(!this.containsItems());
}
} }
@Unique @Unique
@ -96,7 +82,7 @@ public abstract class IngredientMixin implements ExtendedIngredient {
if (value instanceof Ingredient.ItemValue) { if (value instanceof Ingredient.ItemValue) {
return true; return true;
} else if (value instanceof Ingredient.TagValue tagValue && mfix$areTagsAvailable()) { } else if (value instanceof Ingredient.TagValue tagValue && mfix$areTagsAvailable()) {
var holderSetOpt = BuiltInRegistries.ITEM.getTag(tagValue.tag()); var holderSetOpt = BuiltInRegistries.ITEM.getTag(tagValue.tag);
if (holderSetOpt.isPresent() && holderSetOpt.get().size() > 0) { if (holderSetOpt.isPresent() && holderSetOpt.get().size() > 0) {
return true; return true;
} }
@ -120,7 +106,7 @@ public abstract class IngredientMixin implements ExtendedIngredient {
@Inject(method = "getStackingIds", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/crafting/Ingredient;getItems()[Lnet/minecraft/world/item/ItemStack;"), cancellable = true) @Inject(method = "getStackingIds", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/crafting/Ingredient;getItems()[Lnet/minecraft/world/item/ItemStack;"), cancellable = true)
private void modernfix$fasterTagIngredientStacking(CallbackInfoReturnable<IntList> cir) { private void modernfix$fasterTagIngredientStacking(CallbackInfoReturnable<IntList> cir) {
if (this.isVanilla() && this.values.length == 1 && this.values[0] instanceof Ingredient.TagValue tagValue && mfix$areTagsAvailable()) { if (this.isVanilla() && this.values.length == 1 && this.values[0] instanceof Ingredient.TagValue tagValue && mfix$areTagsAvailable()) {
var tag = BuiltInRegistries.ITEM.getTag(tagValue.tag()); var tag = BuiltInRegistries.ITEM.getTag(tagValue.tag);
if (!tag.isPresent() || tag.get().size() == 0) { if (!tag.isPresent() || tag.get().size() == 0) {
return; return;
} }
@ -142,11 +128,6 @@ public abstract class IngredientMixin implements ExtendedIngredient {
if (this.itemStacks != null) { if (this.itemStacks != null) {
return this.itemStacks; return this.itemStacks;
} }
if (this.customIngredient != null) {
// We probably have to cache this as mods won't make it fast if they expect Neo to cache it
this.itemStacks = this.customIngredient.getItems().collect(Collectors.toCollection(ItemStackLinkedSet::createTypeAndComponentsSet)).toArray(ItemStack[]::new);
return this.itemStacks;
}
var cache = this.mfix$cachedItemStacks; var cache = this.mfix$cachedItemStacks;
if (cache != null) { if (cache != null) {
var stacks = cache.get(); var stacks = cache.get();
@ -164,7 +145,7 @@ public abstract class IngredientMixin implements ExtendedIngredient {
// Fast path for case with one item // Fast path for case with one item
if (this.values.length == 1) { if (this.values.length == 1) {
if (this.values[0] instanceof Ingredient.TagValue tagValue && mfix$areTagsAvailable()) { if (this.values[0] instanceof Ingredient.TagValue tagValue && mfix$areTagsAvailable()) {
var tag = BuiltInRegistries.ITEM.getTag(tagValue.tag()); var tag = BuiltInRegistries.ITEM.getTag(tagValue.tag);
if (tag.isPresent() && tag.get().size() > 0) { if (tag.isPresent() && tag.get().size() > 0) {
var holderSet = tag.get(); var holderSet = tag.get();
ItemStack[] result = new ItemStack[holderSet.size()]; ItemStack[] result = new ItemStack[holderSet.size()];
@ -190,4 +171,9 @@ public abstract class IngredientMixin implements ExtendedIngredient {
public void mfix$clearReference() { public void mfix$clearReference() {
this.mfix$cachedItemStacks = null; this.mfix$cachedItemStacks = null;
} }
@Inject(method = "invalidate", at = @At("RETURN"), remap = false)
private void invalidateSoftReference(CallbackInfo ci) {
mfix$clearReference();
}
} }

View File

@ -11,12 +11,12 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(GameRenderer.class) @Mixin(GameRenderer.class)
@ClientOnlyMixin @ClientOnlyMixin
public class GameRendererMixin { public class GameRendererMixin {
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;renderLevel(Lnet/minecraft/client/DeltaTracker;)V", shift = At.Shift.BEFORE)) @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;renderLevel(FJLcom/mojang/blaze3d/vertex/PoseStack;)V", shift = At.Shift.BEFORE))
private void markRenderingLevel(CallbackInfo ci) { private void markRenderingLevel(CallbackInfo ci) {
RenderState.IS_RENDERING_LEVEL = true; RenderState.IS_RENDERING_LEVEL = true;
} }
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;renderLevel(Lnet/minecraft/client/DeltaTracker;)V", shift = At.Shift.AFTER)) @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;renderLevel(FJLcom/mojang/blaze3d/vertex/PoseStack;)V", shift = At.Shift.AFTER))
private void markNotRenderingLevel(CallbackInfo ci) { private void markNotRenderingLevel(CallbackInfo ci) {
RenderState.IS_RENDERING_LEVEL = false; RenderState.IS_RENDERING_LEVEL = false;
} }

View File

@ -0,0 +1,57 @@
package org.embeddedt.modernfix.common.mixin.perf.faster_loot_loading;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraftforge.common.ForgeHooks;
import org.apache.commons.lang3.function.TriFunction;
import org.apache.logging.log4j.Logger;
import org.embeddedt.modernfix.annotation.FeatureLevel;
import org.embeddedt.modernfix.annotation.RequiresFeatureLevel;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import java.util.Optional;
import static net.minecraftforge.common.ForgeHooks.loadLootTable;
@Mixin(value = ForgeHooks.class, remap = false)
@RequiresFeatureLevel(FeatureLevel.BETA)
public class ForgeHooksMixin {
@Shadow
@Final
private static Logger LOGGER;
private static boolean mfix$isVanillaTable(JsonElement data) {
if (!(data instanceof JsonObject obj)) {
return false;
}
var vanillaMarker = obj.getAsJsonPrimitive("mfix$isVanillaTable");
if (vanillaMarker == null) {
return false;
}
return vanillaMarker.getAsBoolean();
}
/**
* @author embeddedt
* @reason avoid getResource() call per loot table by using injected marker
*/
@Overwrite
public static TriFunction<ResourceLocation, JsonElement, ResourceManager, Optional<LootTable>> getLootTableDeserializer(Gson gson, String directory) {
return (location, data, resourceManager) -> {
try {
boolean custom = !mfix$isVanillaTable(data);
return Optional.ofNullable(loadLootTable(gson, location, data, custom));
} catch (Exception exception) {
LOGGER.error("Couldn't parse element {}:{}", directory, location, exception);
return Optional.empty();
}
};
}
}

View File

@ -0,0 +1,41 @@
package org.embeddedt.modernfix.common.mixin.perf.faster_loot_loading;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.resources.FileToIdConverter;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.world.level.storage.loot.LootDataManager;
import net.minecraft.world.level.storage.loot.LootDataType;
import org.embeddedt.modernfix.annotation.FeatureLevel;
import org.embeddedt.modernfix.annotation.RequiresFeatureLevel;
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.CallbackInfo;
import java.util.Map;
@Mixin(LootDataManager.class)
@RequiresFeatureLevel(FeatureLevel.BETA)
public class LootDataManagerMixin {
/**
* @author embeddedt
* @reason inject a marker for vanilla loot tables into the JSON so that we can retrieve it from the deserializer
*/
@Inject(method = "lambda$scheduleElementParse$5", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/packs/resources/SimpleJsonResourceReloadListener;scanDirectory(Lnet/minecraft/server/packs/resources/ResourceManager;Ljava/lang/String;Lcom/google/gson/Gson;Ljava/util/Map;)V", shift = At.Shift.AFTER))
private static void mfix$scanAndCapture(ResourceManager resourceManager, LootDataType lootDataType, Map map, CallbackInfo ci,
@Local(ordinal = 1) Map<ResourceLocation, JsonElement> lootTables) {
FileToIdConverter converter = FileToIdConverter.json(lootDataType.directory());
var lootTableResourceMap = converter.listMatchingResources(resourceManager);
for (var entry : lootTableResourceMap.entrySet()) {
if (lootTables.get(converter.fileToId(entry.getKey())) instanceof JsonObject obj) {
var resource = entry.getValue();
if (resource != null && !resource.isBuiltin()) {
obj.addProperty("mfix$isVanillaTable", true);
}
}
}
}
}

View File

@ -0,0 +1,22 @@
package org.embeddedt.modernfix.common.mixin.perf.faster_structure_location;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.levelgen.structure.StructureCheck;
import org.embeddedt.modernfix.duck.IStructureCheck;
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;
@Mixin(ServerLevel.class)
public class ServerLevelMixin {
@Shadow @Final private ServerChunkCache chunkSource;
@ModifyExpressionValue(method = "<init>", at = @At(value = "NEW", target = "(Lnet/minecraft/world/level/chunk/storage/ChunkScanAccess;Lnet/minecraft/core/RegistryAccess;Lnet/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager;Lnet/minecraft/resources/ResourceKey;Lnet/minecraft/world/level/chunk/ChunkGenerator;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/LevelHeightAccessor;Lnet/minecraft/world/level/biome/BiomeSource;JLcom/mojang/datafixers/DataFixer;)Lnet/minecraft/world/level/levelgen/structure/StructureCheck;", ordinal = 0))
private StructureCheck attachGeneratorState(StructureCheck check) {
((IStructureCheck)check).mfix$setStructureState(this.chunkSource.getGeneratorState());
return check;
}
}

View File

@ -0,0 +1,51 @@
package org.embeddedt.modernfix.common.mixin.perf.faster_structure_location;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.core.Registry;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureCheck;
import net.minecraft.world.level.levelgen.structure.StructureCheckResult;
import org.embeddedt.modernfix.duck.IStructureCheck;
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;
@Mixin(StructureCheck.class)
public class StructureCheckMixin implements IStructureCheck {
@Shadow @Final private Registry<Structure> structureConfigs;
private ChunkGeneratorStructureState mfix$structureState;
@Override
public void mfix$setStructureState(ChunkGeneratorStructureState state) {
mfix$structureState = state;
}
/**
* @author embeddedt (inspired by 24w04a and Bytzo's comment on https://bugs.mojang.com/browse/MC-249136)
* @reason Avoid running the canCreateStructure method (which can be expensive) if the structure placement already
* forbids placing the structure in this chunk.
*/
@ModifyExpressionValue(method = "checkStart", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/structure/StructureCheck;tryLoadFromStorage(Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/levelgen/structure/Structure;ZJ)Lnet/minecraft/world/level/levelgen/structure/StructureCheckResult;"))
private StructureCheckResult mfix$checkForValidPosition(StructureCheckResult storageResult, ChunkPos chunkPos, Structure structure, boolean skipKnownStructures) {
if (storageResult != null) {
return storageResult;
} else if(mfix$structureState != null) {
// Check if any of the placements allow for this structure to be in this chunk
var structureHolder = this.structureConfigs.wrapAsHolder(structure);
for (var placement : mfix$structureState.getPlacementsForStructure(structureHolder)) {
if (placement.isStructureChunk(mfix$structureState, chunkPos.x, chunkPos.z)) {
// Allowed - return null so regular check runs
return null;
}
}
// Not allowed - early exit by returning a non-null value
return StructureCheckResult.START_NOT_PRESENT;
} else {
return null;
}
}
}

View File

@ -0,0 +1,69 @@
package org.embeddedt.modernfix.common.mixin.perf.fix_handshake_stall;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraftforge.network.HandshakeHandler;
import net.minecraftforge.network.NetworkRegistry;
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.Slice;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Collections;
import java.util.List;
@Mixin(value = HandshakeHandler.class, remap = false)
public class HandshakeHandlerMixin {
@Shadow
private int packetPosition;
@Shadow
private List<NetworkRegistry.LoginPayload> messageList;
@Shadow
private List<Integer> sentMessages;
/**
* @author embeddedt
* @reason we must synchronize sentMessages because it is modified from both the Netty thread and the
* server thread
*/
@Inject(method = "<init>", at = @At("RETURN"))
private void synchronizeSentMessages(CallbackInfo ci) {
this.sentMessages = Collections.synchronizedList(this.sentMessages);
}
/**
* @author embeddedt
* @reason Forge only sends one login payload per tick. It takes many seconds to send all the payloads at this rate.
* During this time, the game remains frozen on the chunk loading screen with almost zero CPU usage.
* To fix this, we re-tick the handshake handler until the packetPosition stops advancing or the handler indicates
* it no longer needs ticking.
*/
@WrapMethod(method = "tickServer")
private boolean modernfix$retick(Operation<Boolean> original) {
boolean isDoneTicking;
int prevPacketPosition;
do {
prevPacketPosition = this.packetPosition;
isDoneTicking = original.call();
} while(!isDoneTicking && this.packetPosition > prevPacketPosition);
return isDoneTicking;
}
/**
* @author embeddedt
* @reason The original HandshakeHandler has an off-by-one error in its completion check. We patch this to prevent
* our optimization from potentially triggering it more often due to the timing change.
*/
@WrapOperation(method = "tickServer", at = @At(value = "INVOKE", target = "Ljava/util/List;isEmpty()Z", ordinal = 0), slice = @Slice(from = @At(value = "INVOKE", target = "Ljava/util/List;removeIf(Ljava/util/function/Predicate;)Z", ordinal = 0)))
private boolean preventEarlyExit(List<?> instance, Operation<Boolean> original) {
if (instance != this.sentMessages) {
throw new AssertionError("Injector is misplaced");
}
return original.call(instance) && this.packetPosition >= this.messageList.size();
}
}

View File

@ -15,7 +15,7 @@ import java.util.function.BooleanSupplier;
@Mixin(value = MinecraftServer.class, priority = 500) @Mixin(value = MinecraftServer.class, priority = 500)
public abstract class MinecraftServerMixin extends BlockableEventLoop<Runnable> { public abstract class MinecraftServerMixin extends BlockableEventLoop<Runnable> {
@Shadow private long nextTickTimeNanos; @Shadow private long nextTickTime;
protected MinecraftServerMixin(String name) { protected MinecraftServerMixin(String name) {
super(name); super(name);
@ -37,12 +37,12 @@ public abstract class MinecraftServerMixin extends BlockableEventLoop<Runnable>
} }
} }
@WrapOperation(method = "waitForTasks", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/thread/ReentrantBlockableEventLoop;waitForTasks()V")) @Override
private void waitLongerForTasks(MinecraftServer instance, Operation<Void> original) { protected void waitForTasks() {
if (this.mfix$isWaitingForNextTick) { if (this.mfix$isWaitingForNextTick) {
LockSupport.parkNanos("waiting for tasks", this.nextTickTimeNanos - Util.getNanos()); LockSupport.parkNanos("waiting for tasks", (this.nextTickTime * 1000000L) - Util.getNanos());
} else { } else {
original.call(instance); super.waitForTasks();
} }
} }
} }

View File

@ -0,0 +1,26 @@
package org.embeddedt.modernfix.common.mixin.perf.forge_cap_retrieval;
import net.minecraftforge.event.AttachCapabilitiesEvent;
import net.minecraftforge.eventbus.api.Event;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(AttachCapabilitiesEvent.class)
public abstract class AttachCapabilitiesEventMixin extends Event {
/**
* @author embeddedt
* @reason EventSubclassTransformer is supposed to inject an override returning a constant on the class to avoid the
* {@link net.minecraftforge.eventbus.api.EventListenerHelper#isCancelable(Class)} slow path.
* However, the false case is only done for direct subclasses of Event (the true case is done for
* any cancelable event). This works for normal events because they must subclass Event directly, or be a subclass
* of an event that does. However, AttachCapabilitiesEvent subclasses GenericEvent, which does not pass through
* the EventSubclassTransformer as it comes from the EventBus library (where transformers are not run) rather than
* Forge which is on the GAME layer. The transformer on AttachCapabilitiesEvent then does not add the override as
* it expects it to be present on GenericEvent already.
* <p>
* The simplest workaround to that whole mess is to just inject the override ourselves.
*/
@Override
public boolean isCancelable() {
return false;
}
}

View File

@ -0,0 +1,24 @@
package org.embeddedt.modernfix.common.mixin.perf.forge_cap_retrieval;
import net.minecraft.core.Direction;
import net.minecraft.world.entity.LivingEntity;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.ForgeCapabilities;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import javax.annotation.Nullable;
@Mixin(LivingEntity.class)
public class LivingEntityMixin {
/**
* @author embeddedt (issue noted by XFactHD)
* @reason check capability equality before checking that entity is alive, the latter requires a lot more
* indirection
*/
@Redirect(method = "getCapability", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;isAlive()Z"))
private <T> boolean checkAliveAfterCap(LivingEntity entity, Capability<T> capability, @Nullable Direction facing) {
return capability == ForgeCapabilities.ITEM_HANDLER && entity.isAlive();
}
}

View File

@ -1,7 +1,7 @@
package org.embeddedt.modernfix.common.mixin.perf.forge_registry_alloc; package org.embeddedt.modernfix.common.mixin.perf.forge_registry_alloc;
import net.minecraft.world.level.levelgen.DebugLevelSource; import net.minecraft.world.level.levelgen.DebugLevelSource;
import net.neoforged.neoforge.registries.GameData; import net.minecraftforge.registries.GameData;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.Redirect;

View File

@ -0,0 +1,30 @@
package org.embeddedt.modernfix.common.mixin.perf.forge_registry_alloc;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import org.embeddedt.modernfix.forge.registry.DelegateHolder;
import org.spongepowered.asm.mixin.Mixin;
@Mixin({Block.class, Item.class})
public class DelegateHolderMixin<T> implements DelegateHolder<T> {
private Holder.Reference<T> mfix$delegate;
private ResourceKey<Registry<T>> mfix$key;
@Override
public Holder.Reference<T> mfix$getDelegate(ResourceKey<Registry<T>> registryKey) {
if(mfix$key == registryKey) {
return mfix$delegate;
} else {
return null;
}
}
@Override
public void mfix$setDelegate(ResourceKey<Registry<T>> registryKey, Holder.Reference<T> holder) {
this.mfix$delegate = holder;
this.mfix$key = registryKey;
}
}

View File

@ -0,0 +1,99 @@
package org.embeddedt.modernfix.common.mixin.perf.forge_registry_alloc;
import it.unimi.dsi.fastutil.Hash;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.registries.ForgeRegistry;
import net.minecraftforge.registries.RegistryManager;
import org.embeddedt.modernfix.forge.registry.DelegateHolder;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
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.CallbackInfoReturnable;
import java.util.Locale;
import java.util.Map;
@Mixin(value = ForgeRegistry.class, remap = false)
public abstract class ForgeRegistryMixin<V> {
// Replace the backing maps with fastutil maps for a bit more speed, since value->holder lookups in particular
// are a bottleneck in many areas (e.g. render type lookup)
@Shadow @Final private Map<ResourceLocation, Holder.Reference<V>> delegatesByName = new Object2ObjectOpenHashMap<>();
@Shadow @Final private Map<V, Holder.Reference<V>> delegatesByValue = new Object2ObjectOpenHashMap<>(Hash.DEFAULT_INITIAL_SIZE, 0.5F);
@Shadow public abstract ResourceKey<Registry<V>> getRegistryKey();
@Shadow @Final private RegistryManager stage;
/**
* @author embeddedt
* @reason stop allocating so many unneeded objects. stop.
*/
@Overwrite
public Holder.Reference<V> getDelegateOrThrow(ResourceLocation location) {
Holder.Reference<V> holder = delegatesByName.get(location);
if (holder == null) {
throw new IllegalArgumentException(String.format(Locale.ENGLISH, "No delegate exists for location %s", location));
}
return holder;
}
/**
* @author embeddedt
* @reason see above
*/
@Overwrite
public Holder.Reference<V> getDelegateOrThrow(ResourceKey<V> rkey) {
Holder.Reference<V> holder = delegatesByName.get(rkey.location());
if (holder == null) {
throw new IllegalArgumentException(String.format(Locale.ENGLISH, "No delegate exists for key %s", rkey));
}
return holder;
}
/**
* @author embeddedt
* @reason store delegates that are accessed extremely regularly on the registry entry itself, rather than
* going through a map lookup
*/
@Inject(method = "bindDelegate", at = @At("RETURN"))
private void attachDelegate(ResourceKey<V> rkey, V value, CallbackInfoReturnable<Holder.Reference<V>> cir) {
// Only attach delegates for the ACTIVE registry. The Forge registry system is weird and seems to keep multiple
// copies of itself at once.
if(this.stage == RegistryManager.ACTIVE && value instanceof DelegateHolder<?>) {
((DelegateHolder<V>)value).mfix$setDelegate(this.getRegistryKey(), cir.getReturnValue());
}
}
/**
* @author embeddedt
* @reason skip map lookup for hot delegates, avoid allocations otherwise
*/
@Overwrite
public Holder.Reference<V> getDelegateOrThrow(V value) {
Holder.Reference<V> holder = null;
if (this.stage == RegistryManager.ACTIVE && value instanceof DelegateHolder<?>) {
holder = ((DelegateHolder<V>)value).mfix$getDelegate(this.getRegistryKey());
}
if (holder == null) {
holder = delegatesByValue.get(value);
if (holder == null) {
throw new IllegalArgumentException(String.format(Locale.ENGLISH, "No delegate exists for value %s", value));
}
}
return holder;
}
}

View File

@ -0,0 +1,29 @@
package org.embeddedt.modernfix.common.mixin.perf.forge_registry_lambda;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.registries.RegistryObject;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
@Mixin(value = RegistryObject.class, remap = false)
public class RegistryObjectMixin<T> {
@Shadow private @Nullable T value;
@Shadow @Final private ResourceLocation name;
/**
* @author embeddedt
* @reason avoid lambda allocation on every call
*/
@Overwrite
public T get() {
T ret = this.value;
if(ret == null) {
throw new NullPointerException("Registry Object not present: " + this.name);
}
return ret;
}
}

View File

@ -1,7 +1,7 @@
package org.embeddedt.modernfix.common.mixin.perf.ingredient_item_deduplication; package org.embeddedt.modernfix.common.mixin.perf.ingredient_item_deduplication;
import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.item.crafting.Ingredient;
import org.embeddedt.modernfix.neoforge.recipe.IngredientValueDeduplicator; import org.embeddedt.modernfix.forge.recipe.IngredientValueDeduplicator;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable; import org.spongepowered.asm.mixin.injection.ModifyVariable;
@ -10,20 +10,8 @@ import java.util.stream.Stream;
@Mixin(Ingredient.class) @Mixin(Ingredient.class)
public class IngredientMixin { public class IngredientMixin {
@ModifyVariable(method = "<init>(Ljava/util/stream/Stream;)V", at = @At("HEAD"), argsOnly = true, ordinal = 0) @ModifyVariable(method = "<init>", at = @At("HEAD"), argsOnly = true, ordinal = 0)
private static Stream<? extends Ingredient.Value> injectDeduplicationPass(Stream<? extends Ingredient.Value> stream) { private static Stream<? extends Ingredient.Value> injectDeduplicationPass(Stream<? extends Ingredient.Value> stream) {
return stream.map(IngredientValueDeduplicator::deduplicate); return stream.map(IngredientValueDeduplicator::deduplicate);
} }
@ModifyVariable(method = "<init>([Lnet/minecraft/world/item/crafting/Ingredient$Value;)V", at = @At("HEAD"), argsOnly = true, ordinal = 0)
private static Ingredient.Value[] injectDeduplicationPassArray(Ingredient.Value[] values) {
if (values.length == 0) {
return values;
}
Ingredient.Value[] newValues = new Ingredient.Value[values.length];
for (int i = 0; i < values.length; i++) {
newValues[i] = IngredientValueDeduplicator.deduplicate(values[i]);
}
return newValues;
}
} }

View File

@ -1,18 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.ingredient_item_deduplication;
import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
import net.minecraft.core.component.DataComponentMap;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.component.PatchedDataComponentMap;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import java.util.Optional;
@Mixin(PatchedDataComponentMap.class)
public interface PatchedDataComponentMapAccessor {
@Accessor("prototype")
DataComponentMap mfix$getPrototype();
@Accessor("patch")
Reference2ObjectMap<DataComponentType<?>, Optional<?>> mfix$getPatch();
}

View File

@ -0,0 +1,86 @@
package org.embeddedt.modernfix.common.mixin.perf.kubejs;
import com.google.gson.JsonElement;
import dev.latvian.mods.kubejs.recipe.RecipeJS;
import dev.latvian.mods.kubejs.recipe.RecipesEventJS;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.crafting.Recipe;
import net.minecraft.world.item.crafting.RecipeManager;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.FeatureLevel;
import org.embeddedt.modernfix.annotation.RequiresMod;
import org.embeddedt.modernfix.core.config.ModernFixEarlyConfig;
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.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Map;
@Mixin(RecipesEventJS.class)
@RequiresMod("kubejs")
public class RecipeEventJSMixin {
/**
* The recipe event object can be leaked in scripts and this wastes 40MB of memory.
*/
@Inject(method = "post", at = @At("RETURN"), remap = false)
private void clearRecipeLists(CallbackInfo ci) {
ModernFix.LOGGER.info("Clearing KubeJS recipe lists...");
// Even though we are a mixin class, use reflection so this works across a variety of versions
Field[] fields = RecipesEventJS.class.getDeclaredFields();
for(Field f : fields) {
try {
if(!Modifier.isStatic(f.getModifiers())
&& (Collection.class.isAssignableFrom(f.getType())
|| Map.class.isAssignableFrom(f.getType()))
) {
f.setAccessible(true);
Object collection = f.get(this);
int size;
if(collection instanceof Map) {
size = ((Map<?, ?>)collection).size();
((Map<?, ?>)collection).clear();
} else if(collection instanceof Collection) {
size = ((Collection<?>)collection).size();
((Collection<?>)collection).clear();
} else
throw new IllegalStateException();
ModernFix.LOGGER.debug("Cleared {} with {} entries", f.getName(), size);
}
} catch(RuntimeException | ReflectiveOperationException e) {
ModernFix.LOGGER.debug("Uh oh, couldn't clear field", e);
}
}
}
/**
* @author embeddedt
* @reason once datapackRecipeMap is iterated, it is never referenced again, so clear it to avoid retaining
* references to the JSON objects
*/
@Inject(method = "post", at = @At(value = "NEW", target = "()Ljava/util/concurrent/ConcurrentLinkedQueue;", ordinal = 0), remap = false)
private void modernfix$clearDatapackRecipeMap(RecipeManager recipeManager, Map<ResourceLocation, JsonElement> datapackRecipeMap, CallbackInfo ci) {
if (ModernFixEarlyConfig.ACTIVE_FEATURE_LEVEL.isAtLeast(FeatureLevel.BETA)) {
datapackRecipeMap.clear();
}
}
/**
* @author embeddedt
* @reason As we start materializing the final recipe objects, null out the JSON references so we avoid having
* to keep both in memory at the same time
*/
@Inject(method = "createRecipe", at = @At("RETURN"), remap = false)
private void modernfix$clearJson(RecipeJS r, CallbackInfoReturnable<Recipe<?>> cir) {
if (!ModernFixEarlyConfig.ACTIVE_FEATURE_LEVEL.isAtLeast(FeatureLevel.BETA)) {
return;
}
r.json = null;
r.originalJson = null;
}
}

Some files were not shown because too many files have changed in this diff Show More