Merge remote-tracking branch 'origin/1.21.1' into 26.1

This commit is contained in:
embeddedt 2026-03-23 18:41:32 -04:00
commit 6a4e2810b4
No known key found for this signature in database
GPG Key ID: A69433EC199B5613
42 changed files with 1675 additions and 35 deletions

View File

@ -32,10 +32,3 @@ jobs:
with:
files: 'bin/*'
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Add changelog to release
uses: irongut/EditRelease@v1.2.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
id: ${{ github.event.release.id }}
replacebody: true
files: "CHANGELOG.md"

View File

@ -30,7 +30,7 @@ dependencies {
}
tasks.withType(JavaCompile) {
options.release = 21
options.release = 17
}
shadowJar {

View File

@ -90,24 +90,19 @@ public class ClientMixinValidator {
}
private boolean targetsClient(Object classTarget) {
return switch (classTarget) {
case TypeElement te ->
isClientMarked(te);
case TypeMirror tm -> {
var el = types.asElement(tm);
yield el != null ? targetsClient(el) : warn("TypeMirror of " + tm);
}
// If you're using a dollar sign in class names you are insane
case String s -> {
var te =
elemUtils.getTypeElement(toSourceString(s.split("\\$")[0]));
yield te != null ? targetsClient(te) : warn(s);
}
default ->
throw new IllegalArgumentException("Unhandled type: "
if (classTarget instanceof TypeElement te) {
return isClientMarked(te);
} else if (classTarget instanceof TypeMirror tm) {
var el = types.asElement(tm);
return el != null ? targetsClient(el) : warn("TypeMirror of " + tm);
} else if (classTarget instanceof String s) {
var te = elemUtils.getTypeElement(toSourceString(s.split("\\$")[0]));
return te != null ? targetsClient(te) : warn(s);
} else {
throw new IllegalArgumentException("Unhandled type: "
+ classTarget.getClass() + "\n" + "Stringified contents: "
+ classTarget.toString());
};
}
}
private boolean isClientMarked(TypeElement te) {

View File

@ -89,9 +89,6 @@ tasks.named<Jar>("jar") {
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
val curSourceCompatLevel = JavaVersion.VERSION_25
sourceCompatibility = curSourceCompatLevel
targetCompatibility = curSourceCompatLevel
@ -212,7 +209,8 @@ tasks.named("build") {
}
publishMods {
file.set(tasks.named<Jar>(finalJarTask).get().outputs.files.singleFile)
file.set(tasks.named<Jar>(finalJarTask).flatMap { it.archiveFile })
displayName.set(tasks.named<Jar>(finalJarTask).flatMap { it.archiveFileName })
changelog = "Please check the [GitHub wiki](https://github.com/embeddedt/ModernFix/wiki/Changelog) for major changes."
type = STABLE
@ -225,7 +223,7 @@ publishMods {
minecraftVersions.add(minecraft_version)
}
modrinth {
projectId = "modernfix"
projectId = "nmDcB62a"
accessToken = providers.environmentVariable("MODRINTH_TOKEN")
minecraftVersions.add(minecraft_version)
}

View File

@ -1 +0,0 @@
loom.platform=neoforge

View File

@ -0,0 +1,90 @@
package org.embeddedt.modernfix.common.mixin.bugfix.chunk_deadlock;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.CrashReport;
import net.minecraft.ReportedException;
import net.minecraft.server.MinecraftServer;
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 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.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Supplier;
@Mixin(ChunkStatusTasks.class)
public abstract class ChunkMapLoadMixin {
@Unique
private static final ThreadLocal<CompletableFuture<ChunkAccess>> MFIX_SURROGATE_FUTURE = new ThreadLocal<>();
/**
* @author embeddedt
* @reason This redirect makes several changes to how full chunk promotion works. First of all, promotion runs
* directly in the context of the main thread executor, rather than going through the priority sorter.
* This change allows attempts to load other chunks from within the promotion lambda to succeed (important
* for bad EntityJoinLevelEvent implementations to not deadlock the game). Second, it slightly alters the
* semantics of protoChunkToFullChunk so that the FULL chunk future will be completed before postload
* callbacks finish running. This change allows attempts to load the _same_ chunk in the promotion lambda to
* succeed, as otherwise the future would block waiting for itself to complete.
*
* <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>();
// 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($ -> {
// running on thread that executes lambda body
MFIX_SURROGATE_FUTURE.set(surrogate);
try {
return supplier.get();
} finally {
MFIX_SURROGATE_FUTURE.remove();
}
}, mainThreadExecutor).whenComplete((either, throwable) -> {
if (throwable != null) {
if (!surrogate.isDone()) {
surrogate.completeExceptionally(throwable);
} else {
// 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;
});
}
} else {
surrogate.complete(either);
}
});
// Return the surrogate
return surrogate;
}
/**
* @author embeddedt
* @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) {
var future = MFIX_SURROGATE_FUTURE.get();
if (future != null) {
future.complete(levelChunk);
}
}
}

View File

@ -0,0 +1,11 @@
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,45 @@
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.client.Minecraft;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.server.packs.resources.ReloadableResourceManager;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.neoforge.init.ModernFixForge;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@Mixin(ReloadableResourceManager.class)
@ClientOnlyMixin
public abstract class ReloadableResourceManagerMixin {
@Shadow
@Final
private PackType type;
@Shadow
public abstract void registerReloadListener(PreparableReloadListener listener);
/**
* @author embeddedt
* @reason complain loudly when reload listeners are being registered too late in a way that would cause
* concurrency issues, and prevent them from crashing the game
*/
@WrapMethod(method = "registerReloadListener")
private void checkCallingThread(PreparableReloadListener listener, Operation<Void> original) {
if (ModernFixForge.registryEventsFired && this.type == PackType.CLIENT_RESOURCES
&& (Object)this == Minecraft.getInstance().getResourceManager()
&& !Minecraft.getInstance().isSameThread()) {
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));
return;
}
original.call(listener);
}
}

View File

@ -1,8 +1,8 @@
package org.embeddedt.modernfix.common.mixin.core;
import net.minecraft.server.Bootstrap;
import org.embeddedt.modernfix.util.TimeFormatter;
import org.slf4j.Logger;
import org.embeddedt.modernfix.util.TimeFormatter;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.MixinEnvironment;
@ -23,6 +23,7 @@ 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();
}

View File

@ -0,0 +1,16 @@
package org.embeddedt.modernfix.common.mixin.core;
import net.neoforged.neoforge.registries.GameData;
import org.embeddedt.modernfix.neoforge.init.ModernFixForge;
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 = GameData.class, remap = false)
public class GameDataMixin {
@Inject(method = "postRegisterEvents", at = @At("RETURN"))
private static void markPosted(CallbackInfo ci) {
ModernFixForge.registryEventsFired = true;
}
}

View File

@ -0,0 +1,31 @@
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.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.Map;
@Mixin(AttributeSupplier.Builder.class)
public class AttributeSupplierBuilderMixin {
@Shadow
@Final
private Map<Holder<Attribute>, AttributeInstance> builder;
/**
* @author embeddedt
* @reason canonicalize identical AttributeInstance templates, many entities are created with the same values
*/
@Inject(method = "build", at = @At(value = "NEW", target = "(Ljava/util/Map;)Lnet/minecraft/world/entity/ai/attributes/AttributeSupplier;"))
private void deduplicateInstances(CallbackInfoReturnable<AttributeSupplier> cir) {
this.builder.replaceAll((a, i) -> AttributeInstanceTemplates.intern(i));
}
}

View File

@ -0,0 +1,33 @@
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.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Map;
@Mixin(AttributeSupplier.class)
public class AttributeSupplierMixin {
@Shadow
@Final
@Mutable
private Map<Holder<Attribute>, AttributeInstance> instances;
/**
* @author embeddedt
* @reason Java 9's Map.of() implementation is significantly more compact than ImmutableMap, and we do not
* care about insertion order in this context
*/
@Inject(method = "<init>", at = @At("RETURN"))
private void useCompactJavaMap(Map<Holder<Attribute>, AttributeInstance> instances, CallbackInfo ci) {
this.instances = Map.copyOf(this.instances);
}
}

View File

@ -0,0 +1,168 @@
package org.embeddedt.modernfix.common.mixin.perf.cache_strongholds;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
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.world.level.ChunkPos;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
import org.embeddedt.modernfix.ModernFix;
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 java.lang.ref.SoftReference;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@Mixin(ChunkGeneratorStructureState.class)
public class ChunkGeneratorMixin implements IChunkGenerator {
@Shadow
@Final
private long concentricRingsSeed;
@Shadow
@Final
private BiomeSource biomeSource;
private Path mfix$dimensionPath;
private RegistryAccess.Frozen mfix$registryAccess;
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) {
this.mfix$dimensionPath = cachePath;
this.mfix$registryAccess = registryAccess;
}
@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) {
return original.call(structureSet, placement);
}
String cacheKey = mfix$makeCacheKey(placement);
// Try reading from cache
List<ChunkPos> cached = mfix$readFromCache(cacheKey);
if (cached != null) {
ModernFix.LOGGER.debug("Using cached stronghold positions for {}", cacheKey);
return CompletableFuture.completedFuture(List.copyOf(cached));
}
return original.call(structureSet, placement).thenApplyAsync(positions -> {
mfix$writeToCache(cacheKey, positions);
return positions;
}, Util.ioPool());
}
private String mfix$makeCacheKey(ConcentricRingsStructurePlacement placement) {
RegistryOps<Tag> ops = RegistryOps.create(NbtOps.INSTANCE, this.mfix$registryAccess);
String placementKey = ConcentricRingsStructurePlacement.CODEC.codec().encodeStart(ops, placement)
.result().map(Tag::toString).orElse(null);
String biomeSourceKey = BiomeSource.CODEC.encodeStart(ops, this.biomeSource)
.result().map(Tag::toString).orElse(null);
if (placementKey == null || biomeSourceKey == null) {
ModernFix.LOGGER.warn("Failed to create cache key for concentric structure placement");
return null;
}
String data = placementKey + ";biomes=" + biomeSourceKey + ";seed=" + this.concentricRingsSeed;
try {
byte[] hash = MessageDigest.getInstance("SHA-256").digest(data.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder(64);
for (byte b : hash) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
return null;
}
}
private synchronized List<ChunkPos> mfix$readFromCache(String cacheKey) {
Map<String, List<ChunkPos>> cache = mfix$getOrLoadCache();
return cache.get(cacheKey);
}
private synchronized void mfix$writeToCache(String cacheKey, List<ChunkPos> positions) {
Map<String, List<ChunkPos>> cache = mfix$getOrLoadCache();
cache.put(cacheKey, List.copyOf(positions));
mfix$cachedPositions = new SoftReference<>(cache);
mfix$saveCacheFile(cache);
}
private Map<String, List<ChunkPos>> mfix$getOrLoadCache() {
Map<String, List<ChunkPos>> cache = mfix$cachedPositions.get();
if (cache != null) {
return cache;
}
cache = mfix$loadCacheFile();
mfix$cachedPositions = new SoftReference<>(cache);
return cache;
}
private Map<String, List<ChunkPos>> mfix$loadCacheFile() {
Path file = mfix$dimensionPath.resolve(CACHE_FILENAME);
if (!Files.exists(file)) {
return new HashMap<>();
}
try {
CompoundTag root = NbtIo.readCompressed(file, NbtAccounter.unlimitedHeap());
Map<String, List<ChunkPos>> result = new HashMap<>();
for (String key : root.keySet()) {
root.getIntArray(key).ifPresent(data -> {
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) {
positions.add(new ChunkPos(data[i], data[i + 1]));
}
result.put(key, positions);
}
});
}
return result;
} catch (Exception e) {
ModernFix.LOGGER.warn("Failed to read stronghold cache, will recompute", e);
return new HashMap<>();
}
}
private void mfix$saveCacheFile(Map<String, List<ChunkPos>> cache) {
CompoundTag root = new CompoundTag();
for (var entry : cache.entrySet()) {
List<ChunkPos> positions = entry.getValue();
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();
}
root.putIntArray(entry.getKey(), data);
}
Path file = mfix$dimensionPath.resolve(CACHE_FILENAME);
try {
NbtIo.writeCompressed(root, file);
} catch (Exception e) {
ModernFix.LOGGER.warn("Failed to write stronghold cache", e);
}
}
}

View File

@ -0,0 +1,30 @@
package org.embeddedt.modernfix.common.mixin.perf.cache_strongholds;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.embeddedt.modernfix.duck.IChunkGenerator;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(ServerLevel.class)
public class ServerLevelMixin {
/**
* @author embeddedt
* @reason Make the dimension path accessible to ChunkGeneratorStructureState.
*/
@WrapOperation(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/ChunkGeneratorStructureState;ensureStructuresGenerated()V"))
private void setCachePath(ChunkGeneratorStructureState instance, Operation<Void> original,
@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());
original.call(instance);
}
}

View File

@ -0,0 +1,22 @@
package org.embeddedt.modernfix.common.mixin.perf.compact_imposterprotochunks;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.lighting.ChunkSkyLightSources;
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;
@Mixin(ChunkAccess.class)
public class ChunkAccessMixin {
@Shadow
@Final
@Mutable
protected LevelChunkSection[] sections;
@Shadow
protected ChunkSkyLightSources skyLightSources;
}

View File

@ -0,0 +1,22 @@
package org.embeddedt.modernfix.common.mixin.perf.compact_imposterprotochunks;
import net.minecraft.world.level.chunk.ImposterProtoChunk;
import net.minecraft.world.level.chunk.LevelChunk;
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(ImposterProtoChunk.class)
public abstract class ImposterProtoChunkMixin extends ChunkAccessMixin {
/**
* @author embeddedt
* @reason ImposterProtoChunks allocate their own LevelChunkSection objects etc. which wastes quite
* a bit of memory
*/
@Inject(method = "<init>", at = @At("RETURN"))
private void replaceDuplicateObjects(LevelChunk wrapped, boolean allowWrites, CallbackInfo ci) {
this.sections = wrapped.getSections();
this.skyLightSources = wrapped.getSkyLightSources();
}
}

View File

@ -36,7 +36,7 @@ public class BlockStateDataMixin {
t = compactTag(ct);
}
t = TAG_INTERNER.addOrGet(t);
entries[i++] = Map.entry(key, t);
entries[i++] = Map.entry(key.intern(), t);
}
return new CompoundTag(Map.ofEntries(entries));
}

View File

@ -0,0 +1,20 @@
package org.embeddedt.modernfix.common.mixin.perf.compress_unihex_font;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.mojang.serialization.MapCodec;
import net.minecraft.client.gui.font.providers.GlyphProviderDefinition;
import net.minecraft.client.gui.font.providers.GlyphProviderType;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.render.font.LazyGlyphProvider;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(GlyphProviderType.class)
@ClientOnlyMixin
public class GlyphProviderTypeMixin {
@ModifyExpressionValue(method = "<clinit>", at = @At(value = "FIELD", opcode = Opcodes.GETSTATIC, target = "Lnet/minecraft/client/gui/font/providers/UnihexProvider$Definition;CODEC:Lcom/mojang/serialization/MapCodec;"))
private static MapCodec<? extends GlyphProviderDefinition> lazyUnihex(MapCodec<? extends GlyphProviderDefinition> codec) {
return LazyGlyphProvider.wrap(codec);
}
}

View File

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

View File

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

View File

@ -0,0 +1,14 @@
package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules;
import net.minecraft.world.level.biome.BiomeManager;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(BiomeManager.class)
public interface BiomeManagerAccessor {
@Accessor("biomeZoomSeed")
long mfix$getZoomSeed();
@Accessor("noiseBiomeSource")
BiomeManager.NoiseBiomeSource mfix$getBiomeSource();
}

View File

@ -0,0 +1,19 @@
package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules;
import net.minecraft.world.level.levelgen.SurfaceRules;
import org.embeddedt.modernfix.world.gen.SurfaceRuleOptimizer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(targets = {"net/minecraft/world/level/levelgen/SurfaceRules$SequenceRuleSource"})
public class SequenceRuleSourceMixin {
@Inject(method = "apply(Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;)Lnet/minecraft/world/level/levelgen/SurfaceRules$SurfaceRule;", at = @At("HEAD"), cancellable = true)
private void optimizeApply(SurfaceRules.Context context, CallbackInfoReturnable<SurfaceRules.SurfaceRule> cir) {
var optimized = SurfaceRuleOptimizer.optimizeSequenceRule((SurfaceRules.SequenceRuleSource)(Object) this, context);
if (optimized != null) {
cir.setReturnValue(optimized);
}
}
}

View File

@ -0,0 +1,80 @@
package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.chunk.BlockColumn;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.NoiseChunk;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.SurfaceRules;
import net.minecraft.world.level.levelgen.SurfaceSystem;
import net.minecraft.world.level.levelgen.WorldGenerationContext;
import org.embeddedt.modernfix.world.gen.ChunkBiomeLookup;
import org.embeddedt.modernfix.world.gen.PrefetchingBlockColumn;
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.ModifyArg;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.function.Function;
@Mixin(SurfaceSystem.class)
public class SurfaceSystemMixin {
private static final ThreadLocal<ChunkBiomeLookup> MFIX_LOOKUP_CACHE = ThreadLocal.withInitial(ChunkBiomeLookup::new);
private static final ThreadLocal<PrefetchingBlockColumn> MFIX_BLOCK_COLUMN = new ThreadLocal<>();
@ModifyArg(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;<init>(Lnet/minecraft/world/level/levelgen/SurfaceSystem;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/world/level/levelgen/NoiseChunk;Ljava/util/function/Function;Lnet/minecraft/core/Registry;Lnet/minecraft/world/level/levelgen/WorldGenerationContext;)V"), index = 4)
private Function<BlockPos, Holder<Biome>> useFasterLookup(Function<BlockPos, Holder<Biome>> biomeGetter,
@Local(ordinal = 0, argsOnly = true) BiomeManager manager,
@Local(ordinal = 0, argsOnly = true) ChunkAccess chunk,
@Share("chunkBiomeLookup") LocalRef<ChunkBiomeLookup> lookupRef) {
var lookup = MFIX_LOOKUP_CACHE.get();
BiomeManagerAccessor accessor = (BiomeManagerAccessor)manager;
lookup.prepare(accessor.mfix$getBiomeSource(), accessor.mfix$getZoomSeed(), chunk, manager);
lookupRef.set(lookup);
return lookup;
}
@Inject(method = "buildSurface", at = @At("RETURN"))
private void finishAndDisposeLookups(RandomState randomState, BiomeManager biomeManager, Registry<Biome> biomes, boolean p_224652_, WorldGenerationContext context, ChunkAccess chunk, NoiseChunk noiseChunk, SurfaceRules.RuleSource ruleSource, CallbackInfo ci) {
MFIX_LOOKUP_CACHE.get().dispose();
var column = MFIX_BLOCK_COLUMN.get();
if (column != null) {
column.dispose();
}
}
@Redirect(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/biome/BiomeManager;getBiome(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/core/Holder;"))
private Holder<Biome> useFasterLookup(BiomeManager instance, BlockPos pos, @Share("chunkBiomeLookup") LocalRef<ChunkBiomeLookup> lookupRef) {
return lookupRef.get().apply(pos);
}
@Inject(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;<init>(Lnet/minecraft/world/level/levelgen/SurfaceSystem;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/world/level/levelgen/NoiseChunk;Ljava/util/function/Function;Lnet/minecraft/core/Registry;Lnet/minecraft/world/level/levelgen/WorldGenerationContext;)V"))
private void captureRealBlockColumn(CallbackInfo ci, @Local(ordinal = 0) LocalRef<BlockColumn> column,
@Local(ordinal = 0, argsOnly = true) ChunkAccess chunk,
@Share("prefetchColumn") LocalRef<PrefetchingBlockColumn> prefetchRef) {
var prefetchingBlockColumn = MFIX_BLOCK_COLUMN.get();
if (prefetchingBlockColumn == null || prefetchingBlockColumn.getExpectedHeight() != chunk.getHeight()) {
prefetchingBlockColumn = new PrefetchingBlockColumn(chunk.getHeight());
MFIX_BLOCK_COLUMN.set(prefetchingBlockColumn);
}
column.set(prefetchingBlockColumn);
prefetchRef.set(prefetchingBlockColumn);
}
@Inject(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/BlockPos$MutableBlockPos;setZ(I)Lnet/minecraft/core/BlockPos$MutableBlockPos;", ordinal = 0, shift = At.Shift.AFTER))
private void prefetchBlockArray(RandomState randomState, BiomeManager biomeManager, Registry<Biome> biomes, boolean p_224652_,
WorldGenerationContext context, ChunkAccess chunk, NoiseChunk noiseChunk, SurfaceRules.RuleSource ruleSource, CallbackInfo ci,
@Local(ordinal = 0) BlockColumn column,
@Local(ordinal = 0) BlockPos.MutableBlockPos cursor) {
((PrefetchingBlockColumn)column).prefetch(chunk, cursor.getX() & 15, cursor.getZ() & 15);
}
}

View File

@ -0,0 +1,74 @@
package org.embeddedt.modernfix.common.mixin.perf.release_protochunks;
import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ChunkLevel;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.server.level.FullChunkStatus;
import net.minecraft.server.level.GenerationChunkHolder;
import net.minecraft.world.level.ChunkPos;
import org.embeddedt.modernfix.duck.release_protochunks.IClearableChunkHolder;
import org.embeddedt.modernfix.duck.release_protochunks.ISuspendedHolderTrackingChunkMap;
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.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
@Mixin(ChunkHolder.class)
public abstract class ChunkHolderMixin extends GenerationChunkHolder implements IClearableChunkHolder {
@Shadow
private CompletableFuture<?> saveSync;
@Shadow
private int ticketLevel;
@Shadow
@Final
private ChunkHolder.PlayerProvider playerProvider;
public ChunkHolderMixin(ChunkPos pos) {
super(pos);
}
@Override
public void mfix$resetProtoChunkFutures() {
int len = this.futures.length();
for (int i = 0; i < len; i++) {
this.futures.set(i, null);
}
this.saveSync = CompletableFuture.completedFuture(null);
this.startedWork.set(null);
}
/*
* The methods below trigger the ChunkMap to check whether this holder can be "suspended" (have its ProtoChunk-only
* futures cleared) each time a new version of the chunkToSave future has completed. The ChunkMap itself
* also verifies that all conditions are still met for suspension in case the holder has become necessary
* again in the meantime.
*/
@Inject(method = "addSaveDependency", at = @At("RETURN"))
private void recheckSuspensionAfterNeighbor(CompletableFuture<?> future, CallbackInfo ci) {
this.mfix$markAsNeedingProtoChunkDrop();
}
@Inject(method = "updateFutures", at = @At("RETURN"))
private void markForSuspensionOnDemotion(ChunkMap chunkMap, Executor executor, CallbackInfo ci) {
this.mfix$markAsNeedingProtoChunkDrop();
}
private void mfix$markAsNeedingProtoChunkDrop() {
if (!ChunkLevel.fullStatus(this.ticketLevel).isOrAfter(FullChunkStatus.FULL)
&& ChunkLevel.isLoaded(this.ticketLevel)) {
// register for suspension check when chain completes
var map = ((ISuspendedHolderTrackingChunkMap)this.playerProvider);
this.saveSync.whenCompleteAsync((r, e) -> {
map.mfix$markForSuspensionCheck(this.pos);
}, map.mfix$getMainThreadExecutor());
}
}
}

View File

@ -0,0 +1,107 @@
package org.embeddedt.modernfix.common.mixin.perf.release_protochunks;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ChunkLevel;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.server.level.FullChunkStatus;
import net.minecraft.util.thread.BlockableEventLoop;
import net.minecraft.world.level.ChunkPos;
import org.embeddedt.modernfix.duck.release_protochunks.IClearableChunkHolder;
import org.embeddedt.modernfix.duck.release_protochunks.ISuspendedHolderTrackingChunkMap;
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.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BooleanSupplier;
@Mixin(ChunkMap.class)
public abstract class ChunkMapMixin implements ISuspendedHolderTrackingChunkMap {
private static final int MFIX$TICKS_TO_WAIT_BEFORE_SUSPENDING = 100;
@Shadow
@Final
public Long2ObjectLinkedOpenHashMap<ChunkHolder> updatingChunkMap;
@Shadow
@Final
private BlockableEventLoop<Runnable> mainThreadExecutor;
@Shadow
protected abstract void lambda$scheduleUnload$0(ChunkHolder holder, CompletableFuture<?> future, long chunkPos);
@Shadow
@Final
public Long2ObjectLinkedOpenHashMap<ChunkHolder> pendingUnloads;
private final Long2IntOpenHashMap mfix$protoChunksToDrop = new Long2IntOpenHashMap();
private int mfix$dropTickCounter = 0;
/**
* @author embeddedt
* @reason We keep track of ChunkHolders that only contain protochunks, and are not loaded to a full status.
* This hook unloads their contents once there are no generation tasks actively relying on the chunk.
*/
@Inject(method = "processUnloads(Ljava/util/function/BooleanSupplier;)V", at = @At("RETURN"))
private void dropProtoChunks(BooleanSupplier hasMoreTime, CallbackInfo ci) {
int suspended = 0;
int iterations = 0;
mfix$dropTickCounter++;
var dropIterator = mfix$protoChunksToDrop.long2IntEntrySet().fastIterator();
while (dropIterator.hasNext() && suspended < 50 && iterations < 500 && (hasMoreTime.getAsBoolean() || mfix$protoChunksToDrop.size() > 1000)) {
iterations++;
var entry = dropIterator.next();
long pos = entry.getLongKey();
ChunkHolder holder = this.updatingChunkMap.get(pos);
if (holder == null // already removed
|| ChunkLevel.fullStatus(holder.getTicketLevel()).isOrAfter(FullChunkStatus.FULL) // promoted to FULL
|| !ChunkLevel.isLoaded(holder.getTicketLevel()) // is going to be dropped through normal code path
) {
dropIterator.remove();
continue;
}
if (!holder.isReadyForSaving() // saveSync dependencies have not completed or chunk is still being referenced by another chunk for generation
) {
// Not safe to suspend yet, reset timer
entry.setValue(mfix$dropTickCounter);
continue;
}
if ((mfix$dropTickCounter - entry.getIntValue()) < MFIX$TICKS_TO_WAIT_BEFORE_SUSPENDING) {
// Chunk has not been idle for long enough, wait
continue;
}
// All generation work done, so we can suspend and remove from set
dropIterator.remove();
// Execute the logic inside scheduleUnload() inline, without delegating to a queue
// When this returns it is safe to release any data the ChunkHolder holds
this.pendingUnloads.put(pos, holder);
this.lambda$scheduleUnload$0(holder, holder.getSaveSyncFuture(), pos);
((IClearableChunkHolder)holder).mfix$resetProtoChunkFutures();
suspended++;
}
}
@Override
public void mfix$markForSuspensionCheck(ChunkPos pos) {
this.mfix$protoChunksToDrop.put(pos.pack(), this.mfix$dropTickCounter);
}
@Override
public Executor mfix$getMainThreadExecutor() {
return this.mainThreadExecutor;
}
}

View File

@ -0,0 +1,70 @@
package org.embeddedt.modernfix.common.mixin.perf.suspend_integrated_server_during_load;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.mojang.datafixers.DataFixer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.server.IntegratedServer;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.Services;
import net.minecraft.server.WorldStem;
import net.minecraft.server.level.progress.LevelLoadListener;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.world.level.gamerules.GameRules;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.suspend_integrated_server_during_load.IDeferrableIntegratedServer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import java.net.Proxy;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
@Mixin(IntegratedServer.class)
@ClientOnlyMixin
public abstract class IntegratedServerMixin extends MinecraftServer implements IDeferrableIntegratedServer {
@Shadow
private boolean paused;
private final AtomicBoolean mfix$hasPrimaryClientJoined = new AtomicBoolean(false);
public IntegratedServerMixin(Thread serverThread, LevelStorageSource.LevelStorageAccess storageSource, PackRepository packRepository, WorldStem worldStem, Optional<GameRules> gameRules, Proxy proxy, DataFixer fixerUpper, Services services, LevelLoadListener levelLoadListener, boolean propagatesCrashes) {
super(serverThread, storageSource, packRepository, worldStem, gameRules, proxy, fixerUpper, services, levelLoadListener, propagatesCrashes);
}
/**
* @author embeddedt
* @reason Wait to be finished processing all expensive packets (recipes, tags, etc.)
* before continuing to tick the integrated server.
*/
@WrapOperation(method = "tickServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;isPaused()Z", ordinal = 0))
private boolean preventTicks(Minecraft instance, Operation<Boolean> original) {
return !mfix$hasPrimaryClientJoined.get() || original.call(instance);
}
/**
* @author embeddedt
* @reason If waiting for a client connection to exist, we only need to tick the server connection,
* not the whole server as vanilla does.
*/
@WrapWithCondition(method = "tickServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;tickServer(Ljava/util/function/BooleanSupplier;)V", ordinal = 0))
private boolean preventRunningFullServerTick(MinecraftServer server, BooleanSupplier hasTimeLeft) {
if (this.paused && !mfix$hasPrimaryClientJoined.get()) {
var conn = this.getConnection();
if (conn != null) {
conn.tick();
}
return false;
}
return true;
}
@Override
public void mfix$markClientLoadFinished() {
mfix$hasPrimaryClientJoined.set(true);
}
}

View File

@ -0,0 +1,23 @@
package org.embeddedt.modernfix.common.mixin.perf.suspend_integrated_server_during_load;
import net.minecraft.network.Connection;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.CommonListenerCookie;
import net.minecraft.server.players.PlayerList;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.neoforge.packet.ClientLoadFinishedPayload;
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(PlayerList.class)
@ClientOnlyMixin
public class PlayerListMixin {
@Inject(method = "placeNewPlayer", at = @At("RETURN"))
private void sendConfigFinishedSentinelPacket(Connection connection, ServerPlayer player, CommonListenerCookie cookie, CallbackInfo ci) {
if (connection.isMemoryConnection()) {
player.connection.send(ClientLoadFinishedPayload.INSTANCE);
}
}
}

View File

@ -173,6 +173,7 @@ public class ModernFixEarlyConfig {
.put("mixin.feature.blockentity_incorrect_thread", false)
.put("mixin.perf.clear_mixin_classinfo", false)
.put("mixin.perf.deduplicate_climate_parameters", false)
.put("mixin.perf.faster_capabilities.bytecode_analysis", false)
.put("mixin.bugfix.packet_leak", false)
.put("mixin.perf.deduplicate_location", false)
.put("mixin.perf.dynamic_entity_renderers", false)
@ -182,6 +183,7 @@ public class ModernFixEarlyConfig {
.put("mixin.feature.spam_thread_dump", false)
.put("mixin.feature.disable_unihex_font", false)
.put("mixin.feature.remove_chat_signing", false)
.put("mixin.bugfix.skip_redundant_saves", false)
.put("mixin.feature.snapshot_easter_egg", true)
.put("mixin.feature.warn_missing_perf_mods", true)
.put("mixin.feature.spark_profile_launch", false)

View File

@ -1,7 +1,9 @@
package org.embeddedt.modernfix.duck;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.core.RegistryAccess;
import java.nio.file.Path;
public interface IChunkGenerator {
void mfix$setAssociatedServerLevel(ServerLevel level);
void mfix$setStrongholdCachePath(Path cachePath, RegistryAccess.Frozen registryAccess);
}

View File

@ -0,0 +1,10 @@
package org.embeddedt.modernfix.duck;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import org.apache.commons.lang3.tuple.Pair;
public interface ISpawnTrackingMinecraftServer {
Pair<ResourceKey<Level>, ChunkPos> mfix$getInitialStartTicketLocation();
}

View File

@ -0,0 +1,5 @@
package org.embeddedt.modernfix.duck.release_protochunks;
public interface IClearableChunkHolder {
void mfix$resetProtoChunkFutures();
}

View File

@ -0,0 +1,11 @@
package org.embeddedt.modernfix.duck.release_protochunks;
import net.minecraft.world.level.ChunkPos;
import java.util.concurrent.Executor;
public interface ISuspendedHolderTrackingChunkMap {
void mfix$markForSuspensionCheck(ChunkPos pos);
Executor mfix$getMainThreadExecutor();
}

View File

@ -0,0 +1,10 @@
package org.embeddedt.modernfix.duck.suspend_integrated_server_during_load;
import net.minecraft.resources.Identifier;
import org.embeddedt.modernfix.ModernFix;
public interface IDeferrableIntegratedServer {
Identifier CLIENT_LOAD_SENTINEL = Identifier.fromNamespaceAndPath(ModernFix.MODID, "mark_client_load_finished");
void mfix$markClientLoadFinished();
}

View File

@ -0,0 +1,42 @@
package org.embeddedt.modernfix.entity;
import it.unimi.dsi.fastutil.Hash;
import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
public class AttributeInstanceTemplates {
private static final ObjectOpenCustomHashSet<AttributeInstance> INTERNER = new ObjectOpenCustomHashSet<>(new Hash.Strategy<>() {
@Override
public int hashCode(AttributeInstance o) {
if (o == null) {
return 0;
}
int h = System.identityHashCode(o.getAttribute());
h = 31 * h + Double.hashCode(o.getBaseValue());
h = 31 * h + o.getModifiers().hashCode();
return h;
}
@Override
public boolean equals(AttributeInstance a, AttributeInstance b) {
if (a == b) {
return true;
}
if (a == null || b == null) {
return false;
}
return a.getAttribute() == b.getAttribute()
&& a.getBaseValue() == b.getBaseValue()
&& a.getModifiers().equals(b.getModifiers());
}
});
public static AttributeInstance intern(AttributeInstance a) {
if (a == null || a.getClass() != AttributeInstance.class) {
return a;
}
synchronized (INTERNER) {
return INTERNER.addOrGet(a);
}
}
}

View File

@ -1,6 +1,7 @@
package org.embeddedt.modernfix.neoforge.init;
import com.google.common.collect.ImmutableList;
import net.minecraft.client.Minecraft;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.item.Item;
@ -17,10 +18,14 @@ import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.server.ServerStartedEvent;
import net.neoforged.neoforge.event.server.ServerStoppedEvent;
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
import net.neoforged.neoforge.network.registration.HandlerThread;
import net.neoforged.neoforge.network.registration.PayloadRegistrar;
import net.neoforged.neoforge.registries.RegisterEvent;
import org.apache.commons.lang3.tuple.Pair;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.core.ModernFixMixinPlugin;
import org.embeddedt.modernfix.duck.suspend_integrated_server_during_load.IDeferrableIntegratedServer;
import org.embeddedt.modernfix.neoforge.packet.ClientLoadFinishedPayload;
import java.util.List;
@ -28,6 +33,7 @@ import java.util.List;
public class ModernFixForge {
private static ModernFix commonMod;
public static boolean launchDone = false;
public static boolean registryEventsFired = false;
public ModernFixForge(ModContainer modContainer, IEventBus modBus) {
commonMod = new ModernFix();
@ -74,7 +80,19 @@ public class ModernFixForge {
}
private void registerNetworkChannel(final RegisterPayloadHandlersEvent event) {
final PayloadRegistrar registrar = event.registrar("1").executesOn(HandlerThread.MAIN).optional();
registrar.playToClient(
ClientLoadFinishedPayload.TYPE,
ClientLoadFinishedPayload.STREAM_CODEC,
(payload, ctx) -> {
ctx.enqueueWork(() -> {
Minecraft mc = Minecraft.getInstance();
if (mc.hasSingleplayerServer()) {
((IDeferrableIntegratedServer)mc.getSingleplayerServer()).mfix$markClientLoadFinished();
}
});
});
}
@SubscribeEvent(priority = EventPriority.LOWEST)

View File

@ -0,0 +1,18 @@
package org.embeddedt.modernfix.neoforge.packet;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import org.embeddedt.modernfix.duck.suspend_integrated_server_during_load.IDeferrableIntegratedServer;
public enum ClientLoadFinishedPayload implements CustomPacketPayload {
INSTANCE;
public static final CustomPacketPayload.Type<ClientLoadFinishedPayload> TYPE = new CustomPacketPayload.Type<>(IDeferrableIntegratedServer.CLIENT_LOAD_SENTINEL);
public static final StreamCodec<RegistryFriendlyByteBuf, ClientLoadFinishedPayload> STREAM_CODEC = StreamCodec.unit(INSTANCE);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}

View File

@ -0,0 +1,99 @@
package org.embeddedt.modernfix.render.font;
import com.mojang.blaze3d.font.GlyphProvider;
import com.mojang.blaze3d.font.UnbakedGlyph;
import com.mojang.datafixers.util.Either;
import com.mojang.serialization.MapCodec;
import it.unimi.dsi.fastutil.ints.IntSet;
import net.minecraft.client.gui.font.providers.GlyphProviderDefinition;
import net.minecraft.client.gui.font.providers.GlyphProviderType;
import net.minecraft.server.packs.resources.ResourceManager;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.function.Function;
public class LazyGlyphProvider implements GlyphProvider {
private final GlyphProviderDefinition.Loader loader;
private final ResourceManager manager;
private SoftReference<GlyphProvider> innerProvider = new SoftReference<>(null);
LazyGlyphProvider(GlyphProviderDefinition.Loader loader, ResourceManager manager) {
this.loader = loader;
this.manager = manager;
}
@Override
public void close() {
// best effort
var prov = innerProvider.get();
if (prov != null) {
prov.close();
}
}
private synchronized @Nullable GlyphProvider getGlyphProvider() {
GlyphProvider prov = innerProvider.get();
if (prov == null) {
try {
prov = this.loader.load(this.manager);
} catch (IOException e) {
return null;
}
innerProvider = new SoftReference<>(prov);
}
return prov;
}
@Override
public @Nullable UnbakedGlyph getGlyph(int character) {
var prov = getGlyphProvider();
if (prov != null) {
return prov.getGlyph(character);
} else {
return null;
}
}
@Override
public IntSet getSupportedGlyphs() {
var prov = getGlyphProvider();
if (prov != null) {
return prov.getSupportedGlyphs();
} else {
return IntSet.of();
}
}
private static class Definition implements GlyphProviderDefinition {
private final GlyphProviderDefinition delegate;
public Definition(GlyphProviderDefinition delegate) {
this.delegate = delegate;
}
@Override
public GlyphProviderType type() {
return this.delegate.type();
}
@Override
public Either<Loader, Reference> unpack() {
return this.delegate.unpack().mapBoth(
loader -> resourceManager -> new LazyGlyphProvider(loader, resourceManager),
Function.identity()
);
}
@SuppressWarnings("unchecked")
public <T extends GlyphProviderDefinition> T delegate() {
return (T)this.delegate;
}
}
public static MapCodec<? extends GlyphProviderDefinition> wrap(MapCodec<? extends GlyphProviderDefinition> codec) {
return codec.xmap(Definition::new, Definition::delegate);
}
}

View File

@ -36,11 +36,26 @@ public class SparkLaunchProfiler {
private static ExecutorService executor = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setDaemon(true).setNameFormat("spark-modernfix-async-worker").build());
private static final SparkPlatform platform = new SparkPlatform(new ModernFixSparkPlugin());
private static final String ALLOW_SPARK_PROFILING_PROP = "modernfix.allowSparkProfiling";
private static final boolean USE_JAVA_SAMPLER_FOR_LAUNCH = !Boolean.getBoolean("modernfix.profileWithAsyncSampler");
private static final boolean ALLOW_SPARK_PROFILING = Boolean.getBoolean(ALLOW_SPARK_PROFILING_PROP);
private static final int SAMPLING_INTERVAL = Integer.getInteger("modernfix.profileSamplingIntervalMicroseconds", 4000);
private static final String THREAD_GROUPER = System.getProperty("modernfix.profileSamplingThreadGrouper", "by-pool");
private static boolean checkSparkProfilingAllowed() {
if (!ALLOW_SPARK_PROFILING) {
ModernFixMixinPlugin.instance.logger.fatal("To reduce excessive load on the Spark servers, you must set " +
"-D{}=true in your JVM arguments for profiling to proceed. Please do " +
"this and relaunch the game.", ALLOW_SPARK_PROFILING_PROP);
return false;
}
return true;
}
public static void start(String key) {
if (!checkSparkProfilingAllowed()) {
return;
}
if (!ongoingSamplers.containsKey(key)) {
Sampler sampler;
SamplerSettings settings = new SamplerSettings(SAMPLING_INTERVAL, ThreadDumper.ALL, ThreadGrouper.parseConfigSetting(THREAD_GROUPER).get(), -1, false, true);

View File

@ -0,0 +1,262 @@
package org.embeddedt.modernfix.world.gen;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.QuartPos;
import net.minecraft.util.LinearCongruentialGenerator;
import net.minecraft.util.Mth;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.chunk.ChunkAccess;
import java.util.Arrays;
import java.util.function.Function;
/**
* Drop-in replacement for {@code biomeManager::getBiome} in SurfaceSystem.buildSurface.
*
* <p>Pre-computes the Voronoi bias (fiddle) values and quart-resolution biome data for an
* entire chunk, then uses two optimizations:
* <ul>
* <li><b>Uniform check:</b> If all 8 Voronoi candidate cells for a given quart position
* hold the same biome, the Voronoi computation is skipped entirely.</li>
* <li><b>Pre-computed bias:</b> When the Voronoi is needed, the 48 LCG operations per block
* are replaced by array lookups of pre-computed fiddle values.</li>
* </ul>
*/
public class ChunkBiomeLookup implements Function<BlockPos, Holder<Biome>> {
@SuppressWarnings("unchecked")
private Holder<Biome>[] biomes = new Holder[0];
private double[] biasX = new double[0], biasY = new double[0], biasZ = new double[0];
private boolean[] uniform = new boolean[0];
private int qMinX, qMinY, qMinZ;
private int sizeX, sizeY, sizeZ;
private BiomeManager fallbackManager;
/**
* Pre-compute biome and bias data for the given chunk. Must be called before any
* {@link #getBiome} calls for positions within this chunk.
*
* @param source the underlying quart-resolution biome source (e.g. the chunk)
* @param biomeZoomSeed the obfuscated biome zoom seed from BiomeManager
*/
@SuppressWarnings("unchecked")
public void prepare(BiomeManager.NoiseBiomeSource source, long biomeZoomSeed, ChunkAccess chunk, BiomeManager fallback) {
int chunkMinX = chunk.getPos().getMinBlockX();
int chunkMinZ = chunk.getPos().getMinBlockZ();
int minBuildHeight = chunk.getMinY();
int maxBuildHeight = minBuildHeight + chunk.getHeight(); // exclusive
// BiomeManager.getBiome subtracts a 2-block offset before converting to quart coords,
// then considers quart and quart+1 as the 8 Voronoi candidates.
int biomeOffset = 2;
int minBlockX = chunkMinX - biomeOffset;
int maxBlockX = chunkMinX + 15 - biomeOffset;
int minBlockZ = chunkMinZ - biomeOffset;
int maxBlockZ = chunkMinZ + 15 - biomeOffset;
int minBlockY = minBuildHeight - biomeOffset;
int maxBlockY = maxBuildHeight - 1 - biomeOffset;
// Quart range: fromBlock(min) to fromBlock(max) + 1 (for the +1 Voronoi candidate)
this.qMinX = QuartPos.fromBlock(minBlockX);
int qMaxX = QuartPos.fromBlock(maxBlockX) + 1;
this.qMinZ = QuartPos.fromBlock(minBlockZ);
int qMaxZ = QuartPos.fromBlock(maxBlockZ) + 1;
this.qMinY = QuartPos.fromBlock(minBlockY);
int qMaxY = QuartPos.fromBlock(maxBlockY) + 1;
this.sizeX = qMaxX - qMinX + 1; // always 6 for 16-wide chunks
this.sizeY = qMaxY - qMinY + 1;
this.sizeZ = qMaxZ - qMinZ + 1;
int totalCells = sizeX * sizeY * sizeZ;
// Reuse arrays across chunks if large enough
if (biomes.length < totalCells) {
biomes = new Holder[totalCells];
biasX = new double[totalCells];
biasY = new double[totalCells];
biasZ = new double[totalCells];
uniform = new boolean[totalCells];
}
// Fetch quart-resolution biomes
boolean isSingleBiome = !fetchBiomes(source);
if (isSingleBiome) {
// All cells hold the same biome, so no need to do expensive computations.
Arrays.fill(uniform, 0, totalCells, true);
} else {
this.computeUniformity();
this.computeBiases(biomeZoomSeed);
}
this.fallbackManager = fallback;
}
public void dispose() {
// Make sure we do not retain strong references to the biome holders
Arrays.fill(biomes, null);
}
private boolean fetchBiomes(BiomeManager.NoiseBiomeSource source) {
var biomes = this.biomes;
Holder<Biome> firstSeen = null;
boolean seenMultiple = false;
for (int rx = 0; rx < sizeX; rx++) {
int wx = qMinX + rx;
for (int rz = 0; rz < sizeZ; rz++) {
int wz = qMinZ + rz;
for (int ry = 0; ry < sizeY; ry++) {
int wy = qMinY + ry;
var biome = source.getNoiseBiome(wx, wy, wz);
biomes[index(rx, ry, rz)] = biome;
if (biome != firstSeen) {
if (firstSeen == null) {
firstSeen = biome;
} else {
seenMultiple = true;
}
}
}
}
}
return seenMultiple;
}
private void computeUniformity() {
// For each quart position, check if all 8 Voronoi candidates hold the same biome.
// If so, the Voronoi result is guaranteed to be that biome regardless of fractional
// position, so we can skip the distance computation entirely.
var uniform = this.uniform;
int sizeX = this.sizeX, sizeY = this.sizeY, sizeZ = this.sizeZ;
for (int rx = 0; rx < sizeX - 1; rx++) {
for (int rz = 0; rz < sizeZ - 1; rz++) {
for (int ry = 0; ry < sizeY - 1; ry++) {
uniform[index(rx, ry, rz)] = isUniform(rx, ry, rz);
}
}
}
}
private void computeBiases(long biomeZoomSeed) {
int sizeX = this.sizeX, sizeY = this.sizeY, sizeZ = this.sizeZ;
int qMinX = this.qMinX, qMinY = this.qMinY, qMinZ = this.qMinZ;
// Pre-compute bias (fiddle) values for the Voronoi distance computation.
for (int rx = 0; rx < sizeX; rx++) {
int wx = qMinX + rx;
for (int rz = 0; rz < sizeZ; rz++) {
int wz = qMinZ + rz;
for (int ry = 0; ry < sizeY; ry++) {
computeBias(index(rx, ry, rz), biomeZoomSeed, wx, qMinY + ry, wz);
}
}
}
}
private void computeBias(int idx, long seed, int x, int y, int z) {
// Reproduces the LCG chain from BiomeManager.getFiddledDistance exactly
long s = LinearCongruentialGenerator.next(seed, x);
s = LinearCongruentialGenerator.next(s, y);
s = LinearCongruentialGenerator.next(s, z);
s = LinearCongruentialGenerator.next(s, x);
s = LinearCongruentialGenerator.next(s, y);
s = LinearCongruentialGenerator.next(s, z);
biasX[idx] = getFiddle(s);
s = LinearCongruentialGenerator.next(s, seed);
biasY[idx] = getFiddle(s);
s = LinearCongruentialGenerator.next(s, seed);
biasZ[idx] = getFiddle(s);
}
private static double getFiddle(long seed) {
double d = (double) Math.floorMod(seed >> 24, 1024) / 1024.0D;
return (d - 0.5D) * 0.9D;
}
private boolean isUniform(int rx, int ry, int rz) {
var biomes = this.biomes;
Holder<Biome> ref = biomes[index(rx, ry, rz)];
for (int dx = 0; dx <= 1; dx++) {
for (int dy = 0; dy <= 1; dy++) {
for (int dz = 0; dz <= 1; dz++) {
if (biomes[index(rx + dx, ry + dy, rz + dz)] != ref) {
return false;
}
}
}
}
return true;
}
private int index(int rx, int ry, int rz) {
return (rx * sizeY + ry) * sizeZ + rz;
}
@Override
public Holder<Biome> apply(BlockPos pos) {
return getBiome(pos);
}
public Holder<Biome> getBiome(BlockPos pos) {
int i = pos.getX() - 2;
int j = pos.getY() - 2;
int k = pos.getZ() - 2;
int rx = QuartPos.fromBlock(i) - qMinX;
int ry = QuartPos.fromBlock(j) - qMinY;
int rz = QuartPos.fromBlock(k) - qMinZ;
if (rx < 0 || rx >= sizeX - 1 || ry < 0 || ry >= sizeY - 1 || rz < 0 || rz >= sizeZ - 1) {
return fallbackManager.getBiome(pos);
}
int baseIdx = index(rx, ry, rz);
if (uniform[baseIdx]) {
return biomes[baseIdx];
}
return getBiomeWithVoronoi(i, j, k, rx, ry, rz);
}
private Holder<Biome> getBiomeWithVoronoi(int i, int j, int k, int rx, int ry, int rz) {
var biasX = this.biasX;
var biasY = this.biasY;
var biasZ = this.biasZ;
double d0 = (double) QuartPos.quartLocal(i) / 4.0D;
double d1 = (double) QuartPos.quartLocal(j) / 4.0D;
double d2 = (double) QuartPos.quartLocal(k) / 4.0D;
int closestIdx = 0;
double closestDist = Double.POSITIVE_INFINITY;
for (int c = 0; c < 8; c++) {
boolean fx = (c & 4) == 0;
boolean fy = (c & 2) == 0;
boolean fz = (c & 1) == 0;
int idx = index(
rx + (fx ? 0 : 1),
ry + (fy ? 0 : 1),
rz + (fz ? 0 : 1)
);
double dx = (fx ? d0 : d0 - 1.0D) + biasX[idx];
double dy = (fy ? d1 : d1 - 1.0D) + biasY[idx];
double dz = (fz ? d2 : d2 - 1.0D) + biasZ[idx];
double dist = Mth.square(dx) + Mth.square(dy) + Mth.square(dz);
if (dist < closestDist) {
closestDist = dist;
closestIdx = idx;
}
}
return biomes[closestIdx];
}
}

View File

@ -0,0 +1,126 @@
package org.embeddedt.modernfix.world.gen;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.BlockColumn;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.levelgen.Heightmap;
/**
* Wraps a BlockColumn and prefetches all block states for a column into an array.
*
* <p>Writes bypass {@link ChunkAccess#setBlockState} and go directly to the section,
* skipping heightmap and light updates that are unnecessary during the surface building
* stage (which runs before {@code INITIALIZE_LIGHT}).
*/
public class PrefetchingBlockColumn implements BlockColumn {
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
private static final BlockState VOID_AIR = Blocks.VOID_AIR.defaultBlockState();
private final BlockState[] states;
private final BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
private ChunkAccess chunk;
private LevelChunkSection[] sections;
private int minBuildHeight;
private int localX, localZ;
public PrefetchingBlockColumn(int height) {
this.states = new BlockState[height];
}
public int getExpectedHeight() {
return this.states.length;
}
/**
* Prefetch all block states for the column at the given local XZ coordinates.
* Must be called before any getBlock/setBlock calls for this column.
*/
public void prefetch(ChunkAccess chunk, int localX, int localZ) {
if (chunk.getHeight() != this.states.length) {
throw new IllegalStateException();
}
this.chunk = chunk;
this.sections = chunk.getSections();
this.minBuildHeight = chunk.getMinY();
this.localX = localX;
this.localZ = localZ;
var sections = this.sections;
var states = this.states;
int offset = 0;
for (LevelChunkSection section : sections) {
if (section.hasOnlyAir()) {
for (int y = 0; y < 16; y++) {
states[offset + y] = AIR;
}
} else {
var container = section.getStates();
for (int y = 0; y < 16; y++) {
states[offset + y] = container.get(localX, y, localZ);
}
}
offset += 16;
}
}
/**
* Clear cached references to allow GC of the chunk between uses.
*/
public void dispose() {
this.chunk = null;
this.sections = null;
}
@Override
public BlockState getBlock(int y) {
int idx = y - minBuildHeight;
if (idx >= 0 && idx < states.length) {
return states[idx];
}
return VOID_AIR;
}
private void markPostprocessing(int y) {
cursor.set(
SectionPos.sectionToBlockCoord(chunk.getPos().x(), localX),
y,
SectionPos.sectionToBlockCoord(chunk.getPos().z(), localZ)
);
chunk.markPosForPostprocessing(cursor);
}
private void updateHeightmap(Heightmap.Types type, int y, BlockState newState) {
chunk.getOrCreateHeightmapUnprimed(type).update(localX, y, localZ, newState);
}
@Override
public void setBlock(int y, BlockState state) {
int idx = y - minBuildHeight;
var states = this.states;
if (idx >= 0 && idx < states.length) {
BlockState oldState = states[idx];
if (oldState == state) {
return;
}
states[idx] = state;
// Write directly to the section, bypassing ProtoChunk.setBlockState which
// does expensive heightmap updates that are not needed during surface building.
int sectionIdx = idx >> 4;
sections[sectionIdx].setBlockState(localX, y & 15, localZ, state, false);
if (!state.getFluidState().isEmpty()) {
markPostprocessing(y);
}
// Update heightmaps if the air/motion-blocking properties changed.
if (oldState.isAir() != state.isAir()) {
updateHeightmap(Heightmap.Types.WORLD_SURFACE_WG, y, state);
}
if (oldState.blocksMotion() != state.blocksMotion()) {
updateHeightmap(Heightmap.Types.OCEAN_FLOOR_WG, y, state);
}
}
}
}

View File

@ -0,0 +1,100 @@
package org.embeddedt.modernfix.world.gen;
import it.unimi.dsi.fastutil.objects.Reference2ObjectMaps;
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import net.minecraft.core.Holder;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.SurfaceRules;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class SurfaceRuleOptimizer {
public static @Nullable SurfaceRules.SurfaceRule optimizeSequenceRule(SurfaceRules.SequenceRuleSource source, SurfaceRules.Context context) {
// First pass: collect which biomes appear and count biome-gated branches
Reference2ObjectOpenHashMap<ResourceKey<Biome>, List<SurfaceRules.RuleSource>> perBiomeSources = new Reference2ObjectOpenHashMap<>();
int biomeGatedBranches = 0;
for (var innerSource : source.sequence()) {
if (innerSource instanceof SurfaceRules.TestRuleSource testRuleSource
&& testRuleSource.ifTrue() instanceof SurfaceRules.BiomeConditionSource biomeConditionSource) {
biomeGatedBranches++;
for (var biome : biomeConditionSource.biomes) {
perBiomeSources.putIfAbsent(biome, new ArrayList<>());
}
}
}
if (biomeGatedBranches < 3) {
return null;
}
// Second pass: build per-biome source lists preserving original interleaving order
List<SurfaceRules.RuleSource> noMatchSources = new ArrayList<>();
for (var innerSource : source.sequence()) {
if (innerSource instanceof SurfaceRules.TestRuleSource testRuleSource
&& testRuleSource.ifTrue() instanceof SurfaceRules.BiomeConditionSource biomeConditionSource) {
// Add the inner rule (condition stripped) only to the matching biomes' lists
for (var biome : biomeConditionSource.biomes) {
perBiomeSources.get(biome).add(testRuleSource.thenRun());
}
} else {
// Non-biome-gated rule: add to every biome list and the no-match list
for (var list : perBiomeSources.values()) {
list.add(innerSource);
}
noMatchSources.add(innerSource);
}
}
// Compile all source lists into rule lists
Reference2ObjectOpenHashMap<ResourceKey<Biome>, List<SurfaceRules.SurfaceRule>> compiledBiomeMatch = new Reference2ObjectOpenHashMap<>(perBiomeSources.size());
Reference2ObjectMaps.fastForEach(perBiomeSources, entry -> {
List<SurfaceRules.SurfaceRule> compiled = new ArrayList<>(entry.getValue().size());
for (var src : entry.getValue()) {
compiled.add(src.apply(context));
}
compiledBiomeMatch.put(entry.getKey(), List.copyOf(compiled));
});
List<SurfaceRules.SurfaceRule> compiledNoMatch = new ArrayList<>(noMatchSources.size());
for (var src : noMatchSources) {
compiledNoMatch.add(src.apply(context));
}
return new OptimizedBiomeLookupSequenceRule(compiledBiomeMatch, List.copyOf(compiledNoMatch), context);
}
public record OptimizedBiomeLookupSequenceRule(
Map<ResourceKey<Biome>, List<SurfaceRules.SurfaceRule>> rulesForBiomeMatch,
List<SurfaceRules.SurfaceRule> rulesForNoBiomeMatch,
SurfaceRules.Context context
) implements SurfaceRules.SurfaceRule {
@Override
public @Nullable BlockState tryApply(int x, int y, int z) {
var biome = context.biome.get();
var key = (biome instanceof Holder.Reference<Biome> ref) ? ref.key() : biome.unwrapKey().orElseThrow();
var ruleList = rulesForBiomeMatch.getOrDefault(key, rulesForNoBiomeMatch);
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < ruleList.size(); i++) {
var rule = ruleList.get(i);
var state = rule.tryApply(x, y, z);
if (state != null) {
return state;
}
}
return null;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
OptimizedBiomeLookupSequenceRule that = (OptimizedBiomeLookupSequenceRule) o;
return rulesForBiomeMatch.equals(that.rulesForBiomeMatch) && rulesForNoBiomeMatch.equals(that.rulesForNoBiomeMatch);
}
@Override
public int hashCode() {
return Objects.hash(rulesForBiomeMatch, rulesForNoBiomeMatch);
}
}
}

View File

@ -5,9 +5,19 @@ public net.minecraft.client.renderer.block.model.multipart.MultiPart definition
public net.minecraft.client.resources.model.ModelBakery$ModelBakerImpl
public net.minecraft.client.resources.model.ModelBakery$ModelBakerImpl <init>(Lnet/minecraft/client/resources/model/ModelBakery;Lnet/minecraft/client/resources/model/ModelBakery$TextureGetter;Lnet/minecraft/client/resources/model/ModelResourceLocation;)V
public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRule
public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRuleSource
public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRuleSource <init>(Ljava/util/List;)V
public net.minecraft.world.level.levelgen.SurfaceRules$TestRuleSource
public net.minecraft.world.level.levelgen.SurfaceRules$TestRuleSource <init>(Lnet/minecraft/world/level/levelgen/SurfaceRules$ConditionSource;Lnet/minecraft/world/level/levelgen/SurfaceRules$RuleSource;)V
public net.minecraft.world.level.levelgen.SurfaceRules$BiomeConditionSource
public net.minecraft.world.level.levelgen.SurfaceRules$BiomeConditionSource <init>(Ljava/util/List;)V
public net.minecraft.world.level.levelgen.SurfaceRules$BiomeConditionSource biomes
public net.minecraft.world.level.levelgen.SurfaceRules$Context biome
public net.minecraft.client.renderer.block.model.BlockModel GSON
public net.minecraft.server.packs.resources.ProfiledReloadInstance$State reloadNanos
public net.minecraft.server.packs.resources.ProfiledReloadInstance$State preparationNanos
public net.minecraft.client.resources.model.ModelBakery$BlockStateDefinitionException
public net.minecraft.world.item.crafting.Ingredient$TagValue f_43959_
public net.minecraft.server.MinecraftServer$ReloadableResources
public net.minecraft.resources.ResourceKey <init>(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)V
public net.minecraft.server.level.ServerChunkCache$MainThreadExecutor
@ -53,10 +63,13 @@ public net.minecraft.server.level.ChunkMap pendingUnloads
public net.minecraft.world.level.levelgen.DensityFunctions$MulOrAdd$Type
public net.minecraft.client.renderer.entity.EnderDragonRenderer$DragonModel entity
public net.minecraft.client.KeyMapping ALL
protected net.minecraft.server.level.GenerationChunkHolder futures
protected net.minecraft.server.level.GenerationChunkHolder startedWork
public net.minecraft.client.resources.model.BlockStateModelLoader$LoadedBlockModelDefinition
public net.minecraft.client.resources.model.ModelManager$ResolvedModels
public net.minecraft.client.resources.model.ModelManager$ResolvedModels <init>(Lnet/minecraft/client/resources/model/ResolvedModel;Ljava/util/Map;)V
public net.minecraft.client.resources.model.ModelDiscovery$ModelWrapper
public net.minecraft.client.resources.model.ModelDiscovery$ModelWrapper ModelWrapper(Lnet/minecraft/resources/Identifier;Lnet/minecraft/client/resources/model/UnbakedModel;Z)V
public net.minecraft.client.resources.model.ModelDiscovery createAndQueueWrapper(Lnet/minecraft/resources/Identifier;Lnet/minecraft/client/resources/model/UnbakedModel;)Lnet/minecraft/client/resources/model/ModelDiscovery$ModelWrapper;
public net.minecraft.client.resources.model.ModelDiscovery createAndQueueWrapper(Lnet/minecraft/resources/Identifier;Lnet/minecraft/client/resources/model/UnbakedModel;)Lnet/minecraft/client/resources/model/ModelDiscovery$ModelWrapper;