Merge remote-tracking branch 'origin/1.20' into 1.21.1
This commit is contained in:
commit
10f8be3d93
|
|
@ -30,7 +30,7 @@ dependencies {
|
|||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.release = 21
|
||||
options.release = 17
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -85,12 +85,7 @@ tasks.named<Jar>("jar") {
|
|||
))
|
||||
}
|
||||
|
||||
// We must force the Java 21 compiler to be used because our AP requires Java 21
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
val curSourceCompatLevel = JavaVersion.VERSION_21
|
||||
sourceCompatibility = curSourceCompatLevel
|
||||
targetCompatibility = curSourceCompatLevel
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
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"));
|
||||
MinecraftServer.setFatalException(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$2", 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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().tell(() -> this.registerReloadListener(listener));
|
||||
return;
|
||||
}
|
||||
|
||||
original.call(listener);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
package org.embeddedt.modernfix.common.mixin.core;
|
||||
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import org.embeddedt.modernfix.util.TimeFormatter;
|
||||
import org.embeddedt.modernfix.forge.classloading.ManifestCompactor;
|
||||
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.Shadow;
|
||||
|
|
@ -22,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));
|
||||
ManifestCompactor.compactManifests();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +1,169 @@
|
|||
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.Util;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.nbt.*;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
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.embeddedt.modernfix.duck.IServerLevel;
|
||||
import org.embeddedt.modernfix.world.StrongholdLocationCache;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
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 {
|
||||
private WeakReference<ServerLevel> mfix$serverLevel;
|
||||
@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$setAssociatedServerLevel(ServerLevel level) {
|
||||
mfix$serverLevel = new WeakReference<>(level);
|
||||
public void mfix$setStrongholdCachePath(Path cachePath, RegistryAccess.Frozen registryAccess) {
|
||||
this.mfix$dimensionPath = cachePath;
|
||||
this.mfix$registryAccess = registryAccess;
|
||||
}
|
||||
|
||||
@Inject(method = "generateRingPositions", at = @At("HEAD"), cancellable = true)
|
||||
private void useCachedDataIfAvailable(Holder<StructureSet> structureSet, ConcentricRingsStructurePlacement placement, CallbackInfoReturnable<CompletableFuture<List<ChunkPos>>> cir) {
|
||||
if(placement.count() == 0)
|
||||
return;
|
||||
ServerLevel level = searchLevel();
|
||||
if(level == null)
|
||||
return;
|
||||
StrongholdLocationCache cache = ((IServerLevel)level).mfix$getStrongholdCache();
|
||||
List<ChunkPos> positions = cache.getChunkPosList();
|
||||
if(positions.isEmpty())
|
||||
return;
|
||||
ModernFix.LOGGER.debug("Loaded stronghold cache for dimension {} with {} positions", level.dimension().location(), positions.size());
|
||||
cir.setReturnValue(CompletableFuture.completedFuture(positions));
|
||||
@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 ServerLevel searchLevel() {
|
||||
if(mfix$serverLevel != null)
|
||||
return mfix$serverLevel.get();
|
||||
else
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "generateRingPositions", at = @At("RETURN"), cancellable = true)
|
||||
private void saveCachedData(Holder<StructureSet> structureSet, ConcentricRingsStructurePlacement placement, CallbackInfoReturnable<CompletableFuture<List<ChunkPos>>> cir) {
|
||||
cir.setReturnValue(cir.getReturnValue().thenApplyAsync(list -> {
|
||||
if(list.size() == 0)
|
||||
return list;
|
||||
ServerLevel level = searchLevel();
|
||||
if(level != null) {
|
||||
StrongholdLocationCache cache = ((IServerLevel)level).mfix$getStrongholdCache();
|
||||
cache.setChunkPosList(list);
|
||||
ModernFix.LOGGER.debug("Saved stronghold cache for dimension {}", level.dimension().location());
|
||||
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.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) {
|
||||
positions.add(new ChunkPos(data[i], data[i + 1]));
|
||||
}
|
||||
result.put(key, positions);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}, Util.backgroundExecutor()));
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,61 +1,30 @@
|
|||
package org.embeddedt.modernfix.common.mixin.perf.cache_strongholds;
|
||||
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
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.level.ServerChunkCache;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.profiling.ProfilerFiller;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.storage.DimensionDataStorage;
|
||||
import net.minecraft.world.level.storage.WritableLevelData;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import org.embeddedt.modernfix.duck.IChunkGenerator;
|
||||
import org.embeddedt.modernfix.duck.IServerLevel;
|
||||
import org.embeddedt.modernfix.world.StrongholdLocationCache;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Mixin(ServerLevel.class)
|
||||
public abstract class ServerLevelMixin extends Level implements IServerLevel {
|
||||
protected ServerLevelMixin(WritableLevelData arg, ResourceKey<Level> arg2, RegistryAccess arg3, Holder<DimensionType> arg4, Supplier<ProfilerFiller> supplier, boolean bl, boolean bl2, long l, int i) {
|
||||
super(arg, arg2, arg3, arg4, supplier, bl, bl2, l, i);
|
||||
}
|
||||
|
||||
@Shadow public abstract DimensionDataStorage getDataStorage();
|
||||
|
||||
@Shadow @Final private ServerChunkCache chunkSource;
|
||||
private StrongholdLocationCache mfix$strongholdCache;
|
||||
|
||||
public class ServerLevelMixin {
|
||||
/**
|
||||
* Initialize the stronghold cache but don't force any structure generation yet.
|
||||
* @author embeddedt
|
||||
* @reason Make the dimension path accessible to ChunkGeneratorStructureState.
|
||||
*/
|
||||
@Redirect(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/ChunkGeneratorStructureState;ensureStructuresGenerated()V"))
|
||||
private void hookStrongholdCache(ChunkGeneratorStructureState generator) {
|
||||
((IChunkGenerator)generator).mfix$setAssociatedServerLevel((ServerLevel)(Object)this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Now start the stronghold generation process.
|
||||
*/
|
||||
@Inject(method = "<init>", at = @At("TAIL"))
|
||||
private void ensureGeneration(CallbackInfo ci) {
|
||||
mfix$strongholdCache = this.getDataStorage().computeIfAbsent(
|
||||
StrongholdLocationCache.factory((ServerLevel)(Object)this),
|
||||
StrongholdLocationCache.getFileId(this.dimensionTypeRegistration()));
|
||||
this.chunkSource.getGeneratorState().ensureStructuresGenerated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StrongholdLocationCache mfix$getStrongholdCache() {
|
||||
return mfix$strongholdCache;
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
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 net.minecraft.world.level.chunk.ChunkAccess;
|
||||
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<ChunkAccess> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
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.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$12(ChunkHolder holder, 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$12(holder, pos);
|
||||
|
||||
((IClearableChunkHolder)holder).mfix$resetProtoChunkFutures();
|
||||
|
||||
suspended++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mfix$markForSuspensionCheck(ChunkPos pos) {
|
||||
this.mfix$protoChunksToDrop.put(pos.toLong(), this.mfix$dropTickCounter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Executor mfix$getMainThreadExecutor() {
|
||||
return this.mainThreadExecutor;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
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.ChunkProgressListenerFactory;
|
||||
import net.minecraft.server.packs.repository.PackRepository;
|
||||
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.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, Proxy proxy, DataFixer fixerUpper, Services services, ChunkProgressListenerFactory progressListenerFactory) {
|
||||
super(serverThread, storageSource, packRepository, worldStem, proxy, fixerUpper, services, progressListenerFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package org.embeddedt.modernfix.common.mixin.perf.suspend_integrated_server_during_load;
|
||||
|
||||
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.Redirect;
|
||||
|
||||
@Mixin(Minecraft.class)
|
||||
@ClientOnlyMixin
|
||||
public class MinecraftMixin {
|
||||
/**
|
||||
* @author embeddedt
|
||||
* @reason spin-waiting burns CPU time on the main thread, when the server thread is likely to take some time
|
||||
* to be ready.
|
||||
*/
|
||||
@Redirect(method = "doWorldLoad", at = @At(value = "INVOKE", target = "Ljava/lang/Thread;yield()V"))
|
||||
private void sleepInsteadOfYield() {
|
||||
try {
|
||||
Thread.sleep(16L);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
package org.embeddedt.modernfix.duck;
|
||||
|
||||
import org.embeddedt.modernfix.world.StrongholdLocationCache;
|
||||
|
||||
public interface IServerLevel {
|
||||
StrongholdLocationCache mfix$getStrongholdCache();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package org.embeddedt.modernfix.duck.release_protochunks;
|
||||
|
||||
public interface IClearableChunkHolder {
|
||||
void mfix$resetProtoChunkFutures();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package org.embeddedt.modernfix.duck.suspend_integrated_server_during_load;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.embeddedt.modernfix.ModernFix;
|
||||
|
||||
public interface IDeferrableIntegratedServer {
|
||||
ResourceLocation CLIENT_LOAD_SENTINEL = ResourceLocation.fromNamespaceAndPath(ModernFix.MODID, "mark_client_load_finished");
|
||||
|
||||
void mfix$markClientLoadFinished();
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package org.embeddedt.modernfix.forge.classloading;
|
||||
|
||||
import cpw.mods.jarhandling.impl.Jar;
|
||||
import net.neoforged.fml.loading.LoadingModList;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.jar.Attributes;
|
||||
|
||||
public class ManifestCompactor {
|
||||
public static void compactManifests() {
|
||||
for (var mfi : LoadingModList.get().getModFiles()) {
|
||||
if (!(mfi.getFile().getSecureJar() instanceof Jar jar)) {
|
||||
continue;
|
||||
}
|
||||
var manifest = jar.moduleDataProvider().getManifest();
|
||||
if (manifest == null) {
|
||||
continue;
|
||||
}
|
||||
var entries = manifest.getEntries();
|
||||
var entryKeys = new HashSet<>(entries.keySet());
|
||||
var digests = Set.of(new Attributes.Name("SHA-256-Digest"), new Attributes.Name("SHA-384-Digest"));
|
||||
entryKeys.forEach(key -> entries.compute(key, (k, attrs) -> {
|
||||
if (attrs != null && attrs.keySet().stream().anyMatch(n -> n != null && !digests.contains(n))) {
|
||||
return attrs;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.ResourceLocation;
|
||||
import net.minecraft.world.item.Item;
|
||||
|
|
@ -18,12 +19,15 @@ 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.ModernFixConfig;
|
||||
import org.embeddedt.modernfix.neoforge.packet.ClientLoadFinishedPayload;
|
||||
import org.embeddedt.modernfix.neoforge.packet.SmartIngredientSyncPayload;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -32,6 +36,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();
|
||||
|
|
@ -79,15 +84,25 @@ public class ModernFixForge {
|
|||
}
|
||||
|
||||
private void registerNetworkChannel(final RegisterPayloadHandlersEvent event) {
|
||||
final PayloadRegistrar registrar = event.registrar("1").executesOn(HandlerThread.MAIN).optional();
|
||||
if (ModernFixMixinPlugin.instance.isOptionEnabled("perf.smart_ingredient_sync.Channel")) {
|
||||
// Sets the current network version
|
||||
final PayloadRegistrar registrar = event.registrar("1").optional();
|
||||
registrar.playToClient(
|
||||
SmartIngredientSyncPayload.TYPE,
|
||||
SmartIngredientSyncPayload.STREAM_CODEC,
|
||||
(payload, ctx) -> {}
|
||||
);
|
||||
}
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package org.embeddedt.modernfix.render.font;
|
||||
|
||||
import com.mojang.blaze3d.font.GlyphInfo;
|
||||
import com.mojang.blaze3d.font.GlyphProvider;
|
||||
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 GlyphInfo 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
package org.embeddedt.modernfix.world;
|
||||
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.datafix.DataFixTypes;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class StrongholdLocationCache extends SavedData {
|
||||
private List<ChunkPos> chunkPosList;
|
||||
public StrongholdLocationCache() {
|
||||
super();
|
||||
chunkPosList = new ArrayList<>();
|
||||
}
|
||||
|
||||
public static SavedData.Factory<StrongholdLocationCache> factory(ServerLevel serverLevel) {
|
||||
// FIXME datafixer will probably throw on update
|
||||
return new SavedData.Factory<>(StrongholdLocationCache::new, StrongholdLocationCache::load, DataFixTypes.SAVED_DATA_FORCED_CHUNKS);
|
||||
}
|
||||
|
||||
public List<ChunkPos> getChunkPosList() {
|
||||
return new ArrayList<>(chunkPosList);
|
||||
}
|
||||
|
||||
public void setChunkPosList(List<ChunkPos> positions) {
|
||||
this.chunkPosList = new ArrayList<>(positions);
|
||||
this.setDirty();
|
||||
}
|
||||
|
||||
public static StrongholdLocationCache load(CompoundTag arg, HolderLookup.Provider provider) {
|
||||
StrongholdLocationCache cache = new StrongholdLocationCache();
|
||||
if(arg.contains("Positions", Tag.TAG_LONG_ARRAY)) {
|
||||
long[] positions = arg.getLongArray("Positions");
|
||||
for(long position : positions) {
|
||||
cache.chunkPosList.add(new ChunkPos(position));
|
||||
}
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag save(CompoundTag compoundTag, HolderLookup.Provider provider) {
|
||||
long[] serialized = new long[chunkPosList.size()];
|
||||
for(int i = 0; i < chunkPosList.size(); i++) {
|
||||
ChunkPos thePos = chunkPosList.get(i);
|
||||
serialized[i] = thePos.toLong();
|
||||
}
|
||||
compoundTag.putLongArray("Positions", serialized);
|
||||
return compoundTag;
|
||||
}
|
||||
|
||||
public static String getFileId(Holder<DimensionType> dimensionType) {
|
||||
return "mfix_strongholds";
|
||||
}
|
||||
}
|
||||
|
|
@ -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.getMinBuildHeight();
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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.getMinBuildHeight();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,3 +63,5 @@ 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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user