Compare commits

..

27 Commits
26.1 ... 1.20

Author SHA1 Message Date
thirtyninerealms-cloud
667ac6c6ee
Fix thread leak and graceful shutdown issue in NightConfigWatchThrottler
Fix thread leak and graceful shutdown issue in NightConfigWatchThrottler
2026-06-14 17:30:06 +08:00
thirtyninerealms-cloud
2d760eecbb
Fix thread leak and graceful shutdown issue in NightConfigWatchThrottler
Problem:
- FileSystemWatchService threads accumulate over time (observed 17+ threads)
- Threads cannot be interrupted during container shutdown due to unhandled parkNanos()
- Container fails to stop gracefully, requiring force kill

Root cause:
- LockSupport.parkNanos() called without interruption handling
- No shutdown detection mechanism
- Threads continue polling file system even when JVM is terminating

Changes:
1. Add AtomicBoolean shutdown flag to prevent new watch iterations during shutdown
2. Add proper thread interruption handling with graceful fallback to empty iterator
3. Register shutdown hook to set flag on JVM exit

Testing:
- Verified threads no longer accumulate after multiple config reloads
- Container now responds to SIGTERM and stops within 5 seconds
- CPU usage returns to normal after shutdown sequence
2026-06-14 17:24:20 +08:00
embeddedt
292a6aeab3
Fix optimize_surface_rules breaking mods that provide custom BiomeManagers 2026-06-11 20:01:31 -04:00
embeddedt
7fbfcf1a92
Remove error when missing_block_entities sees null BE
Blocks may legitimately not have a block entity for some states
2026-06-07 21:50:44 -04:00
embeddedt
1bcb28a1ad
Allow feature level requirement to be set at package level 2026-06-07 19:43:28 -04:00
embeddedt
d51b0f60a2
Fix an instance of vanilla leaking a BufferBuilder 2026-06-07 19:19:25 -04:00
embeddedt
ab9880159e
Add experimental KubeJS memory usage optimization 2026-06-06 21:17:34 -04:00
embeddedt
0f94634361
Remove the item stack reference thread 2026-06-06 21:04:09 -04:00
embeddedt
f1492cc829
Allow ZipPackIndex to work with any byte channel 2026-06-04 20:57:13 -04:00
embeddedt
0ecee529d7
Fix Forge calling getResource on every loot table unnecessarily 2026-06-03 18:05:56 -04:00
embeddedt
e9bfd96dd9
Fix Forge pack finder being injected multiple times into pack repository 2026-05-28 22:33:03 -04:00
embeddedt
fb9dcf77c6
Improve ZipPackIndex 2026-05-28 22:20:28 -04:00
embeddedt
33851c1cb6
Fix ImposterProtoChunk leaking live block entities to worldgen 2026-05-24 23:09:52 -04:00
embeddedt
494203ef5a
Fix potential crash during worldgen with release_protochunks enabled
The crash can occur if a protochunk next to a FULL chunk is dropped,
and then later re-requested. If it was not persisted to disk for any
reason, it starts regeneration from scratch. At FEATURES stage, it may
try to place blocks into the adjacent LevelChunk already in the world.

The fix is to prevent this situation from even happening by pinning
protochunks directly next to FULL chunks, and preventing them from
unloading.
2026-05-24 19:45:24 -04:00
embeddedt
74f76f7305
Improvements to ZipPackIndex
- Allow it to work on channels that don't support mapping
- Skip indexing folders that are not part of a pack type
2026-05-23 21:44:14 -04:00
embeddedt
62dbbea083
Optimize ZIP resource packs significantly 2026-05-23 21:28:19 -04:00
embeddedt
538c52bc2a
Run stronghold gen on dedicated thread pool 2026-05-23 17:00:08 -04:00
embeddedt
b62eb1845b
Avoid blocking chunk generation on concentric rings calculation where possible 2026-05-23 16:43:56 -04:00
embeddedt
7c45564979
Fix potential stronghold cache corruption if player exits world too quickly 2026-05-23 16:32:56 -04:00
embeddedt
f8d2425242
Improve accuracy of possible biomes check 2026-05-23 12:50:48 -04:00
embeddedt
50cedfc699
Fix stability level being impossible to override 2026-05-23 12:50:33 -04:00
embeddedt
f4f596ca0c
Fix mixin failing at runtime due to missing AT 2026-05-23 12:50:21 -04:00
embeddedt
85aab426c5
Fix mixin AP complaints 2026-05-23 12:01:33 -04:00
embeddedt
29ff5f152e
Log the state of each mixin at DEBUG level 2026-05-23 11:58:36 -04:00
embeddedt
8213a720a3
Optimize TerraBlender using extended surface biome context
Supersedes TerraBlenderFix
2026-05-23 11:56:45 -04:00
embeddedt
afe3e09a27
Add feature level system for mixins 2026-05-23 11:51:11 -04:00
embeddedt
ae20fa17c9
Fix random CMEs from NightConfigWatchThrottler 2026-05-18 10:05:23 -04:00
281 changed files with 11520 additions and 2359 deletions

View File

@ -1,6 +0,0 @@
- Reimplement mixin.bugfix.entity_pose_stack
- Reimplement dynamic_resources for items & Neo standalone models
- Investigate if cache_strongholds is still needed in 26.1
- Check if BlockStateData patch is still needed in compact_mojang_registries
- Check if faster_texture_stitching is still worthwhile with 21.x changes to the stitcher
- Sculk deadlock fix looks unnecessary since Mojang is careful about when they send the event

View File

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

View File

@ -25,7 +25,7 @@ public record MixinConfig(
InjectorOptions injectors, OverwriteOptions overwrites
) {
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);
}
public record InjectorOptions(int defaultRequire) {

View File

@ -0,0 +1,9 @@
package org.embeddedt.modernfix.annotation;
public enum FeatureLevel {
GA, BETA;
public boolean isAtLeast(FeatureLevel required) {
return this.ordinal() >= required.ordinal();
}
}

View File

@ -0,0 +1,12 @@
package org.embeddedt.modernfix.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE, ElementType.PACKAGE})
public @interface RequiresFeatureLevel {
FeatureLevel value() default FeatureLevel.GA;
}

View File

@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
@Target({ElementType.TYPE, ElementType.PACKAGE})
public @interface RequiresMod {
String value() default "";
}

View File

@ -1,5 +1,5 @@
plugins {
id("net.neoforged.moddev") version("2.0.140")
id("net.neoforged.moddev.legacyforge") version("2.0.134")
id("me.modmuss50.mod-publish-plugin") version("1.1.0")
}
@ -16,25 +16,22 @@ val gitVersion = providers.of(GitVersionSource::class) {
version = gitVersion.get()
base.archivesName = "modernfix-neoforge"
base.archivesName = "modernfix-forge"
neoForge {
legacyForge {
enable {
version = rootProject.properties["forge_version"].toString()
forgeVersion = rootProject.properties["forge_version"].toString()
isDisableRecompilation = System.getenv("CI") == "true"
}
rootProject.properties["parchment_version"]?.let { parchmentVer ->
parchment {
minecraftVersion = rootProject.properties["parchment_mc_version"].toString()
minecraftVersion = minecraft_version
mappingsVersion = parchmentVer.toString()
}
}
runs {
configureEach {
systemProperty("modernfix.auditMixinsAtStart", "true")
}
create("client") {
client()
}
@ -54,8 +51,14 @@ neoForge {
}
}
mixin {
add(sourceSets.main.get(), "modernfix.refmap.json")
config("modernfix-modernfix.mixins.json")
}
tasks.named<Jar>("jar") {
manifest.attributes(mapOf(
"MixinConfigs" to "modernfix-modernfix.mixins.json",
"Specification-Version" to "1",
"Implementation-Title" to project.name,
"Implementation-Version" to version
@ -63,7 +66,7 @@ tasks.named<Jar>("jar") {
}
java {
val curSourceCompatLevel = JavaVersion.VERSION_25
val curSourceCompatLevel = JavaVersion.VERSION_17
sourceCompatibility = curSourceCompatLevel
targetCompatibility = curSourceCompatLevel
}
@ -106,18 +109,27 @@ val embed by configurations.creating {
dependencies {
implementation(project(":annotations"))
embed(project(":annotations"))
"additionalRuntimeClasspath"(project(":annotations"))
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_minecraft_version = rootProject.properties["jei_minecraft_version"]?.toString() ?: minecraft_version
compileOnly("mezz.jei:jei-${jei_minecraft_version}-neoforge:${jei_version}")
compileOnly("curse.maven:spark-361579:${rootProject.properties["spark_version"].toString()}")
compileOnly("curse.maven:ctm-267602:${rootProject.properties["ctm_version"].toString()}")
compileOnly("curse.maven:ldlib-626676:${rootProject.properties["ldlib_version"].toString()}")
compileOnly("curse.maven:supermartijncore-454372:4455391")
compileOnly("curse.maven:cofhcore-69162:5374122")
compileOnly("curse.maven:resourcefullib-570073:5659871")
compileOnly("curse.maven:kubejs-238086:5853326")
modCompileOnly("mezz.jei:jei-${minecraft_version}-forge:${jei_version}")
modCompileOnly("curse.maven:spark-361579:${rootProject.properties["spark_version"].toString()}")
modCompileOnly("curse.maven:ctm-267602:${rootProject.properties["ctm_version"].toString()}")
modCompileOnly("curse.maven:ldlib-626676:${rootProject.properties["ldlib_version"].toString()}")
modCompileOnly("curse.maven:supermartijncore-454372:4455391")
modCompileOnly("curse.maven:patchouli-306770:6164575")
modCompileOnly("curse.maven:cofhcore-69162:5374122")
modCompileOnly("curse.maven:resourcefullib-570073:5659871")
modCompileOnly("curse.maven:kubejs-238086:5853326")
modCompileOnly("curse.maven:terrablender-563928:6290448")
}
tasks.named<Jar>("jar") {
@ -134,8 +146,6 @@ tasks.withType<JavaCompile>().configureEach {
)
)
}
// Show more errors when porting
options.compilerArgs.addAll(listOf("-Xmaxerrs", "1000"))
}
sourceSets {
@ -151,12 +161,12 @@ tasks.named<ProcessResources>("processResources") {
inputs.property("version", project.version)
filesMatching("META-INF/neoforge.mods.toml") {
filesMatching("META-INF/mods.toml") {
expand("version" to project.version)
}
}
val finalJarTask = "jar"
val finalJarTask = "reobfJar"
tasks.register<Copy>("copyJarNameConsistent") {
from(tasks.named<Jar>(finalJarTask).get().outputs.files)
@ -180,7 +190,7 @@ publishMods {
changelog = "Please check the [GitHub wiki](https://github.com/embeddedt/ModernFix/wiki/Changelog) for major changes."
type = STABLE
modLoaders.add("neoforge")
modLoaders.add("forge")
curseforge {
projectId = "790626"
@ -197,4 +207,4 @@ publishMods {
tasks.named("publishMods") {
dependsOn(finalJarTask)
}
}

View File

@ -5,30 +5,28 @@ junit_version=5.10.0-M1
mixinextras_version=0.4.1
mod_id=modernfix
minecraft_version=26.1.2
enabled_platforms=neoforge
forge_version=26.1.2.73
parchment_version=2025.12.20
parchment_mc_version=1.21.11
minecraft_version=1.20.1
enabled_platforms=forge
forge_version=1.20.1-47.4.0
parchment_version=2023.07.09
refined_storage_version=4392788
jei_version=27.3.0.12
jei_minecraft_version=1.21.11
rei_version=13.0.678
ctm_version=5587515
ldlib_version=5782845
kubejs_version=2101.7.1-build.181
rhino_version=2101.2.7-build.74
supported_minecraft_versions=26.1
jei_version=15.8.0.11
rei_version=11.0.597
ctm_version=5983309
ldlib_version=5927130
kubejs_version=2001.6.5-build.16
rhino_version=2001.2.3-build.10
supported_minecraft_versions=1.20.1
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
spark_version=6225208
spark_version=4587310
use_fabric_api_at_runtime=true

View File

@ -5,7 +5,7 @@ import re
def get_valid_mixin_options():
all_mixin_options = set()
# 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"
for root, dirs, files in os.walk(base_path):
for file in files:

View File

@ -1,9 +1,8 @@
package org.embeddedt.modernfix;
import net.minecraft.SharedConstants;
import net.minecraft.TracingExecutor;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.util.Util;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.server.level.ServerLevel;
@ -17,6 +16,7 @@ import org.embeddedt.modernfix.util.ClassInfoManager;
import org.spongepowered.asm.mixin.MixinEnvironment;
import java.lang.management.ManagementFactory;
import java.util.concurrent.ExecutorService;
// The value here should match an entry in the META-INF/mods.toml file
public class ModernFix {
@ -33,17 +33,17 @@ public class ModernFix {
// Used to skip computing the blockstate caches twice
public static boolean runningFirstInjection = false;
private static TracingExecutor resourceReloadService = null;
private static ExecutorService resourceReloadService = null;
static {
if(ModernFixMixinPlugin.instance.isOptionEnabled("perf.dedicated_reload_executor.ReloadExecutor")) {
resourceReloadService = new TracingExecutor(ReloadExecutor.createCustomResourceReloadExecutor());
resourceReloadService = ReloadExecutor.createCustomResourceReloadExecutor();
} else {
resourceReloadService = Util.backgroundExecutor();
}
}
public static TracingExecutor resourceReloadExecutor() {
public static ExecutorService resourceReloadExecutor() {
return resourceReloadService;
}
@ -61,7 +61,7 @@ public class ModernFix {
public ModernFix() {
INSTANCE = this;
if(ModernFixMixinPlugin.instance.isOptionEnabled("feature.snapshot_easter_egg.NameChange") && !SharedConstants.getCurrentVersion().stable())
if(ModernFixMixinPlugin.instance.isOptionEnabled("feature.snapshot_easter_egg.NameChange") && !SharedConstants.getCurrentVersion().isStable())
NAME = "PreemptiveFix";
ModernFixPlatformHooks.INSTANCE.onServerCommandRegister(ModernFixCommands::register);
}

View File

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

View File

@ -1,5 +1,12 @@
package org.embeddedt.modernfix.api.entrypoint;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.*;
import net.minecraft.resources.ResourceLocation;
import java.util.function.Function;
/**
* Implement this interface in a mod class and add it to "modernfix:integration_v1" in your mod metadata file
* to integrate with ModernFix's features.
@ -13,4 +20,58 @@ public interface ModernFixClientIntegration {
*/
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
* 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
* @param textureGetter function to retrieve textures for this model
* @return the model which should actually be loaded for this resource location
*/
default BakedModel onBakedModelLoad(ResourceLocation location, UnbakedModel baseModel, BakedModel originalModel, ModelState state, ModelBakery bakery, Function<Material, TextureAtlasSprite> textureGetter) {
return onBakedModelLoad(location, baseModel, originalModel, state, bakery);
}
}

View File

@ -0,0 +1,87 @@
package org.embeddedt.modernfix.api.helpers;
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.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
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.duck.IExtendedModelBakery;
import org.embeddedt.modernfix.dynamicresources.ModelBakeryHelpers;
import org.embeddedt.modernfix.util.DynamicMap;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
@SuppressWarnings("unused")
public final class ModelHelpers {
/**
* Allows converting a ModelResourceLocation back into the corresponding BlockState(s). Try to avoid calling this
* multiple times if possible.
* @param location the location of the model
* @return a list of all blockstates related to the model
*/
public static ImmutableList<BlockState> getBlockStateForLocation(ModelResourceLocation location) {
Optional<Block> blockOpt = BuiltInRegistries.BLOCK.getOptional(new ResourceLocation(location.getNamespace(), location.getPath()));
if(blockOpt.isPresent())
return ModelBakeryHelpers.getBlockStatesForMRL(blockOpt.get().getStateDefinition(), location);
else
return ImmutableList.of();
}
/**
* Allows converting a ModelResourceLocation back into the corresponding BlockState(s). Faster version of its
* companion function if and only if you know the corresponding Block already for some reason.
* @param definition the state definition for the Block
* @param location the location of the model
* @return a list of all blockstates related to the model
*/
public static ImmutableList<BlockState> getBlockStateForLocation(StateDefinition<Block, BlockState> definition, ModelResourceLocation location) {
return ModelBakeryHelpers.getBlockStatesForMRL(definition, location);
}
/**
* Compatibility helper for mods to use to get a map-like view of the model bakery.
* @param modelGetter the model getter function supplied by the integration class
* @return a fake map of the top-level models
*/
public static Map<ResourceLocation, BakedModel> createFakeTopLevelMap(BiFunction<ResourceLocation, ModelState, BakedModel> modelGetter) {
return new DynamicMap<>(location -> modelGetter.apply(location, BlockModelRotation.X0_Y0));
}
/**
* Provides a ModelBaker for mods to use.
* @param bakery the ModelBakery supplied to your integration
* @return an appropriate ModelBaker
*/
public static ModelBaker adaptBakery(ModelBakery bakery) {
return new ModelBaker() {
@Override
public UnbakedModel getModel(ResourceLocation resourceLocation) {
return bakery.getModel(resourceLocation);
}
@Nullable
@Override
public BakedModel bake(ResourceLocation resourceLocation, ModelState 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,16 +3,59 @@ package org.embeddedt.modernfix.command;
import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.permissions.Permissions;
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.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;
public class ModernFixCommands {
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(literal("modernfix")
.then(literal("mcfunctions").requires(source -> source.permissions().hasPermission(Permissions.COMMANDS_ADMIN))
.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))
.executes(context -> {
ServerLevel level = context.getSource().getLevel();
if(level == null) {

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;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.datafixers.util.Either;
import net.minecraft.CrashReport;
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.LevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatusTasks;
import net.minecraft.world.level.chunk.status.WorldGenContext;
import net.minecraftforge.fml.util.ObfuscationReflectionHelper;
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.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.lang.reflect.Field;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
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 {
@Shadow
@Nullable
protected abstract ChunkHolder getVisibleChunkIfPresent(long l);
@Shadow
@Final
private BlockableEventLoop<Runnable> mainThreadExecutor;
@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
@ -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
* 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))
private static CompletableFuture<ChunkAccess> createSurrogateFuture(Supplier<ChunkAccess> supplier, Executor executor,
@Local(ordinal = 0, argsOnly = true) WorldGenContext worldGenContext) {
var surrogate = new CompletableFuture<ChunkAccess>();
@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 CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> createSurrogateFuture(
CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> previousFuture,
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
// 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
// from running earlier than it would in vanilla.
var mainThreadExecutor = ((ServerChunkCacheAccessor) worldGenContext.level().getChunkSource()).mfix$getMainThreadProcessor();
CompletableFuture.runAsync(() -> {}, executor).thenApplyAsync($ -> {
previousFuture.thenComposeAsync(CompletableFuture::completedFuture, executor).thenApplyAsync(either -> {
// running on thread that executes lambda body
MFIX_SURROGATE_FUTURE.set(surrogate);
try {
return supplier.get();
return fn.apply(either);
} finally {
MFIX_SURROGATE_FUTURE.remove();
}
}, mainThreadExecutor).whenComplete((either, throwable) -> {
}, this.mainThreadExecutor).whenComplete((either, throwable) -> {
if (throwable != null) {
if (!surrogate.isDone()) {
surrogate.completeExceptionally(throwable);
@ -62,10 +85,7 @@ public abstract class ChunkMapLoadMixin {
// The chunk has already become visible at FULL status, so we
// track the exception ourselves and manually rethrow it at the right point
// to trigger a server crash
var exc = new ReportedException(CrashReport.forThrowable(throwable, "Exception during promotion of chunk to FULL status"));
mainThreadExecutor.schedule(() -> {
throw exc;
});
this.mfix$promotionExceptions.add(throwable);
}
} else {
surrogate.complete(either);
@ -80,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
* & block entities. This allows EntityJoinLevelEvent to read the current chunk.
*/
@Inject(method = "lambda$full$0", 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) {
@Inject(method = "lambda$protoChunkToFullChunk$34", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunk;runPostLoad()V"))
private void completeSurrogateFuture(ChunkHolder holder, ChunkAccess p_214856_, CallbackInfoReturnable<ChunkAccess> cir,
@Local(ordinal = 0) LevelChunk levelChunk) {
var future = MFIX_SURROGATE_FUTURE.get();
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

@ -0,0 +1,30 @@
package org.embeddedt.modernfix.common.mixin.bugfix.chunk_deadlock;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.gameevent.GameEvent;
import org.embeddedt.modernfix.ModernFix;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(Entity.class)
public class EntityMixin {
/**
* @author embeddedt
* @reason When an entity is added to the world via the worldgen load path (ChunkMap#postLoadProtoChunk calling
* ServerLevel#addWorldGenChunkEntities), attempts to add a passenger result in a deadlock when the sculk event
* tries to raytrace blocks. To fix this, we skip firing the sculk event if the chunk the entity is within is not
* loaded.
*/
@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, GameEvent event, Entity entity) {
var chunkPos = instance.chunkPosition();
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());
return false;
} else {
return true;
}
}
}

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.resources.PreparableReloadListener;
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.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.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@ -36,7 +40,7 @@ public abstract class ReloadableResourceManagerMixin {
ModernFix.LOGGER.error("A mod is calling registerReloadListener at the wrong time. This will cause random concurrency crashes when ModernFix is not installed. Please report this to them. If you are a modder, refer to https://github.com/embeddedt/ModernFix/wiki/registerReloadListener-called-on-wrong-thread for more information.", new Exception("registerReloadListener called on wrong thread"));
// Defer the call onto the main client thread. There is a decent chance the mod's listener will be
// ignored in this case, but it is more predictable than allowing them to randomly crash the game.
Minecraft.getInstance().schedule(() -> this.registerReloadListener(listener));
Minecraft.getInstance().tell(() -> this.registerReloadListener(listener));
return;
}

View File

@ -0,0 +1,24 @@
package org.embeddedt.modernfix.common.mixin.bugfix.ender_dragon_leak;
import net.minecraft.client.renderer.entity.EnderDragonRenderer;
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.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(EnderDragonRenderer.class)
@ClientOnlyMixin
public abstract class EnderDragonRendererMixin {
@Shadow @Final private EnderDragonRenderer.DragonModel model;
/**
* Prevent leaking the client world through the entity reference.
*/
@Inject(method = "render(Lnet/minecraft/world/entity/boss/enderdragon/EnderDragon;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V", at = @At("RETURN"))
private void clearDragonEntityReference(CallbackInfo ci) {
this.model.entity = null;
}
}

View File

@ -1,29 +0,0 @@
package org.embeddedt.modernfix.common.mixin.bugfix.entity_pose_stack;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.renderer.entity.player.AvatarRenderer;
import net.neoforged.bus.api.Event;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.client.event.RenderPlayerEvent;
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.Redirect;
@Mixin(AvatarRenderer.class)
@ClientOnlyMixin
public class AvatarRendererMixin {
@Redirect(method = "submit(Lnet/minecraft/client/renderer/entity/state/AvatarRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;Lnet/minecraft/client/renderer/state/level/CameraRenderState;)V", at = @At(value = "INVOKE", target = "Lnet/neoforged/bus/api/IEventBus;post(Lnet/neoforged/bus/api/Event;)Lnet/neoforged/bus/api/Event;", ordinal = 0))
private Event fireCheckingPoseStack(IEventBus instance, Event event) {
PoseStack stack = ((RenderPlayerEvent)event).getPoseStack();
int size = ((PoseStackAccessor)stack).mfix$getLastIndex();
instance.post(event);
if (((RenderPlayerEvent.Pre)event).isCanceled()) {
// Pop the stack if someone pushed it in the event
while (((PoseStackAccessor)stack).mfix$getLastIndex() > size) {
stack.popPose();
}
}
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 net.minecraft.client.renderer.entity.LivingEntityRenderer;
import net.neoforged.neoforge.client.event.RenderLivingEvent;
import net.neoforged.bus.api.Event;
import net.neoforged.bus.api.IEventBus;
import net.minecraftforge.client.event.RenderLivingEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.eventbus.api.IEventBus;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@ -13,17 +13,18 @@ import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(LivingEntityRenderer.class)
@ClientOnlyMixin
public class LivingEntityRendererMixin {
@Redirect(method = "submit(Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;Lnet/minecraft/client/renderer/state/level/CameraRenderState;)V", at = @At(value = "INVOKE", target = "Lnet/neoforged/bus/api/IEventBus;post(Lnet/neoforged/bus/api/Event;)Lnet/neoforged/bus/api/Event;", ordinal = 0))
private Event fireCheckingPoseStack(IEventBus instance, Event event) {
@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 boolean fireCheckingPoseStack(IEventBus instance, Event event) {
PoseStack stack = ((RenderLivingEvent)event).getPoseStack();
int size = ((PoseStackAccessor)stack).mfix$getLastIndex();
instance.post(event);
if (((RenderLivingEvent.Pre)event).isCanceled()) {
int size = ((PoseStackAccessor)stack).getPoseStack().size();
if (instance.post(event)) {
// Pop the stack if someone pushed it in the event
while (((PoseStackAccessor)stack).mfix$getLastIndex() > size) {
while (((PoseStackAccessor)stack).getPoseStack().size() > size) {
stack.popPose();
}
return true;
} else {
return false;
}
return event;
}
}
}

View File

@ -0,0 +1,30 @@
package org.embeddedt.modernfix.common.mixin.bugfix.entity_pose_stack;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.renderer.entity.player.PlayerRenderer;
import net.minecraftforge.client.event.RenderPlayerEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.eventbus.api.IEventBus;
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.Redirect;
@Mixin(PlayerRenderer.class)
@ClientOnlyMixin
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/minecraftforge/eventbus/api/IEventBus;post(Lnet/minecraftforge/eventbus/api/Event;)Z", ordinal = 0))
private boolean fireCheckingPoseStack(IEventBus instance, Event event) {
PoseStack stack = ((RenderPlayerEvent)event).getPoseStack();
int size = ((PoseStackAccessor)stack).getPoseStack().size();
if (instance.post(event)) {
// Pop the stack if someone pushed it in the event
while (((PoseStackAccessor)stack).getPoseStack().size() > size) {
stack.popPose();
}
return true;
} else {
return false;
}
}
}

View File

@ -5,9 +5,11 @@ import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import java.util.Deque;
@Mixin(PoseStack.class)
@ClientOnlyMixin
public interface PoseStackAccessor {
@Accessor("lastIndex")
int mfix$getLastIndex();
@Accessor
Deque<PoseStack.Pose> getPoseStack();
}

View File

@ -2,8 +2,8 @@ package org.embeddedt.modernfix.common.mixin.bugfix.extra_experimental_screen;
import com.mojang.serialization.Lifecycle;
import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen;
import net.minecraft.world.level.storage.LevelDataAndDimensions;
import net.minecraft.world.level.storage.PrimaryLevelData;
import net.minecraft.world.level.storage.WorldData;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@ -16,11 +16,11 @@ public class CreateWorldScreenMixin {
* Fix experimental world dialog still being shown the first time you reopen a world that was created
* as experimental.
*/
@ModifyArg(method = "createNewWorld", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/worldselection/WorldOpenFlows;createLevelFromExistingSettings(Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;Lnet/minecraft/server/ReloadableServerResources;Lnet/minecraft/core/LayeredRegistryAccess;Lnet/minecraft/world/level/storage/LevelDataAndDimensions$WorldDataAndGenSettings;Ljava/util/Optional;)V"), index = 3)
private LevelDataAndDimensions.WorldDataAndGenSettings setExperimentalFlag(LevelDataAndDimensions.WorldDataAndGenSettings settings) {
if(settings.data() instanceof PrimaryLevelData pld && settings.data().worldGenSettingsLifecycle() != Lifecycle.stable()) {
@ModifyArg(method = "createNewWorld", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/worldselection/WorldOpenFlows;createLevelFromExistingSettings(Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;Lnet/minecraft/server/ReloadableServerResources;Lnet/minecraft/core/LayeredRegistryAccess;Lnet/minecraft/world/level/storage/WorldData;)V"), index = 3)
private WorldData setExperimentalFlag(WorldData data) {
if(data instanceof PrimaryLevelData pld && data.worldGenSettingsLifecycle() != Lifecycle.stable()) {
pld.withConfirmedWarning(true);
}
return settings;
return data;
}
}

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

@ -2,16 +2,17 @@ package org.embeddedt.modernfix.common.mixin.bugfix.missing_block_entities;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Registry;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.levelgen.blending.BlendingData;
import org.embeddedt.modernfix.ModernFix;
@ -37,14 +38,14 @@ public abstract class LevelChunkMixin extends ChunkAccess {
@Shadow @Nullable public abstract BlockEntity getBlockEntity(BlockPos pos, LevelChunk.EntityCreationType creationType);
public LevelChunkMixin(ChunkPos chunkPos, UpgradeData upgradeData, LevelHeightAccessor levelHeightAccessor, PalettedContainerFactory palettedContainerFactory, long inhabitedTime, LevelChunkSection @org.jspecify.annotations.Nullable [] sections, @org.jspecify.annotations.Nullable BlendingData blendingData) {
super(chunkPos, upgradeData, levelHeightAccessor, palettedContainerFactory, inhabitedTime, sections, blendingData);
public LevelChunkMixin(ChunkPos chunkPos, UpgradeData upgradeData, LevelHeightAccessor levelHeightAccessor, Registry<Biome> biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sections, @Nullable BlendingData blendingData) {
super(chunkPos, upgradeData, levelHeightAccessor, biomeRegistry, inhabitedTime, sections, blendingData);
}
@Inject(method = "replaceWithPacketData", at = @At("RETURN"))
private void validateBlockEntitiesInChunk(CallbackInfo ci) {
// No reason to check in singleplayer or on the integrated server
if (this.level.isClientSide() && !Minecraft.getInstance().isLocalServer()) {
if (this.level.isClientSide && !Minecraft.getInstance().isLocalServer()) {
for (int i = 0; i < this.sections.length; i++) {
var section = this.sections[i];
try {
@ -61,8 +62,8 @@ public abstract class LevelChunkMixin extends ChunkAccess {
@Unique
private void scanSectionForBlockEntities(LevelChunkSection section, int i) {
int chunkXOff = this.chunkPos.x() * 16;
int chunkZOff = this.chunkPos.z() * 16;
int chunkXOff = this.chunkPos.x * 16;
int chunkZOff = this.chunkPos.z * 16;
BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
int sectionYOff = this.getSectionYFromSectionIndex(i) * 16;
for (int y = 0; y < 16; y++) {
@ -85,13 +86,9 @@ public abstract class LevelChunkMixin extends ChunkAccess {
}
BlockEntity blockEntity = this.getBlockEntity(pos.immutable(), LevelChunk.EntityCreationType.IMMEDIATE);
String blockName = state.getBlock().toString();
if (blockEntity != null) {
if (ModernFix.LOGGER.isDebugEnabled()) {
ModernFix.LOGGER.debug("Created missing block entity for {} at {}", blockName, pos.toShortString());
}
} else {
ModernFix.LOGGER.error("Block entity is missing for {} at {}, but could not be created", blockName, pos.toShortString());
if (blockEntity != null && ModernFix.LOGGER.isDebugEnabled()) {
String blockName = state.getBlock().toString();
ModernFix.LOGGER.debug("Created missing block entity for {} at {}", blockName, pos.toShortString());
}
}
}

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;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.stats.RecipeBookSettings;
import net.minecraft.world.inventory.RecipeBookType;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.forge.packet.NetworkUtils;
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.Modifier;
@ -30,14 +36,12 @@ public class RecipeBookSettingsMixin {
}
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"))
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());
return false; // skip actually reading buffer
}
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.
*/
@Inject(method = "disconnect(Lnet/minecraft/client/gui/screens/Screen;ZZ)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) {
if(this.level != null) {
try {

View File

@ -0,0 +1,22 @@
package org.embeddedt.modernfix.common.mixin.core;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.server.Bootstrap;
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(Bootstrap.class)
@ClientOnlyMixin
public class BootstrapClientMixin {
/**
* Hack to workaround RenderStateShard deadlock (by loading it early on a single thread). We use validate
* here to ensure Forge registries are initialized.
*/
@Inject(method = "validate", at = @At("HEAD"))
private static void loadClientClasses(CallbackInfo ci) {
RenderType.solid();
}
}

View File

@ -1,11 +1,13 @@
package org.embeddedt.modernfix.common.mixin.core;
import net.minecraft.server.Bootstrap;
import net.minecraftforge.network.NetworkConstants;
import org.embeddedt.modernfix.forge.classloading.ManifestCompactor;
import org.slf4j.Logger;
import org.embeddedt.modernfix.forge.load.ModWorkManagerQueue;
import org.embeddedt.modernfix.util.TimeFormatter;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.MixinEnvironment;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@ -23,10 +25,14 @@ public class BootstrapMixin {
private static void doModernFixBootstrap(CallbackInfo ci) {
if(!isBootstrapped) {
LOGGER.info("ModernFix reached bootstrap stage ({} after launch)", TimeFormatter.formatNanos(ManagementFactory.getRuntimeMXBean().getUptime() * 1000L * 1000L));
if (Boolean.getBoolean("modernfix.auditMixinsAtStart")) {
MixinEnvironment.getCurrentEnvironment().audit();
}
ModWorkManagerQueue.replace();
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;
import net.neoforged.neoforge.registries.GameData;
import org.embeddedt.modernfix.neoforge.init.ModernFixForge;
import net.minecraftforge.registries.GameData;
import org.embeddedt.modernfix.forge.init.ModernFixForge;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;

View File

@ -1,10 +1,10 @@
package org.embeddedt.modernfix.common.mixin.core;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.util.Util;
import net.minecraft.Util;
import net.minecraft.server.MinecraftServer;
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.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@ -36,7 +36,7 @@ public class MinecraftServerMixin implements ITimeTrackingServer {
return mfix$lastTickStartTime;
}
@Inject(method = "runServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;processPacketsAndTick(Z)V"))
@Inject(method = "runServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;tickServer(Ljava/util/function/BooleanSupplier;)V"))
private void trackTickTime(CallbackInfo ci) {
mfix$lastTickStartTime = Util.getMillis();
}

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.sugar.Local;
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.injection.At;
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;
import com.google.common.collect.ImmutableList;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.ModList;
import net.neoforged.neoforge.internal.BrandingControl;
import net.minecraftforge.internal.BrandingControl;
import net.minecraftforge.fml.ModContainer;
import net.minecraftforge.fml.ModList;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@ -14,7 +14,7 @@ import java.util.Optional;
@Mixin(value = BrandingControl.class, remap = false, priority = 1100)
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) {
Optional<? extends ModContainer> mfContainer = ModList.get().getModContainerById("modernfix");
if(mfContainer.isPresent())

View File

@ -1,6 +1,6 @@
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.spongepowered.asm.mixin.Mixin;
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.TimeUnit;
@Mixin(SectionRenderDispatcher.class)
@Mixin(ChunkRenderDispatcher.class)
@ClientOnlyMixin
public class ChunkRenderDispatcherMixin {
private static final Executor MFIX_CHUNK_BUILD_EXECUTOR = new ThreadPoolExecutor(1, computeNumThreads(), 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

View File

@ -1,15 +1,16 @@
package org.embeddedt.modernfix.common.mixin.feature.cause_lag_by_disabling_threads;
import net.minecraft.TracingExecutor;
import net.minecraft.util.Util;
import net.minecraft.Util;
import org.embeddedt.modernfix.util.SingleThreadedWorkerService;
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 java.util.concurrent.ExecutorService;
@Mixin(Util.class)
public class UtilMixin {
@Shadow @Final @Mutable
private static final TracingExecutor BACKGROUND_EXECUTOR = new TracingExecutor(new SingleThreadedWorkerService());
private static final ExecutorService BACKGROUND_EXECUTOR = new SingleThreadedWorkerService();
}

View File

@ -5,9 +5,8 @@ import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.functions.CommandFunction;
import net.minecraft.resources.Identifier;
import net.minecraft.commands.CommandFunction;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.ServerFunctionManager;
import org.embeddedt.modernfix.duck.IProfilingServerFunctionManager;
import org.spongepowered.asm.mixin.Final;
@ -25,27 +24,27 @@ import java.util.Map;
@Mixin(ServerFunctionManager.class)
public class ServerFunctionManagerMixin implements IProfilingServerFunctionManager {
@Shadow @Final private static Identifier TICK_FUNCTION_TAG;
@Shadow @Final private static ResourceLocation TICK_FUNCTION_TAG;
private final Map<Identifier, Stopwatch> mfix$functionWatches = new Object2ObjectOpenHashMap<>();
private final Map<ResourceLocation, Stopwatch> mfix$functionWatches = new Object2ObjectOpenHashMap<>();
@Inject(method = "executeTagFunctions", at = @At("HEAD"))
private void resetWatches(Collection<CommandFunction<CommandSourceStack>> functionObjects, Identifier identifier, CallbackInfo ci) {
private void resetWatches(Collection<CommandFunction> functionObjects, ResourceLocation identifier, CallbackInfo ci) {
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"))
private void startWatch(Collection<CommandFunction<CommandSourceStack>> functionObjects, Identifier identifier, CallbackInfo ci, @Local(ordinal = 0) CommandFunction<CommandSourceStack> function, @Share("stopwatch") LocalRef<Stopwatch> watchRef) {
@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> functionObjects, ResourceLocation identifier, CallbackInfo ci, @Local(ordinal = 0) CommandFunction function, @Share("stopwatch") LocalRef<Stopwatch> watchRef) {
watchRef.set(null);
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();
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))
private void stopWatch(Collection<CommandFunction<CommandSourceStack>> functionObjects, Identifier identifier, CallbackInfo ci, @Share("stopwatch") LocalRef<Stopwatch> watchRef) {
@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> functionObjects, ResourceLocation identifier, CallbackInfo ci, @Share("stopwatch") LocalRef<Stopwatch> watchRef) {
var watch = watchRef.get();
if (watch != null && watch.isRunning()) {
watch.stop();
@ -53,14 +52,14 @@ public class ServerFunctionManagerMixin implements IProfilingServerFunctionManag
}
@Inject(method = "executeTagFunctions", at = @At("RETURN"))
private void pruneUnusedWatches(Collection<CommandFunction<CommandSourceStack>> functionObjects, Identifier identifier, CallbackInfo ci) {
private void pruneUnusedWatches(Collection<CommandFunction> functionObjects, ResourceLocation identifier, CallbackInfo ci) {
mfix$functionWatches.values().removeIf(watch -> watch.elapsed().isZero());
}
@Override
public String mfix$getProfilingResults() {
var list = new ArrayList<>(mfix$functionWatches.entrySet());
list.sort(Comparator.<Map.Entry<Identifier, Stopwatch>, Duration>comparing(e -> e.getValue().elapsed()).reversed());
list.sort(Comparator.<Map.Entry<ResourceLocation, Stopwatch>, Duration>comparing(e -> e.getValue().elapsed()).reversed());
StringBuilder sb = new StringBuilder();
for (var entry : list) {
sb.append(entry.getKey().toString());

View File

@ -0,0 +1,20 @@
package org.embeddedt.modernfix.common.mixin.feature.measure_time;
import net.minecraft.server.packs.resources.PreparableReloadListener;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@Mixin(targets = "net/minecraftforge/event/AddReloadListenerEvent$WrappedStateAwareListener")
public abstract class AddReloadListenerEventWrapperMixin implements PreparableReloadListener {
@Shadow @Final private PreparableReloadListener wrapped;
/**
* @author embeddedt
* @reason make a proper name show up in ProfiledReloadInstance
*/
@Override
public String getName() {
return this.wrapped.getClass().getName();
}
}

View File

@ -39,7 +39,7 @@ public class ProfiledReloadInstanceMixin {
@ModifyVariable(method = "finish", ordinal = 0, argsOnly = true, at = @At("HEAD"))
private List<ProfiledReloadInstance.State> sortStates(List<ProfiledReloadInstance.State> datapoints) {
datapoints = new ArrayList<>(datapoints);
datapoints.sort(Comparator.<ProfiledReloadInstance.State>comparingLong(s -> s.preparationNanos().get() + s.reloadNanos().get()).reversed());
datapoints.sort(Comparator.<ProfiledReloadInstance.State>comparingLong(s -> s.preparationNanos.get() + s.reloadNanos.get()).reversed());
return datapoints;
}
}

View File

@ -1,50 +0,0 @@
package org.embeddedt.modernfix.common.mixin.feature.registry_event_progress;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet;
import net.minecraft.world.level.block.Block;
import net.neoforged.fml.loading.progress.ProgressMeter;
import net.neoforged.fml.loading.progress.StartupNotificationManager;
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.Set;
@Mixin(targets = {"net/neoforged/neoforge/registries/NeoForgeRegistryCallbacks$BlockCallbacks"})
public class BlockCallbacksMixin {
@Shadow @Final @Mutable
private Set<Block> addedBlocks;
/**
* @author embeddedt
* @reason Use an ordered set to make the baking order more predictable for users watching the splash screen
*/
@Inject(method = "<init>", at = @At("RETURN"))
private void useOrderedSet(CallbackInfo ci) {
this.addedBlocks = new ReferenceLinkedOpenHashSet<>(this.addedBlocks);
}
@Inject(method = "onBake", at = @At("HEAD"))
private void startBakeProgress(CallbackInfo ci, @Share("meter") LocalRef<ProgressMeter> meter) {
meter.set(StartupNotificationManager.prependProgressBar("Build blockstate caches", addedBlocks.size()));
}
@Inject(method = "onBake", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/Block;getStateDefinition()Lnet/minecraft/world/level/block/state/StateDefinition;", ordinal = 0))
private void showBakeProgressPerBlock(CallbackInfo ci, @Local(ordinal = 0) Block block, @Share("meter") LocalRef<ProgressMeter> meter) {
var id = block.builtInRegistryHolder().getKey().identifier();
meter.get().label("Build blockstate caches - " + id.toString());
meter.get().increment();
}
@Inject(method = "onBake", at = @At(value = "INVOKE", target = "Ljava/util/Set;clear()V", ordinal = 0))
private void stopBakeProgress(CallbackInfo ci, @Share("meter") LocalRef<ProgressMeter> meter) {
meter.get().complete();
}
}

View File

@ -1,46 +1,52 @@
package org.embeddedt.modernfix.common.mixin.feature.registry_event_progress;
import net.neoforged.bus.api.Event;
import net.neoforged.bus.api.EventPriority;
import net.neoforged.fml.ModList;
import net.neoforged.fml.ModLoader;
import net.neoforged.fml.ModLoadingContext;
import net.neoforged.fml.event.IModBusEvent;
import net.neoforged.fml.loading.progress.StartupNotificationManager;
import net.neoforged.neoforge.registries.GameData;
import net.neoforged.neoforge.registries.RegisterEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.ModLoader;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.StartupMessageManager;
import net.minecraftforge.fml.event.IModBusEvent;
import net.minecraftforge.registries.GameData;
import net.minecraftforge.registries.RegisterEvent;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.forge.util.AsyncLoadingScreen;
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;
@Mixin(value = GameData.class, remap = false)
@ClientOnlyMixin
public class GameDataMixin {
@Redirect(method = "postRegisterEvents", at = @At(value = "INVOKE", target = "Lnet/neoforged/fml/ModLoader;postEventWrapContainerInModOrder(Lnet/neoforged/bus/api/Event;)V"))
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().identifier().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();
}
}
private static AsyncLoadingScreen mfix$asyncScreen;
@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;
import net.minecraft.core.Holder;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
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.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@ -19,7 +18,7 @@ import java.util.Map;
public class AttributeSupplierBuilderMixin {
@Shadow
@Final
private Map<Holder<Attribute>, AttributeInstance> builder;
private Map<Attribute, AttributeInstance> builder;
/**
* @author embeddedt

View File

@ -1,7 +1,6 @@
package org.embeddedt.modernfix.common.mixin.perf.attribute_supplier_dedup;
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.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
@ -20,7 +19,7 @@ public class AttributeSupplierMixin {
@Shadow
@Final
@Mutable
private Map<Holder<Attribute>, AttributeInstance> instances;
private Map<Attribute, AttributeInstance> instances;
/**
* @author embeddedt
@ -28,7 +27,7 @@ public class AttributeSupplierMixin {
* care about insertion order in this context
*/
@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);
}
}
}

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.CacheBuilder;
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import net.minecraft.client.resources.SkinManager;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin;
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.TimeUnit;
@Mixin(targets = {"net/minecraft/client/resources/SkinManager$TextureCache" })
@Mixin(SkinManager.class)
@ClientOnlyMixin
public class SkinManagerMixin {
@Unique
@ -21,7 +22,7 @@ public class SkinManagerMixin {
.concurrencyLevel(1)
.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))
private String useCachedHash(MinecraftProfileTexture texture) {
// avoid lambda allocation for common case

View File

@ -2,11 +2,13 @@ package org.embeddedt.modernfix.common.mixin.perf.cache_strongholds;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import net.minecraft.Util;
import net.minecraft.core.Holder;
import net.minecraft.core.RegistryAccess;
import net.minecraft.nbt.*;
import net.minecraft.resources.RegistryOps;
import net.minecraft.util.Util;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
@ -17,6 +19,8 @@ import org.embeddedt.modernfix.duck.IChunkGenerator;
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.Redirect;
import java.lang.ref.SoftReference;
import java.nio.charset.StandardCharsets;
@ -29,6 +33,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Mixin(ChunkGeneratorStructureState.class)
public class ChunkGeneratorMixin implements IChunkGenerator {
@ -41,22 +47,24 @@ public class ChunkGeneratorMixin implements IChunkGenerator {
private BiomeSource biomeSource;
private Path mfix$dimensionPath;
private RegistryAccess.Frozen mfix$registryAccess;
private MinecraftServer mfix$server;
private SoftReference<Map<String, List<ChunkPos>>> mfix$cachedPositions = new SoftReference<>(null);
private static final String CACHE_FILENAME = "mfix_stronghold_cache_v2.nbt";
@Override
public void mfix$setStrongholdCachePath(Path cachePath, RegistryAccess.Frozen registryAccess) {
public void mfix$setStrongholdCachePath(Path cachePath, MinecraftServer server) {
this.mfix$dimensionPath = cachePath;
this.mfix$registryAccess = registryAccess;
this.mfix$server = server;
}
@WrapMethod(method = "generateRingPositions")
private CompletableFuture<List<ChunkPos>> modernfix$cacheRingPositions(Holder<StructureSet> structureSet,
ConcentricRingsStructurePlacement placement,
Operation<CompletableFuture<List<ChunkPos>>> original) {
if (this.mfix$registryAccess == null || this.mfix$dimensionPath == null) {
ConcentricRingsStructurePlacement placement,
Operation<CompletableFuture<List<ChunkPos>>> original,
@Share("threadPool") LocalRef<ExecutorService> threadPoolRef) {
if (this.mfix$server == null || this.mfix$dimensionPath == null) {
return original.call(structureSet, placement);
}
@ -69,15 +77,36 @@ public class ChunkGeneratorMixin implements IChunkGenerator {
return CompletableFuture.completedFuture(List.copyOf(cached));
}
return original.call(structureSet, placement).thenApplyAsync(positions -> {
mfix$writeToCache(cacheKey, positions);
return positions;
}, Util.ioPool());
var server = this.mfix$server;
ExecutorService strongholdPool = Executors.newFixedThreadPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 2));
threadPoolRef.set(strongholdPool);
try {
return original.call(structureSet, placement).thenApplyAsync(positions -> {
// Skip write if server exited before we finished
if (server.isRunning()) {
mfix$writeToCache(cacheKey, positions);
}
return positions;
}, Util.ioPool());
} finally {
strongholdPool.shutdown();
}
}
/**
* @author embeddedt
* @reason Ring position calculation is often not required for initial chunk generation, but the tasks still occupy
* CPU time on the main worker pool and prevent higher priority work from progressing. To fix this we use a
* dedicated pool.
*/
@Redirect(method = "generateRingPositions", at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;backgroundExecutor()Ljava/util/concurrent/ExecutorService;"))
private ExecutorService useDedicatedService(@Share("threadPool") LocalRef<ExecutorService> threadPoolRef) {
return threadPoolRef.get();
}
private String mfix$makeCacheKey(ConcentricRingsStructurePlacement placement) {
RegistryOps<Tag> ops = RegistryOps.create(NbtOps.INSTANCE, this.mfix$registryAccess);
String placementKey = ConcentricRingsStructurePlacement.CODEC.codec().encodeStart(ops, placement)
RegistryOps<Tag> ops = RegistryOps.create(NbtOps.INSTANCE, this.mfix$server.registryAccess());
String placementKey = ConcentricRingsStructurePlacement.CODEC.encodeStart(ops, placement)
.result().map(Tag::toString).orElse(null);
String biomeSourceKey = BiomeSource.CODEC.encodeStart(ops, this.biomeSource)
.result().map(Tag::toString).orElse(null);
@ -126,10 +155,11 @@ public class ChunkGeneratorMixin implements IChunkGenerator {
return new HashMap<>();
}
try {
CompoundTag root = NbtIo.readCompressed(file, NbtAccounter.unlimitedHeap());
CompoundTag root = NbtIo.readCompressed(file.toFile());
Map<String, List<ChunkPos>> result = new HashMap<>();
for (String key : root.keySet()) {
root.getIntArray(key).ifPresent(data -> {
for (String key : root.getAllKeys()) {
if (root.contains(key, Tag.TAG_INT_ARRAY)) {
int[] data = root.getIntArray(key);
if (data.length >= 2 && data.length % 2 == 0) {
List<ChunkPos> positions = new ArrayList<>(data.length / 2);
for (int i = 0; i < data.length; i += 2) {
@ -137,7 +167,7 @@ public class ChunkGeneratorMixin implements IChunkGenerator {
}
result.put(key, positions);
}
});
}
}
return result;
} catch (Exception e) {
@ -153,14 +183,14 @@ public class ChunkGeneratorMixin implements IChunkGenerator {
int[] data = new int[positions.size() * 2];
for (int i = 0; i < positions.size(); i++) {
ChunkPos pos = positions.get(i);
data[i * 2] = pos.x();
data[i * 2 + 1] = pos.z();
data[i * 2] = pos.x;
data[i * 2 + 1] = pos.z;
}
root.putIntArray(entry.getKey(), data);
}
Path file = mfix$dimensionPath.resolve(CACHE_FILENAME);
try {
NbtIo.writeCompressed(root, file);
NbtIo.writeCompressed(root, file.toFile());
} catch (Exception e) {
ModernFix.LOGGER.warn("Failed to write stronghold cache", e);
}

View File

@ -0,0 +1,123 @@
package org.embeddedt.modernfix.common.mixin.perf.cache_strongholds;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
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.Shadow;
import org.spongepowered.asm.mixin.Unique;
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;
@Mixin(ConcentricRingsStructurePlacement.class)
@RequiresFeatureLevel(FeatureLevel.BETA)
public class ConcentricRingsStructurePlacementMixin {
@Shadow @Final private int distance;
@Shadow @Final private int spread;
@Shadow @Final private int count;
/**
* Maximum per-axis section displacement from the initial ring chunk after biome snapping.
*
* Vanilla calls findBiomeHorizontal with radius=112 blocks. In quart space this is ±28,
* and converting the selected quart back to section coordinates yields at most ±7 chunks
* per axis from the original (initialX, initialZ).
*/
@Unique private static final int MFIX_MAX_BIOME_SNAP_SECTIONS_PER_AXIS = 7;
/**
* Worst-case Euclidean error introduced by rounding:
* initialX/Z = round(cos(angle) * dist), round(sin(angle) * dist).
*/
@Unique private static final double MFIX_MAX_ROUNDING_ERROR = Math.sqrt(2.0) * 0.5;
/**
* Worst-case Euclidean biome-snap displacement when each axis can move by at most 7 chunks.
*/
@Unique private static final double MFIX_MAX_BIOME_SNAP_ERROR = MFIX_MAX_BIOME_SNAP_SECTIONS_PER_AXIS * Math.sqrt(2.0);
/**
* Total conservative positional slack (rounding + biome snap) applied to radial bounds.
*/
@Unique private static final double MFIX_MAX_POSITION_ERROR = MFIX_MAX_ROUNDING_ERROR + MFIX_MAX_BIOME_SNAP_ERROR;
/** Squared chunk-distance below which no ring position can ever land. */
@Unique private long mfix$innerRadiusSq;
/** Squared chunk-distance above which no ring position can ever land. */
@Unique private long mfix$outerRadiusSq;
/**
* Precomputes conservative radial bounds for vanilla's ring placement distance:
* {@code dist = 4*i + i*i1*6 + noise}, where {@code i=distance} and {@code i1=circle}.
*
* - Inner bound uses the minimum possible base term ({@code i1=0} => {@code 4*i}).
* - Outer bound uses the maximum reachable {@code i1} for this ({@code spread,count}) pair.
*
* Both bounds are expanded by {@link #MFIX_MAX_POSITION_ERROR} so we never reject a valid
* chunk produced by rounding and biome snapping.
*/
@Inject(
method = "<init>(Lnet/minecraft/core/Vec3i;Lnet/minecraft/world/level/levelgen/structure/placement/StructurePlacement$FrequencyReductionMethod;FILjava/util/Optional;IIILnet/minecraft/core/HolderSet;)V",
at = @At("RETURN")
)
private void mfix$computeRadiusBounds(CallbackInfo ci) {
double maxNoise = this.distance * 1.25; // (nextDouble() - 0.5) * (distance * 2.5)
// min(dist): 4*i + i*0*6 - maxNoise
double minDist = 4.0 * this.distance - maxNoise;
double safeInnerRadius = minDist - MFIX_MAX_POSITION_ERROR;
this.mfix$innerRadiusSq = (long)Math.max(0.0, Math.floor(safeInnerRadius * safeInnerRadius));
if (this.spread == 0) {
// Vanilla behavior becomes non-finite here (angle += / 0), so keep only inner rejection.
this.mfix$outerRadiusSq = Long.MAX_VALUE;
return;
}
int maxCircle = this.mfix$computeMaxCircleIndex();
// max(dist): 4*i + i*maxCircle*6 + maxNoise
double maxDist = 4.0 * this.distance + (double)this.distance * maxCircle * 6.0 + maxNoise;
double safeOuterRadius = maxDist + MFIX_MAX_POSITION_ERROR;
this.mfix$outerRadiusSq = (long)Math.ceil(safeOuterRadius * safeOuterRadius);
}
/**
* Computes the highest ring index ({@code circle}) that vanilla can reach for this placement.
*
* This mirrors the spread/total update logic in
* {@link net.minecraft.world.level.chunk.ChunkGeneratorStructureState#generateRingPositions},
* but only tracks deterministic loop state (no RNG).
*/
@Unique
private int mfix$computeMaxCircleIndex() {
int ringSpread = this.spread;
int total = 0;
int circle = 0;
while (total + ringSpread < this.count) {
total += ringSpread;
circle++;
ringSpread += 2 * ringSpread / (circle + 1);
ringSpread = Math.min(ringSpread, this.count - total);
}
return circle;
}
/**
* @author embeddedt, GPT-5.3-Codex
* @reason Avoid calling getRingPositionsFor() when we know the current chunk lies outside the region where
* concentric placement can even happen. This is particularly helpful when creating new worlds, because we can
* avoid blocking on the slow noise computations within the spawn region around (0, 0).
*/
@Inject(method = "isPlacementChunk", at = @At("HEAD"), cancellable = true)
private void mfix$earlyRejectByRadius(ChunkGeneratorStructureState structureState, int x, int z,
CallbackInfoReturnable<Boolean> cir) {
long distSq = (long)x * x + (long)z * z;
if (distSq < this.mfix$innerRadiusSq || distSq > this.mfix$outerRadiusSq) {
cir.setReturnValue(false);
}
}
}

View File

@ -24,7 +24,7 @@ public class ServerLevelMixin {
@Local(ordinal = 0, argsOnly = true) LevelStorageSource.LevelStorageAccess levelStorageAccess,
@Local(ordinal = 0, argsOnly = true) ResourceKey<Level> dimension,
@Local(ordinal = 0, argsOnly = true) MinecraftServer server) {
((IChunkGenerator)instance).mfix$setStrongholdCachePath(levelStorageAccess.getDimensionPath(dimension), server.registryAccess());
((IChunkGenerator)instance).mfix$setStrongholdCachePath(levelStorageAccess.getDimensionPath(dimension), server);
original.call(instance);
}
}

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;
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.world.level.block.state.BlockState;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.annotation.RequiresMod;
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.Redirect;
@Mixin(value = SectionCompiler.class, priority = 2000)
@Mixin(targets = { "net/minecraft/client/renderer/chunk/ChunkRenderDispatcher$RenderChunk$RebuildTask"}, priority = 2000)
@ClientOnlyMixin
@RequiresMod("!fluidlogged")
public class RebuildTaskMixin {
@ -21,4 +23,13 @@ public class RebuildTaskMixin {
private Iterable<BlockPos> fastBetweenClosed(BlockPos firstPos, BlockPos secondPos) {
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,7 +1,6 @@
package org.embeddedt.modernfix.common.mixin.perf.compact_bit_storage;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.level.chunk.PaletteResize;
import net.minecraft.world.level.chunk.PalettedContainer;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.Opcodes;
@ -41,7 +40,7 @@ public abstract class PalettedContainerMixin<T> {
return;
}
this.data = this.createOrReuseData(null, 0);
this.data.palette().idFor(value, (PaletteResize<T>)this);
this.data.palette().idFor(value);
}
}
}

View File

@ -22,7 +22,6 @@ public class BlockStateDataMixin {
* @author embeddedt
* @reason Reduce memory use of these constant CompoundTags via aggressive interning.
*/
/*
@ModifyExpressionValue(method = "parse", at = @At(value = "INVOKE", target = "Lnet/minecraft/nbt/TagParser;parseTag(Ljava/lang/String;)Lnet/minecraft/nbt/CompoundTag;"))
private static CompoundTag compactTag(CompoundTag tag) {
if (TAG_INTERNER == null) {
@ -41,8 +40,6 @@ public class BlockStateDataMixin {
return new CompoundTag(Map.ofEntries(entries));
}
*/
@Inject(method = "<clinit>", at = @At("RETURN"))
private static void clearInterner(CallbackInfo ci) {
if (TAG_INTERNER != null) {

View File

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

View File

@ -17,7 +17,7 @@ public class CreateWorldScreenMixin {
return ModernFix.resourceReloadExecutor();
}
@ModifyArg(method = "openCreateWorldScreen", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/WorldLoader;load(Lnet/minecraft/server/WorldLoader$InitConfig;Lnet/minecraft/server/WorldLoader$WorldDataSupplier;Lnet/minecraft/server/WorldLoader$ResultFactory;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"), index = 3)
@ModifyArg(method = "openFresh", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/WorldLoader;load(Lnet/minecraft/server/WorldLoader$InitConfig;Lnet/minecraft/server/WorldLoader$WorldDataSupplier;Lnet/minecraft/server/WorldLoader$ResultFactory;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"), index = 3)
private static Executor getCreationExecutorService(Executor e) {
return ModernFix.resourceReloadExecutor();
}

View File

@ -1,6 +1,5 @@
package org.embeddedt.modernfix.common.mixin.perf.dedicated_reload_executor;
import net.minecraft.TracingExecutor;
import net.minecraft.client.Minecraft;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
@ -8,11 +7,13 @@ import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.util.concurrent.ExecutorService;
@Mixin(Minecraft.class)
@ClientOnlyMixin
public class MinecraftMixin {
@Redirect(method = { "<init>", "reloadResourcePacks(ZLnet/minecraft/client/Minecraft$GameLoadCookie;)Ljava/util/concurrent/CompletableFuture;" }, at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Util;backgroundExecutor()Lnet/minecraft/TracingExecutor;", ordinal = 0))
private TracingExecutor getResourceReloadExecutor() {
@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() {
return ModernFix.resourceReloadExecutor();
}
}

View File

@ -10,7 +10,7 @@ import java.util.concurrent.Executor;
@Mixin(MinecraftServer.class)
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;Ljava/util/List;Lnet/minecraft/world/flag/FeatureFlagSet;Lnet/minecraft/commands/Commands$CommandSelection;Lnet/minecraft/server/permissions/PermissionSet;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"), index = 6)
@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) {
return ModernFix.resourceReloadExecutor();
}

View File

@ -1,54 +1,66 @@
package org.embeddedt.modernfix.common.mixin.perf.deduplicate_wall_shapes;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.WallBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.Property;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.joml.Vector3d;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
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.List;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author embeddedt
* @reason Avoid excessive memory usage in modpacks that add many variations of wall blocks. The strategy here requires
* multiple memoization maps, but is also the simplest way to do it without having to literally replicate the vanilla
* logic (as that uses blockstates rather than property maps).
* Most wall blocks use the default set of vanilla properties, and the default sizes for their shapes. This means
* there is no need to reconstruct a separate VoxelShape instance for each wall, we can just repurpose the
* same shape instances. To do this we can cache a mapping between a state (represented only as its prop->value map)
* and the desired shape, and generate the BlockState->VoxelShape map from this for each block.
*/
@Mixin(WallBlock.class)
public class WallBlockMixin {
@Unique
private static final ConcurrentHashMap<Vector3d, VoxelShape> COLUMN_CACHE = new ConcurrentHashMap<>();
@Unique
private static final ConcurrentHashMap<List<Double>, VoxelShape> BOX_Z_CACHE = new ConcurrentHashMap<>();
@Unique
private static final ConcurrentHashMap<VoxelShape, Map<Direction, VoxelShape>> ROTATE_HORZ_CACHE = new ConcurrentHashMap<>();
@Unique
private static final ConcurrentHashMap<Pair<VoxelShape, VoxelShape>, VoxelShape> OR_CACHE = new ConcurrentHashMap<>();
public abstract class WallBlockMixin extends Block {
private static Map<ImmutableList<Float>, Pair<Map<ImmutableMap<Property<?>, Comparable<?>>, VoxelShape>, StateDefinition<Block, BlockState>>> CACHE_BY_SHAPE_VALS = new HashMap<>();
@WrapOperation(method = "makeShapes", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/Block;column(DDD)Lnet/minecraft/world/phys/shapes/VoxelShape;"))
private VoxelShape memoizeColumn(double sizeXZ, double minY, double postHeight, Operation<VoxelShape> original) {
return COLUMN_CACHE.computeIfAbsent(new Vector3d(sizeXZ, minY, postHeight), l -> original.call(sizeXZ, minY, postHeight));
public WallBlockMixin(Properties properties) {
super(properties);
}
@WrapOperation(method = "makeShapes", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/Block;boxZ(DDDDD)Lnet/minecraft/world/phys/shapes/VoxelShape;"))
private VoxelShape memoizeBoxZ(double sizeX, double minY, double maxY, double minZ, double maxZ, Operation<VoxelShape> original) {
return BOX_Z_CACHE.computeIfAbsent(List.of(sizeX, minY, maxY, minZ, maxZ), l -> original.call(sizeX, minY, maxY, minZ, maxZ));
@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) {
ImmutableList<Float> key = ImmutableList.of(f1, f2, f3, f4, f5, f6);
Pair<Map<ImmutableMap<Property<?>, Comparable<?>>, VoxelShape>, StateDefinition<Block, BlockState>> cache = CACHE_BY_SHAPE_VALS.get(key);
// require the properties to be identical
if(cache == null || !cache.getSecond().getProperties().equals(this.stateDefinition.getProperties()))
return;
ImmutableMap.Builder<BlockState, VoxelShape> builder = ImmutableMap.builder();
for(BlockState state : this.stateDefinition.getPossibleStates()) {
VoxelShape shape = cache.getFirst().get(state.getValues());
if(shape == null)
return; // fallback to vanilla logic
builder.put(state, shape);
}
cir.setReturnValue(builder.build());
}
@WrapOperation(method = "makeShapes", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/shapes/Shapes;rotateHorizontal(Lnet/minecraft/world/phys/shapes/VoxelShape;)Ljava/util/Map;"))
private Map<Direction, VoxelShape> memoizeRotateHorizontal(VoxelShape north, Operation<Map<Direction, VoxelShape>> original) {
return ROTATE_HORZ_CACHE.computeIfAbsent(north, l -> original.call(north));
}
@WrapOperation(method = "lambda$makeShapes$0", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/shapes/Shapes;or(Lnet/minecraft/world/phys/shapes/VoxelShape;Lnet/minecraft/world/phys/shapes/VoxelShape;)Lnet/minecraft/world/phys/shapes/VoxelShape;"))
private static VoxelShape memoizeOr(VoxelShape one, VoxelShape two, Operation<VoxelShape> original) {
return OR_CACHE.computeIfAbsent(Pair.of(one, two), l -> original.call(one, two));
@Inject(method = "makeShapes", at = @At("RETURN"))
private synchronized void storeCachedShapesByProperty(float f1, float f2, float f3, float f4, float f5, float f6, CallbackInfoReturnable<Map<BlockState, VoxelShape>> cir) {
// never populate cache as a non-vanilla block
if((Class<?>)this.getClass() != WallBlock.class)
return;
ImmutableList<Float> key = ImmutableList.of(f1, f2, f3, f4, f5, f6);
if(!CACHE_BY_SHAPE_VALS.containsKey(key)) {
Map<ImmutableMap<Property<?>, Comparable<?>>, VoxelShape> cacheByProperties = new HashMap<>();
Map<BlockState, VoxelShape> shapeMap = cir.getReturnValue();
for(Map.Entry<BlockState, VoxelShape> entry : shapeMap.entrySet()) {
cacheByProperties.put(entry.getKey().getValues(), entry.getValue());
}
CACHE_BY_SHAPE_VALS.put(key, Pair.of(cacheByProperties, this.stateDefinition));
}
}
}

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;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.mojang.datafixers.DataFixerBuilder;
import com.mojang.datafixers.DSL;
import com.mojang.datafixers.DataFixer;
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.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.Set;
@Mixin(DataFixers.class)
public class DataFixersMixin {
@ModifyExpressionValue(method = "<clinit>", at = @At(value = "INVOKE", target = "Lcom/mojang/datafixers/DataFixerBuilder;build()Lcom/mojang/datafixers/DataFixerBuilder$Result;"))
private static DataFixerBuilder.Result setupMapBlasting(DataFixerBuilder.Result original) {
DFUBlaster.blastMaps();
return original;
public abstract class DataFixersMixin {
@Shadow protected static DataFixer createFixerUpper(Set<DSL.TypeReference> set) {
throw new AssertionError();
}
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

@ -28,11 +28,9 @@ public class ClientLanguageMixin {
* @reason collect the list of all known language resources
*/
@WrapOperation(method = "loadFrom", at = @At(value = "INVOKE",
target = "Lnet/minecraft/client/resources/language/ClientLanguage;appendFrom(Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;)V"))
target = "Lnet/minecraft/client/resources/language/ClientLanguage;appendFrom(Ljava/lang/String;Ljava/util/List;Ljava/util/Map;)V"))
private static void collectResources(String languageName, List<Resource> resources,
Map<String, String> destinationMap,
Map<String, net.minecraft.network.chat.Component> componentMap,
Operation<Void> original,
Map<String, String> destinationMap, Operation<Void> original,
@Share("usedResources") LocalRef<List<Resource>> usedResources) {
List<Resource> collected = usedResources.get();
if (collected == null) {
@ -40,14 +38,14 @@ public class ClientLanguageMixin {
usedResources.set(collected);
}
collected.addAll(resources);
original.call(languageName, resources, destinationMap, componentMap);
original.call(languageName, resources, destinationMap);
}
/**
* @author embeddedt
* @reason figure out which keys are dynamically loaded and which are injected by mixins
*/
@ModifyArg(method = "loadFrom", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/language/ClientLanguage;<init>(Ljava/util/Map;ZLjava/util/Map;)V"), index = 0)
@ModifyArg(method = "loadFrom", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/language/ClientLanguage;<init>(Ljava/util/Map;Z)V"), index = 0)
private static Map<String, String> modifyLanguageMap(Map<String, String> storage, @Share("usedResources") LocalRef<List<Resource>> usedResources) {
List<Resource> collected = Objects.requireNonNullElse(usedResources.get(), List.of());
return DynamicLanguageMap.forVanillaData(storage, collected);

View File

@ -0,0 +1,74 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import net.minecraft.client.renderer.block.BlockModelShaper;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.IModelHoldingBlockState;
import org.embeddedt.modernfix.dynamicresources.ModelLocationCache;
import org.embeddedt.modernfix.util.DynamicOverridableMap;
import org.spongepowered.asm.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(BlockModelShaper.class)
@ClientOnlyMixin
public class BlockModelShaperMixin {
@Shadow @Final private ModelManager modelManager;
@Shadow
private Map<BlockState, BakedModel> modelByStateCache;
@Inject(method = { "<init>", "replaceCache" }, at = @At("RETURN"))
private void replaceModelMap(CallbackInfo ci) {
// replace the backing map for mods which will access it
this.modelByStateCache = new DynamicOverridableMap<>(state -> modelManager.getModel(ModelLocationCache.get(state)));
// Clear the cached models on blockstate objects
for(Block block : BuiltInRegistries.BLOCK) {
for(BlockState state : block.getStateDefinition().getPossibleStates()) {
if(state instanceof IModelHoldingBlockState modelHolder) {
modelHolder.mfix$setModel(null);
}
}
}
}
private BakedModel cacheBlockModel(BlockState state) {
// Do all model system accesses in the unlocked path
ModelResourceLocation mrl = ModelLocationCache.get(state);
BakedModel model = mrl == null ? null : modelManager.getModel(mrl);
if (model == null) {
model = modelManager.getMissingModel();
}
return model;
}
/**
* @author embeddedt
* @reason get the model from the dynamic model provider
*/
@Overwrite
public BakedModel getBlockModel(BlockState state) {
if(state instanceof IModelHoldingBlockState modelHolder) {
BakedModel model = modelHolder.mfix$getModel();
if(model != null) {
return model;
}
model = this.cacheBlockModel(state);
modelHolder.mfix$setModel(model);
return model;
} else {
return this.cacheBlockModel(state);
}
}
}

View File

@ -1,6 +1,6 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import net.minecraft.client.renderer.block.dispatch.BlockStateModel;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.world.level.block.state.BlockBehaviour;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.IModelHoldingBlockState;
@ -10,17 +10,17 @@ import java.lang.ref.SoftReference;
@Mixin(BlockBehaviour.BlockStateBase.class)
@ClientOnlyMixin
public class MixinBlockState implements IModelHoldingBlockState {
private volatile SoftReference<BlockStateModel> mfix$model;
public class BlockStateBaseMixin implements IModelHoldingBlockState {
private volatile SoftReference<BakedModel> mfix$model;
@Override
public BlockStateModel mfix$getModel() {
public BakedModel mfix$getModel() {
var ref = mfix$model;
return ref != null ? ref.get() : null;
}
@Override
public void mfix$setModel(BlockStateModel model) {
public void mfix$setModel(BakedModel model) {
mfix$model = model != null ? new SoftReference<>(model) : null;
}
}

View File

@ -1,21 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import net.minecraft.client.resources.model.BlockStateDefinitions;
import net.minecraft.resources.Identifier;
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.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import java.util.Map;
@Mixin(BlockStateDefinitions.class)
@ClientOnlyMixin
public interface BlockStateDefinitionsAccessor {
@Accessor("STATIC_DEFINITIONS")
static Map<Identifier, StateDefinition<Block, BlockState>> getStaticDefinitions() {
throw new AssertionError();
}
}

View File

@ -0,0 +1,70 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.google.common.base.Stopwatch;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.client.ForgeHooksClient;
import net.minecraftforge.client.event.ModelEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.ModContainer;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.ModLoader;
import net.minecraftforge.fml.util.ObfuscationReflectionHelper;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.dynamicresources.DynamicBakedModelProvider;
import org.embeddedt.modernfix.forge.dynresources.ModelBakeEventHelper;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Mixin(ForgeHooksClient.class)
public class ForgeHooksClientMixin {
/**
* 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/minecraftforge/fml/ModLoader;postEvent(Lnet/minecraftforge/eventbus/api/Event;)V"), remap = false)
private static void postNamespacedKeySetEvent(ModLoader loader, Event event) {
if(!ModLoader.isLoadingStateValid())
return;
ModelEvent.ModifyBakingResult bakeEvent = ((ModelEvent.ModifyBakingResult)event);
Stopwatch globalTimer = Stopwatch.createStarted();
Stopwatch selfTimer = Stopwatch.createStarted();
ModelBakeEventHelper helper = new ModelBakeEventHelper(bakeEvent.getModels());
selfTimer.stop();
Method acceptEv = ObfuscationReflectionHelper.findMethod(ModContainer.class, "acceptEvent", Event.class);
Map<String, Stopwatch> times = new Object2ObjectOpenHashMap<>();
times.put("modernfix", selfTimer);
ModList.get().forEachModContainer((id, mc) -> {
Map<ResourceLocation, BakedModel> newRegistry = helper.wrapRegistry(id);
ModelEvent.ModifyBakingResult postedEvent = new ModelEvent.ModifyBakingResult(newRegistry, bakeEvent.getModelBakery());
Stopwatch timer = times.computeIfAbsent(id, $ -> Stopwatch.createUnstarted());
timer.start();
try {
acceptEv.invoke(mc, postedEvent);
} catch(ReflectiveOperationException e) {
e.printStackTrace();
}
timer.stop();
});
globalTimer.stop();
if (globalTimer.elapsed(TimeUnit.SECONDS) >= 1) {
ModernFix.LOGGER.warn("Posting dynamic ModelEvent.ModifyBakingResult to mods took {}, breakdown below:", globalTimer);
times.entrySet().stream()
.sorted(Comparator.<Map.Entry<String, Stopwatch>, Duration>comparing(e -> e.getValue().elapsed()).reversed())
.filter(e -> e.getValue().elapsed(TimeUnit.MILLISECONDS) > 50)
.forEach(entry -> {
ModernFix.LOGGER.warn(" {}: {}", entry.getKey(), entry.getValue().toString());
});
}
if (bakeEvent.getModels() instanceof DynamicBakedModelProvider dynamicProvider) {
dynamicProvider.dumpStats();
}
}
}

View File

@ -1,12 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import it.unimi.dsi.fastutil.objects.Reference2IntMap;
import net.minecraft.core.IdMapper;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(IdMapper.class)
public interface IdMapperAccessor<T> {
@Accessor("tToId")
Reference2IntMap<T> getReferenceMap();
}

View File

@ -0,0 +1,93 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import net.minecraft.client.renderer.ItemModelShaper;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.core.Holder;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraftforge.client.model.ForgeItemModelShaper;
import net.minecraftforge.registries.ForgeRegistries;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.dynamicresources.DynamicModelCache;
import org.embeddedt.modernfix.dynamicresources.ModelLocationCache;
import org.embeddedt.modernfix.util.ItemMesherMap;
import org.spongepowered.asm.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.HashMap;
import java.util.Map;
@Mixin(ForgeItemModelShaper.class)
@ClientOnlyMixin
public abstract class ItemModelMesherForgeMixin extends ItemModelShaper {
@Shadow(remap = false) @Final @Mutable private Map<Holder.Reference<Item>, ModelResourceLocation> locations;
private Map<Holder.Reference<Item>, ModelResourceLocation> overrideLocations;
private final DynamicModelCache<Holder.Reference<Item>> mfix$modelCache = new DynamicModelCache<>(k -> this.mfix$getModelSlow((Holder.Reference<Item>)k), true);
public ItemModelMesherForgeMixin(ModelManager arg) {
super(arg);
}
private static final ModelResourceLocation SENTINEL = new ModelResourceLocation(new ResourceLocation("modernfix", "sentinel"), "sentinel");
@Inject(method = "<init>", at = @At("RETURN"))
private void replaceLocationMap(CallbackInfo ci) {
overrideLocations = new HashMap<>();
// need to replace this map because mods query locations through it
locations = new ItemMesherMap<>(this::mfix$getLocationForge);
}
@Unique
private ModelResourceLocation mfix$getLocationForge(Holder.Reference<Item> item) {
ModelResourceLocation map = overrideLocations.getOrDefault(item, SENTINEL);
if(map == SENTINEL) {
/* generate the appropriate location from our cache */
map = ModelLocationCache.get(item.get());
}
return map;
}
private BakedModel mfix$getModelSlow(Holder.Reference<Item> key) {
ModelResourceLocation map = mfix$getLocationForge(key);
return map == null ? null : getModelManager().getModel(map);
}
/**
* @author embeddedt
* @reason Get the stored location for that item and meta, and get the model
* from that location from the model manager.
**/
@Overwrite
@Override
public BakedModel getItemModel(Item item) {
return this.mfix$modelCache.get(ForgeRegistries.ITEMS.getDelegateOrThrow(item));
}
/**
* @author embeddedt
* @reason Don't get all models during init (with dynamic loading, that would
* generate them all). Just store location instead.
**/
@Overwrite
@Override
public void register(Item item, ModelResourceLocation location) {
overrideLocations.put(ForgeRegistries.ITEMS.getDelegateOrThrow(item), location);
}
/**
* @author embeddedt
* @reason Disable cache rebuilding (with dynamic loading, that would generate
* all models).
**/
@Overwrite
@Override
public void rebuildCache() {
this.mfix$modelCache.clear();
}
}

View File

@ -0,0 +1,91 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import net.minecraft.client.renderer.ItemModelShaper;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.dynamicresources.DynamicModelCache;
import org.embeddedt.modernfix.dynamicresources.ModelLocationCache;
import org.embeddedt.modernfix.util.DynamicInt2ObjectMap;
import org.spongepowered.asm.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.HashMap;
import java.util.Map;
@Mixin(ItemModelShaper.class)
@ClientOnlyMixin
public abstract class ItemModelShaperMixin {
@Shadow public abstract ModelManager getModelManager();
@Shadow @Final @Mutable private Int2ObjectMap<BakedModel> shapesCache;
private Map<Item, ModelResourceLocation> overrideLocationsVanilla;
public ItemModelShaperMixin() {
super();
}
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);
@Inject(method = "<init>", at = @At("RETURN"))
private void replaceLocationMap(CallbackInfo ci) {
overrideLocationsVanilla = new HashMap<>();
this.shapesCache = new DynamicInt2ObjectMap<>(index -> getModelManager().getModel(ModelLocationCache.get(Item.byId(index))));
}
@Unique
private ModelResourceLocation mfix$getLocation(Item item) {
ModelResourceLocation map = overrideLocationsVanilla.getOrDefault(item, SENTINEL_VANILLA);
if(map == SENTINEL_VANILLA) {
/* generate the appropriate location from our cache */
map = ModelLocationCache.get(item);
}
return map;
}
private BakedModel mfix$getModelForItem(Item item) {
ModelResourceLocation map = mfix$getLocation(item);
return map == null ? null : getModelManager().getModel(map);
}
/**
* @author embeddedt
* @reason Get the stored location for that item and meta, and get the model
* from that location from the model manager.
**/
@Overwrite
public BakedModel getItemModel(Item item) {
return this.mfix$itemModelCache.get(item);
}
/**
* @author embeddedt
* @reason Don't get all models during init (with dynamic loading, that would
* generate them all). Just store location instead.
**/
@Overwrite
public void register(Item item, ModelResourceLocation location) {
overrideLocationsVanilla.put(item, location);
}
/**
* @author embeddedt
* @reason Disable cache rebuilding (with dynamic loading, that would generate
* all models).
**/
@Overwrite
public void rebuildCache() {
this.mfix$itemModelCache.clear();
}
}

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

@ -0,0 +1,22 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import net.minecraft.client.renderer.ItemModelShaper;
import net.minecraft.client.renderer.entity.ItemRenderer;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.world.item.Item;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ItemRenderer.class)
@ClientOnlyMixin
public class ItemRendererMixin {
/**
* Don't waste space putting all these locations into the cache, compute them on demand later.
*/
@Redirect(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/ItemModelShaper;register(Lnet/minecraft/world/item/Item;Lnet/minecraft/client/resources/model/ModelResourceLocation;)V"))
private void skipDefaultRegistration(ItemModelShaper shaper, Item item, ModelResourceLocation mrl) {
}
}

View File

@ -1,41 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.resources.model.BlockStateModelLoader;
import net.minecraft.resources.Identifier;
import net.minecraft.server.packs.resources.Resource;
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.dynresources.DynamicModelSystem;
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.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;
@Mixin(BlockStateModelLoader.class)
@ClientOnlyMixin
public abstract class MixinBlockStateModelLoader {
@Shadow
protected static BlockStateModelLoader.LoadedModels lambda$loadBlockStates$2(Map.Entry<Identifier, List<Resource>> entry, Function<Identifier, StateDefinition<Block, BlockState>> locationToBlockStateMapper) {
throw new AssertionError();
}
/**
* @author embeddedt
* @reason Load blockstate model definitions dynamically instead of all at once
*/
@ModifyArg(method = "loadBlockStates", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenCompose(Ljava/util/function/Function;)Ljava/util/concurrent/CompletableFuture;"))
private static Function<Map<Identifier, List<Resource>>, ? extends CompletionStage<BlockStateModelLoader.LoadedModels>> skipAOTBlockStateLoad(Function<Map<Identifier, List<Resource>>, ? extends CompletionStage<Map<Identifier, BlockStateModelLoader.LoadedModels>>> original, @Local(ordinal = 0) Function<Identifier, StateDefinition<Block, BlockState>> mapper) {
return resourceMap -> CompletableFuture.completedFuture(DynamicModelSystem.createDynamicBlockStateLoadedModels(resourceMap, (id, resources) -> {
return lambda$loadBlockStates$2(Map.entry(id, resources), mapper);
}));
}
}

View File

@ -1,42 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.multiplayer.ClientRegistryLayer;
import net.minecraft.client.resources.model.ClientItemInfoLoader;
import net.minecraft.core.RegistryAccess;
import net.minecraft.resources.Identifier;
import net.minecraft.server.packs.resources.Resource;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.dynresources.DynamicModelSystem;
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.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;
@Mixin(ClientItemInfoLoader.class)
@ClientOnlyMixin
public abstract class MixinClientItemInfoLoader {
@Shadow
private static ClientItemInfoLoader.PendingLoad lambda$scheduleLoad$3(Identifier resourceId, Resource resource, RegistryAccess.Frozen registries) {
throw new AssertionError();
}
/**
* @author embeddedt
* @reason Load client item infos dynamically instead of all at once
*/
@ModifyArg(method = "scheduleLoad", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenCompose(Ljava/util/function/Function;)Ljava/util/concurrent/CompletableFuture;"))
private static Function<Map<Identifier, Resource>, ? extends CompletionStage<ClientItemInfoLoader.LoadedClientInfos>> skipAOTClientItemLoad(
Function<Map<Identifier, Resource>, ? extends CompletionStage<ClientItemInfoLoader.LoadedClientInfos>> original,
@Local(ordinal = 0) RegistryAccess.Frozen staticRegistries) {
return resourceMap -> CompletableFuture.completedFuture(DynamicModelSystem.createDynamicClientInfos(resourceMap, (resourceId, resource) -> {
ClientItemInfoLoader.PendingLoad load = lambda$scheduleLoad$3(resourceId, resource, staticRegistries);
return load.clientItemInfo();
}));
}
}

View File

@ -1,27 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import net.minecraft.client.renderer.item.ItemModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.resources.Identifier;
import net.neoforged.neoforge.client.ClientNeoForgeMod;
import org.embeddedt.modernfix.ModernFix;
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.Redirect;
@Mixin(ClientNeoForgeMod.class)
@ClientOnlyMixin
public class MixinClientNeoForgeMod {
/**
* @author embeddedt
* @reason avoid triggering eager load of every item model
*/
@Redirect(method = "lambda$new$7", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelManager;getItemModel(Lnet/minecraft/resources/Identifier;)Lnet/minecraft/client/renderer/item/ItemModel;"))
private static ItemModel checkExistenceWithoutLoadingModel(ModelManager instance, Identifier id) {
if (!((ModelManagerAccessor)instance).mfix$getBakedItemModels().containsKey(id)) {
ModernFix.LOGGER.warn("Missing item model '{}'", id);
}
return null;
}
}

View File

@ -1,60 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.google.common.collect.Maps;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import net.minecraft.client.renderer.item.ClientItem;
import net.minecraft.client.resources.model.ModelBakery;
import net.minecraft.resources.Identifier;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.dynresources.DynamicModelSystem;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.ModifyConstant;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.Slice;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
@Mixin(ModelBakery.class)
@ClientOnlyMixin
public class MixinModelBakery {
/**
* @author embeddedt
* @reason Create dynamic baked registry with cache instead of baking all entries from the input map at once
*/
@Redirect(method = "bakeModels", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/thread/ParallelMapTransform;schedule(Ljava/util/Map;Ljava/util/function/BiFunction;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"))
private <K, U, V> CompletableFuture<Map<K, V>> dynamicallyBake(Map<K, U> input, BiFunction<K, U, V> baker, Executor executor) {
return CompletableFuture.completedFuture(DynamicModelSystem.createDynamicBakedRegistry(input, baker));
}
@Redirect(method = "bakeModels",
slice = @Slice(from = @At(value = "FIELD", target = "Lnet/minecraft/client/resources/model/ModelBakery;clientInfos:Ljava/util/Map;", opcode = Opcodes.GETFIELD, ordinal = 2)),
at = @At(value = "INVOKE", target = "Ljava/util/Map;forEach(Ljava/util/function/BiConsumer;)V", ordinal = 0))
private void dynamicItemProperties(Map<Identifier, ClientItem> clientItems, BiConsumer<? super Identifier, ? super ClientItem> action,
@Local(name = "itemStackModelProperties") LocalRef<Map<Identifier, ClientItem.Properties>> modelProperties) {
modelProperties.set(Maps.asMap(clientItems.keySet(), id -> {
var item = clientItems.get(id);
var props = ClientItem.Properties.DEFAULT;
if (item != null && !props.equals(item.properties())) {
props = item.properties();
}
return props;
}));
}
/**
* @author embeddedt
* @reason We want log4j to print the stacktrace and not just the exception message
*/
@ModifyConstant(method = "lambda$bakeModels$0", constant = @Constant(stringValue = "Unable to bake model: '{}': {}"))
private static String showFullException(String prefix) {
return "Unable to bake model: '{}'";
}
}

View File

@ -1,100 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.minecraft.client.color.block.BlockColors;
import net.minecraft.client.renderer.block.dispatch.BlockStateModel;
import net.minecraft.client.resources.model.BlockStateModelLoader;
import net.minecraft.client.resources.model.ClientItemInfoLoader;
import net.minecraft.client.resources.model.ModelDiscovery;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ResolvedModel;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.client.resources.model.cuboid.ItemModelGenerator;
import net.minecraft.resources.Identifier;
import net.minecraft.server.packs.resources.Resource;
import net.minecraft.world.level.block.state.BlockState;
import net.neoforged.neoforge.client.model.standalone.StandaloneModelLoader;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.dynresources.BlockStateModelMap;
import org.embeddedt.modernfix.dynresources.DynamicModelSystem;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
import java.util.function.Function;
@Mixin(ModelManager.class)
@ClientOnlyMixin
public class MixinModelManager {
/**
* @author embeddedt
* @reason Instead of loading all unbaked models from the resource packs at once, create a dynamic map backed by
* a cache that loads them on demand
*/
@ModifyArg(method = "loadBlockModels", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenCompose(Ljava/util/function/Function;)Ljava/util/concurrent/CompletableFuture;"))
private static Function<Map<Identifier, Resource>, ? extends CompletionStage<Map<Identifier, UnbakedModel>>> skipAOTUnbakedModelLoad(Function<Map<Identifier, Resource>, ? extends CompletionStage<Map<Identifier, UnbakedModel>>> original) {
return resourceMap -> CompletableFuture.completedFuture(DynamicModelSystem.createDynamicUnbakedModelMap(resourceMap));
}
/**
* @author embeddedt
* @reason Stop all models from being loaded at startup by the model resolution logic.
*/
@Redirect(
method = "discoverModelDependencies(Ljava/util/Map;Lnet/minecraft/client/resources/model/BlockStateModelLoader$LoadedModels;Lnet/minecraft/client/resources/model/ClientItemInfoLoader$LoadedClientInfos;Lnet/neoforged/neoforge/client/model/standalone/StandaloneModelLoader$LoadedModels;)Lnet/minecraft/client/resources/model/ModelManager$ResolvedModels;",
at = @At(value = "INVOKE", target = "Ljava/util/Collection;forEach(Ljava/util/function/Consumer;)V"))
private static <T> void skipAddingRoot(Collection<T> instance, Consumer<T> consumer) {
}
/**
* @author embeddedt, coredex
* @reason Divert to our dynamic resolver. It is cleaner to overwrite the whole method, but this seems to cause
* a conflict with Sodium.
*/
@Redirect(
method = "discoverModelDependencies(Ljava/util/Map;Lnet/minecraft/client/resources/model/BlockStateModelLoader$LoadedModels;Lnet/minecraft/client/resources/model/ClientItemInfoLoader$LoadedClientInfos;Lnet/neoforged/neoforge/client/model/standalone/StandaloneModelLoader$LoadedModels;)Lnet/minecraft/client/resources/model/ModelManager$ResolvedModels;",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelDiscovery;resolve()Ljava/util/Map;")
)
private static Map<Identifier, ResolvedModel> useDynamicResolverMap(
ModelDiscovery discovery,
Map<Identifier, UnbakedModel> allModels,
BlockStateModelLoader.LoadedModels blockStateModels,
ClientItemInfoLoader.LoadedClientInfos itemInfos,
StandaloneModelLoader.LoadedModels standaloneModels
) {
UnbakedModel generatedItemModel;
var generatedItemWrapper = ((ModelDiscoveryAccessor) discovery).mfix$getModelWrappers().get(ItemModelGenerator.GENERATED_ITEM_MODEL_ID);
if (generatedItemWrapper != null) {
generatedItemModel = generatedItemWrapper.wrapped();
} else {
generatedItemModel = new ItemModelGenerator();
}
return new DynamicModelSystem.DynamicResolver(allModels, blockStateModels, itemInfos, standaloneModels, generatedItemModel).resolvedModelsMap();
}
/**
* @author embeddedt
* @reason Build the model groups dynamically
*/
@Overwrite
private static Object2IntMap<BlockState> buildModelGroups(BlockColors blockColors, BlockStateModelLoader.LoadedModels loadedModels) {
return new DynamicModelSystem.BlockGroupingMap(blockColors, loadedModels);
}
/**
* @author embeddedt
* @reason avoid copying inner map
*/
@Overwrite
private static Map<BlockState, BlockStateModel> createBlockStateToModelDispatch(Map<BlockState, BlockStateModel> blockStateModels, BlockStateModel missingModel) {
BlockStateModelMap.resetCache();
return new BlockStateModelMap(blockStateModels, missingModel);
}
}

View File

@ -0,0 +1,102 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
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.resources.model.*;
import net.minecraft.resources.ResourceLocation;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.ModernFixClient;
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.dynamicresources.ModelMissingException;
import org.embeddedt.modernfix.forge.dynresources.IModelBakerImpl;
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.CallbackInfoReturnable;
import java.util.function.Function;
@Mixin(value = ModelBakery.ModelBakerImpl.class, priority = 600)
@ClientOnlyMixin
public abstract class ModelBakerImplMixin implements IModelBakerImpl, IExtendedModelBaker {
private static final boolean debugDynamicModelLoading = Boolean.getBoolean("modernfix.debugDynamicModelLoading");
@Shadow(aliases = {"this$0","f_243927_"}) @Final private ModelBakery field_40571;
private boolean mfix$ignoreCache = false;
@Shadow @Final private Function<Material, TextureAtlasSprite> modelTextureGetter;
@Override
public void mfix$ignoreCache() {
mfix$ignoreCache = true;
}
private boolean throwIfMissing;
@Override
public boolean throwOnMissingModel(boolean flag) {
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);
}
}
}
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;
}
}

View File

@ -0,0 +1,412 @@
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.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.Minecraft;
import net.minecraft.client.color.block.BlockColors;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.*;
import net.minecraft.resources.ResourceLocation;
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.ModernFixClient;
import org.embeddedt.modernfix.api.entrypoint.ModernFixClientIntegration;
import org.embeddedt.modernfix.duck.IExtendedModelBaker;
import org.embeddedt.modernfix.dynamicresources.DynamicBakedModelProvider;
import org.embeddedt.modernfix.duck.IExtendedModelBakery;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
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.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
/* low priority so that our injectors are added after other mods' */
@Mixin(value = ModelBakery.class, priority = 1100)
@ClientOnlyMixin
public abstract class ModelBakeryMixin implements IExtendedModelBakery {
private static final boolean debugDynamicModelLoading = Boolean.getBoolean("modernfix.debugDynamicModelLoading");
@Shadow @Final @Mutable public Map<ResourceLocation, UnbakedModel> unbakedCache;
@Shadow @Final public static ModelResourceLocation MISSING_MODEL_LOCATION;
@Shadow protected abstract BlockModel loadBlockModel(ResourceLocation location) throws IOException;
@Shadow @Final private Set<ResourceLocation> loadingStack;
@Shadow protected abstract void loadModel(ResourceLocation blockstateLocation) throws Exception;
@Shadow @Final @Mutable
private Map<ResourceLocation, BakedModel> bakedTopLevelModels;
@Shadow @Final @Mutable private Map<ModelBakery.BakedCacheKey, BakedModel> bakedCache;
@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;
@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) {
fabric_enableGetOrLoadModelGuard = false;
this.blockColors = val;
this.loadedBakedModels = CacheBuilder.newBuilder()
.expireAfterAccess(ModelBakeryHelpers.MAX_MODEL_LIFETIME_SECS, TimeUnit.SECONDS)
.maximumSize(ModelBakeryHelpers.MAX_BAKED_MODEL_COUNT)
.concurrencyLevel(8)
.removalListener(this::onModelRemoved)
.softValues()
.build();
this.loadedModels = CacheBuilder.newBuilder()
.expireAfterAccess(ModelBakeryHelpers.MAX_MODEL_LIFETIME_SECS, TimeUnit.SECONDS)
.maximumSize(ModelBakeryHelpers.MAX_UNBAKED_MODEL_COUNT)
.concurrencyLevel(8)
.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;
}
@Override
public UnbakedModel put(ResourceLocation key, UnbakedModel value) {
smallLoadingCache.put(key, value);
return super.put(key, value);
}
};
this.bakedTopLevelModels = new DynamicBakedModelProvider((ModelBakery)(Object)this, bakedCache);
}
@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 Map<ResourceLocation, StateDefinition<Block, BlockState>> ignoreFutureModelLoads(Map<ResourceLocation, StateDefinition<Block, BlockState>> original) {
this.ignoreModelLoad = true;
return original;
}
private <K, V> void onModelRemoved(RemovalNotification<K, V> notification) {
if(!debugDynamicModelLoading)
return;
// 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();
if (reason == RemovalCause.REPLACED || reason == RemovalCause.EXPLICIT) {
return;
}
Object k = notification.getKey();
if(k == null)
return;
ResourceLocation rl;
boolean baked = false;
if(k instanceof ResourceLocation) {
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
* @reason don't actually load most models
*/
@Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelBakery;loadTopLevel(Lnet/minecraft/client/resources/model/ModelResourceLocation;)V"))
private void addTopLevelFile(ModelBakery bakery, ModelResourceLocation location) {
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
public BakedModel bakeDefault(ResourceLocation modelLocation, ModelState state) {
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

@ -1,15 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import net.minecraft.client.resources.model.ModelDiscovery;
import net.minecraft.resources.Identifier;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(ModelDiscovery.class)
@ClientOnlyMixin
public interface ModelDiscoveryAccessor {
@Accessor("modelWrappers")
Object2ObjectMap<Identifier, ModelDiscovery.ModelWrapper> mfix$getModelWrappers();
}

View File

@ -1,17 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import net.minecraft.client.renderer.item.ItemModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.resources.Identifier;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import java.util.Map;
@Mixin(ModelManager.class)
@ClientOnlyMixin
public interface ModelManagerAccessor {
@Accessor("bakedItemStackModels")
Map<Identifier, ItemModel> mfix$getBakedItemModels();
}

View File

@ -0,0 +1,97 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelBakery;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.Resource;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.util.GsonHelper;
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.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.util.CacheUtil;
import org.embeddedt.modernfix.util.LambdaMap;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executor;
import java.util.function.Function;
import java.util.stream.Collectors;
@Mixin(ModelManager.class)
@ClientOnlyMixin
public class ModelManagerMixin {
@Shadow private Map<ResourceLocation, BakedModel> bakedRegistry;
@Inject(method = "<init>", at = @At("RETURN"))
private void injectDummyBakedRegistry(CallbackInfo ci) {
if(this.bakedRegistry == null) {
this.bakedRegistry = new HashMap<>();
}
}
@ModifyArg(method = "loadBlockModels", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenCompose(Ljava/util/function/Function;)Ljava/util/concurrent/CompletableFuture;", ordinal = 0), index = 0)
private static Function<Map<ResourceLocation, Resource>, ? extends CompletionStage<Map<ResourceLocation, BlockModel>>> deferBlockModelLoad(Function<Map<ResourceLocation, Resource>, ? extends CompletionStage<Map<ResourceLocation, BlockModel>>> fn, @Local(ordinal = 0, argsOnly = true) ResourceManager manager) {
return resourceMap -> {
var fallbackModel = BlockModel.fromString(ModelBakery.MISSING_MODEL_MESH);
var cache = CacheUtil.<ResourceLocation, BlockModel>simpleCacheForLambda(location -> loadSingleBlockModel(manager, location, fallbackModel), 100L);
return CompletableFuture.completedFuture(Maps.asMap(Set.copyOf(resourceMap.keySet()), location -> cache.getUnchecked(location)));
};
}
@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<ModelBakery.LoadedJson>>> deferBlockStateLoad(ResourceManager manager, Executor executor) {
var cache = CacheUtil.<ResourceLocation, List<ModelBakery.LoadedJson>>simpleCacheForLambda(location -> loadSingleBlockState(manager, location), 100L);
return CompletableFuture.completedFuture(new LambdaMap<>(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;"))
private ImmutableList<BlockState> skipCollection(StateDefinition<Block, BlockState> definition) {
return ImmutableList.of();
}
private static BlockModel loadSingleBlockModel(ResourceManager manager, ResourceLocation location, BlockModel fallbackModel) {
return manager.getResource(location).map(resource -> {
try (BufferedReader reader = resource.openAsReader()) {
return BlockModel.fromStream(reader);
} catch (Exception e) {
// We must return some nonnull value to avoid breaking the map convention. The easiest solution
// is to just return a missing model template.
ModernFix.LOGGER.error("Couldn't load model {}, substituting missing", location, e);
return fallbackModel;
}
}).orElse(null);
}
private List<ModelBakery.LoadedJson> loadSingleBlockState(ResourceManager manager, ResourceLocation location) {
return manager.getResourceStack(location).stream().map(resource -> {
try (BufferedReader reader = resource.openAsReader()) {
return new ModelBakery.LoadedJson(resource.sourcePackId(), GsonHelper.parse(reader));
} catch(IOException e) {
ModernFix.LOGGER.error("Couldn't load blockstate", e);
return null;
}
}).filter(Objects::nonNull).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,16 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources.ctm;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelBakery;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import java.util.Map;
@Mixin(ModelBakery.class)
@ClientOnlyMixin
public interface CTMModelBakeryAccessor {
@Accessor("bakedCache")
Map<ModelBakery.BakedCacheKey, BakedModel> mfix$getBakedCache();
}

View File

@ -0,0 +1,125 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources.ctm;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
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.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.embeddedt.modernfix.forge.dynresources.IModelBakerImpl;
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 team.chisel.ctm.CTM;
import team.chisel.ctm.api.model.IModelCTM;
import team.chisel.ctm.client.model.AbstractCTMBakedModel;
import team.chisel.ctm.client.model.ModelCTM;
import team.chisel.ctm.client.texture.IMetadataSectionCTM;
import team.chisel.ctm.client.util.ResourceUtil;
import team.chisel.ctm.client.util.TextureMetadataHandler;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.*;
import java.util.function.Function;
@Mixin(TextureMetadataHandler.class)
@RequiresMod("ctm")
@ClientOnlyMixin
public abstract class TextureMetadataHandlerMixin implements ModernFixClientIntegration {
@Shadow(remap = false) @Nonnull protected abstract BakedModel wrap(UnbakedModel model, BakedModel object) throws IOException;
@Shadow(remap = false) @Final private Multimap<ResourceLocation, Material> scrapedTextures;
@Inject(method = "<init>", at = @At("RETURN"))
private void subscribeDynamic(CallbackInfo ci) {
ModernFixClient.CLIENT_INTEGRATIONS.add(this);
}
@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) {
ci.cancel();
}
@Override
public BakedModel onBakedModelLoad(ResourceLocation rl, UnbakedModel rootModel, BakedModel baked, ModelState state, ModelBakery bakery) {
if (rl instanceof ModelResourceLocation && !(baked instanceof AbstractCTMBakedModel) && !baked.isCustomRenderer()) {
Deque<ResourceLocation> dependencies = new ArrayDeque<>();
Set<ResourceLocation> seenModels = new HashSet<>();
dependencies.push(rl);
seenModels.add(rl);
boolean shouldWrap = false;
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
while (!shouldWrap && !dependencies.isEmpty()) {
ResourceLocation dep = dependencies.pop();
UnbakedModel model;
try {
model = dep == rl ? rootModel : bakery.getModel(dep);
} catch (Exception e) {
continue;
}
Collection<Material> textures = Sets.newHashSet(scrapedTextures.get(dep));
Collection<ResourceLocation> newDependencies = model.getDependencies();
for (Material tex : textures) {
IMetadataSectionCTM meta = null;
// Cache all dependent texture metadata
try {
meta = ResourceUtil.getMetadata(ResourceUtil.spriteToAbsolute(tex.texture())).orElse(null); // TODO, lazy
} catch (IOException e) {} // Fallthrough
if (meta != null) {
// At least one texture has CTM metadata, so we should wrap this model
shouldWrap = true;
}
}
for (ResourceLocation newDep : newDependencies) {
if (seenModels.add(newDep)) {
dependencies.push(newDep);
}
}
}
if (shouldWrap) {
try {
baked = wrap(rootModel, baked);
handleInit(rl, baked, bakery);
dependencies.clear();
} catch (IOException e) {
CTM.logger.error("Could not wrap model " + rl + ". Aborting...", e);
}
}
}
return baked;
}
private void handleInit(ResourceLocation key, BakedModel wrappedModel, ModelBakery bakery) {
if(wrappedModel instanceof AbstractCTMBakedModel baked) {
IModelCTM var10 = baked.getModel();
if (var10 instanceof ModelCTM ctmModel) {
if (!ctmModel.isInitialized()) {
// Clear the baked cache as upstream CTM does
((CTMModelBakeryAccessor)bakery).mfix$getBakedCache().clear();
Function<Material, TextureAtlasSprite> spriteGetter = (m) -> {
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

@ -0,0 +1,102 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources.ldlib;
import com.lowdragmc.lowdraglib.LDLib;
import com.lowdragmc.lowdraglib.client.ClientProxy;
import com.lowdragmc.lowdraglib.client.forge.ClientProxyImpl;
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 net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.BakedModel;
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 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.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.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
@Mixin(ClientProxyImpl.class)
@ClientOnlyMixin
@RequiresMod("ldlib")
public abstract class ClientProxyImplMixin implements ModernFixClientIntegration {
@Inject(method = "<init>", at = @At("RETURN"))
private void registerIntegration(CallbackInfo ci) {
ModernFixClient.CLIENT_INTEGRATIONS.add(this);
}
@Redirect(method = "modelBake", at = @At(value = "INVOKE", target = "Ljava/util/Map;entrySet()Ljava/util/Set;", ordinal = 0), remap = false)
private Set<?> disableLoop(Map<?, ?> map) {
return Set.of();
}
@Override
public BakedModel onBakedModelLoad(ResourceLocation rl, UnbakedModel rootModel, BakedModel baked, ModelState state, ModelBakery bakery, Function<Material, TextureAtlasSprite> textureGetter) {
if (baked == null) {
return null;
}
if (rl instanceof ModelResourceLocation && rootModel != null) {
if (baked instanceof LDLRendererModel) {
return baked;
}
if (baked.isCustomRenderer()) { // Nothing we can add to builtin models
return baked;
}
Deque<ResourceLocation> dependencies = new ArrayDeque<>();
Set<ResourceLocation> seenModels = new HashSet<>();
dependencies.push(rl);
seenModels.add(rl);
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
while (!shouldWrap && !dependencies.isEmpty()) {
ResourceLocation dep = dependencies.pop();
UnbakedModel model;
try {
model = dep == rl ? rootModel : bakery.getModel(dep);
} catch (Exception e) {
continue;
}
try {
Set<Material> textures = new HashSet<>(ClientProxy.SCRAPED_TEXTURES.get(dep));
for (Material tex : textures) {
// Cache all dependent texture metadata
// At least one texture has CTM metadata, so we should wrap this baked
if (!LDLMetadataSection.getMetadata(LDLMetadataSection.spriteToAbsolute(tex.texture())).isMissing()) { // TODO lazy
shouldWrap = true;
break;
}
}
if (!shouldWrap) {
for (ResourceLocation newDep : model.getDependencies()) {
if (seenModels.add(newDep)) {
dependencies.push(newDep);
}
}
}
} catch (Exception e) {
LDLib.LOGGER.error("Error loading baked dependency {} for baked {}. Skipping...", dep, rl, e);
}
}
ClientProxy.WRAPPED_MODELS.put(rl, shouldWrap);
if (shouldWrap) {
return new CustomBakedModelImpl(baked);
}
}
return baked;
}
}

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