diff --git a/dependencies.gradle b/dependencies.gradle index 8e9411a3..00c58067 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -62,6 +62,8 @@ dependencies { transformedModCompileOnly(rfg.deobf("curse.maven:candycraft-251118:2330488")) runtimeOnly(deobf("https://github.com/makamys/CoreTweaks/releases/download/0.3.3.2/CoreTweaks-1.7.10-0.3.3.2+nomixin.jar")) + runtimeOnlyNonPublishable(deobfCurse("bsprint-227409:2725690")) + runtimeOnlyNonPublishable("com.github.GTNewHorizons:Angelica:1.0.0-alpha47") } // Replace when RFG support deobfuscation from notch mappings diff --git a/src/main/java/com/mitchej123/hodgepodge/config/SpeedupsConfig.java b/src/main/java/com/mitchej123/hodgepodge/config/SpeedupsConfig.java index 04d93f75..decf683f 100644 --- a/src/main/java/com/mitchej123/hodgepodge/config/SpeedupsConfig.java +++ b/src/main/java/com/mitchej123/hodgepodge/config/SpeedupsConfig.java @@ -46,6 +46,11 @@ public class SpeedupsConfig { @Config.DefaultBoolean(true) @Config.RequiresMcRestart public static boolean speedupChunkProviderClient; + + @Config.Comment("Lightly threads chunk generation, loading, and discarding. Experimental, use at your own risk!") + @Config.DefaultBoolean(false) + @Config.RequiresMcRestart + public static boolean fastChunkHandling; // Biomes O' Plenty diff --git a/src/main/java/com/mitchej123/hodgepodge/mixins/Mixins.java b/src/main/java/com/mitchej123/hodgepodge/mixins/Mixins.java index 336a79a2..f08ccba5 100644 --- a/src/main/java/com/mitchej123/hodgepodge/mixins/Mixins.java +++ b/src/main/java/com/mitchej123/hodgepodge/mixins/Mixins.java @@ -431,6 +431,17 @@ public enum Mixins { .addMixinClasses("minecraft.MixinWorldServer_LimitUpdateRecursion") .setApplyIf(() -> FixesConfig.limitRecursiveBlockUpdateDepth >= 0)), + FAST_CHUNK_LOADING(new Builder("Lightly threads chunk generation and loading").setPhase(Phase.EARLY) + .setSide(Side.BOTH).addTargetedMod(TargetedMod.VANILLA) + .addMixinClasses( + "minecraft.fastload.MixinIntCache", + "minecraft.fastload.MixinWorldChunkManager", + "minecraft.fastload.MixinWorldServer", + "minecraft.fastload.MixinEntityPlayerMP", + "minecraft.fastload.MixinPlayerManager", + "minecraft.fastload.MixinPlayerInstance") + .setApplyIf(() -> SpeedupsConfig.fastChunkHandling)), + // Ic2 adjustments IC2_UNPROTECTED_GET_BLOCK_FIX(new Builder("IC2 Kinetic Fix").setPhase(Phase.EARLY).setSide(Side.BOTH) .addMixinClasses("ic2.MixinIc2WaterKinetic").setApplyIf(() -> FixesConfig.fixIc2UnprotectedGetBlock) diff --git a/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinEntityPlayerMP.java b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinEntityPlayerMP.java new file mode 100644 index 00000000..d8b7a485 --- /dev/null +++ b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinEntityPlayerMP.java @@ -0,0 +1,178 @@ +package com.mitchej123.hodgepodge.mixins.early.minecraft.fastload; + +import static com.mitchej123.hodgepodge.Common.log; + +import java.util.List; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.inventory.ICrafting; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.play.server.S26PacketMapChunkBulk; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.World; +import net.minecraft.world.WorldServer; +import net.minecraft.world.chunk.Chunk; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.world.ChunkWatchEvent; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.mitchej123.hodgepodge.mixins.interfaces.ExtEntityPlayerMP; +import com.mitchej123.hodgepodge.util.ChunkPosUtil; +import com.mojang.authlib.GameProfile; + +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectImmutableList; + +@Mixin(EntityPlayerMP.class) +public abstract class MixinEntityPlayerMP extends EntityPlayer implements ICrafting, ExtEntityPlayerMP { + + @Unique + private final List> hodgepodge$chunkSends = new ObjectArrayList<>(); + @Unique + private final List hodgepodge$rollingChunks = new ObjectArrayList<>(); + @Unique + private final List hodgepodge$rollingTEs = new ObjectArrayList<>(); + @Unique + private int hodgepodge$totalChunks = 0; + @Unique + private static final S26PacketMapChunkBulk hodgepodge$dummyPacket = new S26PacketMapChunkBulk(); + @Unique + private boolean hodgepodge$isThrottled = false; + @Unique + private boolean hodgepodge$wasThrottled = false; + @Unique + private final LongOpenHashSet hodgepodge$loadedChunks = new LongOpenHashSet(); + @Unique + private final LongOpenHashSet hodgepodge$chunksToLoad = new LongOpenHashSet(); + + @Override + public void setThrottled(boolean val) { + this.hodgepodge$isThrottled = val; + } + + @Override + public boolean isThrottled() { + return this.hodgepodge$isThrottled; + } + + @Override + public void setWasThrottled(boolean val) { + this.hodgepodge$wasThrottled = val; + } + + @Override + public boolean wasThrottled() { + return this.hodgepodge$wasThrottled; + } + + @Override + public LongOpenHashSet chunksToLoad() { + return this.hodgepodge$chunksToLoad; + } + + @Override + public LongOpenHashSet loadedChunks() { + return this.hodgepodge$loadedChunks; + } + + @Shadow + public abstract WorldServer getServerForPlayer(); + + @Shadow + public NetHandlerPlayServer playerNetServerHandler; + + @Shadow + protected abstract void func_147097_b(TileEntity p_147097_1_); + + public MixinEntityPlayerMP(World p_i45324_1_, GameProfile p_i45324_2_) { + super(p_i45324_1_, p_i45324_2_); + } + + @Redirect(method = "onUpdate", at = @At(value = "INVOKE", target = "Ljava/util/List;isEmpty()Z", ordinal = 1)) + private boolean hodgepodge$skipOGChunkList(List instance) { + return true; + } + + @Inject(method = "onUpdate", at = @At(value = "TAIL")) + private void hodgepodge$replaceChunkList(CallbackInfo ci) { + + if (this.hodgepodge$chunksToLoad.longStream().anyMatch(this.hodgepodge$loadedChunks::contains)) + log.warn("sending duplicate!!!"); + + final int chunksPPacket = S26PacketMapChunkBulk.func_149258_c(); + final LongIterator chunkKeys = this.hodgepodge$chunksToLoad.longIterator(); + Chunk chunk; + + // For every chunk... + while (chunkKeys.hasNext()) { + + final long key = chunkKeys.nextLong(); + final int cx = ChunkPosUtil.getPackedX(key); + final int cz = ChunkPosUtil.getPackedZ(key); + + // Only send the chunk if it exists + if (this.worldObj.blockExists(cx << 4, 0, cz << 4)) { + chunk = this.worldObj.getChunkFromChunkCoords(cx, cz); + + if (chunk.func_150802_k()) { + + ++this.hodgepodge$totalChunks; + this.hodgepodge$rollingChunks.add(chunk); + this.hodgepodge$loadedChunks.add(key); + this.hodgepodge$rollingTEs.addAll( + ((WorldServer) this.worldObj) + .func_147486_a(cx << 4, 0, cz << 4, (cx << 4) + 15, 256, (cz << 16) + 15)); + // BugFix: 16 makes it load an extra chunk, which isn't associated with a player, which makes it not + // unload unless a player walks near it. + chunkKeys.remove(); + } + } + + // Don't overflow the packet size + if (this.hodgepodge$rollingChunks.size() == chunksPPacket) { + this.hodgepodge$chunkSends.add(new ObjectImmutableList<>(this.hodgepodge$rollingChunks)); + this.hodgepodge$rollingChunks.clear(); + } + } + + // Catch a half-full packet + if (this.hodgepodge$rollingChunks.size() < chunksPPacket) + this.hodgepodge$chunkSends.add(new ObjectImmutableList<>(this.hodgepodge$rollingChunks)); + + if (!this.hodgepodge$chunkSends.isEmpty()) { + + for (int i = 0; i < this.hodgepodge$chunkSends.size(); ++i) { + this.playerNetServerHandler.sendPacket(new S26PacketMapChunkBulk(this.hodgepodge$chunkSends.get(i))); + } + + for (int i = 0; i < this.hodgepodge$rollingTEs.size(); ++i) { + this.func_147097_b(this.hodgepodge$rollingTEs.get(i)); + } + + for (int i = 0; i < this.hodgepodge$totalChunks; ++i) { + chunk = this.hodgepodge$chunkSends.get(i / chunksPPacket).get(i % chunksPPacket); + this.getServerForPlayer().getEntityTracker().func_85172_a((EntityPlayerMP) (Object) this, chunk); + MinecraftForge.EVENT_BUS + .post(new ChunkWatchEvent.Watch(chunk.getChunkCoordIntPair(), (EntityPlayerMP) (Object) this)); + } + } + + if (this.hodgepodge$totalChunks > S26PacketMapChunkBulk.func_149258_c()) + log.info("Sent {} chunks to the client", this.hodgepodge$totalChunks); + + this.hodgepodge$totalChunks = 0; + this.hodgepodge$chunkSends.clear(); + this.hodgepodge$rollingChunks.clear(); + this.hodgepodge$rollingTEs.clear(); + } +} diff --git a/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinIntCache.java b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinIntCache.java new file mode 100644 index 00000000..640f863c --- /dev/null +++ b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinIntCache.java @@ -0,0 +1,22 @@ +package com.mitchej123.hodgepodge.mixins.early.minecraft.fastload; + +import net.minecraft.world.gen.layer.IntCache; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +import com.mitchej123.hodgepodge.server.NewIntCache; + +@Mixin(IntCache.class) +public class MixinIntCache { + + /** + * @author ah-OOG-ah + * @reason The old methods are non-threadsafe and barely safe at all - they recycle all allocated instances instead + * of explicitly releasing them. + */ + @Overwrite + public static synchronized int[] getIntCache(int size) { + return NewIntCache.getCache(size); + } +} diff --git a/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinPlayerInstance.java b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinPlayerInstance.java new file mode 100644 index 00000000..268c55b2 --- /dev/null +++ b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinPlayerInstance.java @@ -0,0 +1,41 @@ +package com.mitchej123.hodgepodge.mixins.early.minecraft.fastload; + +import java.util.List; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.server.management.PlayerManager; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import com.llamalad7.mixinextras.sugar.Local; +import com.mitchej123.hodgepodge.mixins.interfaces.ExtEntityPlayerMP; +import com.mitchej123.hodgepodge.util.ChunkPosUtil; + +@Mixin(PlayerManager.PlayerInstance.class) +public class MixinPlayerInstance { + + @Redirect( + method = "addPlayer", + at = @At(value = "INVOKE", target = "Ljava/util/List;add(Ljava/lang/Object;)Z", ordinal = 1)) + private boolean hodgepodge$replaceChunkSetAdd(List instance, Object o, + @Local(argsOnly = true) EntityPlayerMP player) { + return ((ExtEntityPlayerMP) player).chunksToLoad().add(ChunkPosUtil.toLong(o)); + } + + @Redirect( + method = "removePlayer", + at = @At(value = "INVOKE", target = "Ljava/util/List;remove(Ljava/lang/Object;)Z", ordinal = 3)) + private boolean hodgepodge$replaceChunkSetRemove(List instance, Object o, + @Local(argsOnly = true) EntityPlayerMP player) { + return ((ExtEntityPlayerMP) player).chunksToLoad().remove(ChunkPosUtil.toLong(o)); + } + + @Redirect( + method = "sendToAllPlayersWatchingChunk", + at = @At(value = "INVOKE", target = "Ljava/util/List;contains(Ljava/lang/Object;)Z")) + private boolean hodgepodge$replaceChunkSetContains(List instance, Object o, @Local EntityPlayerMP player) { + return ((ExtEntityPlayerMP) player).chunksToLoad().remove(ChunkPosUtil.toLong(o)); + } +} diff --git a/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinPlayerManager.java b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinPlayerManager.java new file mode 100644 index 00000000..fcb35b68 --- /dev/null +++ b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinPlayerManager.java @@ -0,0 +1,168 @@ +package com.mitchej123.hodgepodge.mixins.early.minecraft.fastload; + +import java.util.List; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.server.management.PlayerManager; +import net.minecraft.world.ChunkCoordIntPair; +import net.minecraft.world.WorldServer; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import com.llamalad7.mixinextras.sugar.Local; +import com.mitchej123.hodgepodge.mixins.interfaces.ExtEntityPlayerMP; +import com.mitchej123.hodgepodge.mixins.interfaces.FastWorldServer; +import com.mitchej123.hodgepodge.server.FastCPS; +import com.mitchej123.hodgepodge.util.ChunkPosUtil; + +import it.unimi.dsi.fastutil.longs.LongArrayList; + +@Mixin(PlayerManager.class) +public abstract class MixinPlayerManager { + + @Shadow + @Final + private WorldServer theWorldServer; + @Shadow + private int playerViewRadius; + + @Shadow + protected abstract boolean overlaps(int x1, int z1, int x2, int z2, int radius); + + @Shadow + protected abstract PlayerManager.PlayerInstance getOrCreateChunkWatcher(int cx, int cz, boolean shouldCreate); + + @Unique + final private ChunkPosUtil.FastComparator cmp = new ChunkPosUtil.FastComparator(); + + @Unique + private final ChunkPosUtil.FastComparator hodgepodge$comparator = new ChunkPosUtil.FastComparator(); + + /** + * @author ah-OOG-ah + * @reason Original method kinda sucked ngl + */ + @Overwrite + public void filterChunkLoadQueue(EntityPlayerMP player) { + + ((ExtEntityPlayerMP) player).chunksToLoad() + .removeIf(l -> cmp.setPos(player).withinRadius(l, this.playerViewRadius)); + ((ExtEntityPlayerMP) player).loadedChunks() + .removeIf(l -> cmp.setPos(player).withinRadius(l, this.playerViewRadius)); + } + + /** + * @author ah-OOG-ah + * @reason Original method was convoluted and impossible to fix + */ + @Overwrite + public void updatePlayerPertinentChunks(EntityPlayerMP player) { + + final ExtEntityPlayerMP eemp = (ExtEntityPlayerMP) player; + eemp.setWasThrottled(eemp.isThrottled()); + eemp.setThrottled(false); + + final double deltax = player.posX - player.managedPosX; + final double deltaz = player.posZ - player.managedPosZ; + final double distMovedSquared = deltax * deltax + deltaz * deltaz; + + // If the player moved less than half a chunk (8^2 = 64), or if the player wasn't throttled last tick, no need + // to update + if (!eemp.wasThrottled() && distMovedSquared < 64) return; + + final int pcx = (int) player.posX >> 4; + final int pcz = (int) player.posZ >> 4; + final int prev_cx = (int) player.managedPosX >> 4; + final int prev_cz = (int) player.managedPosZ >> 4; + final int dpcx = pcx - prev_cx; + final int dpcz = pcz - prev_cz; + + // If the player hasn't moved to a new chunk (and wasn't throttled), no need to update + if (!eemp.wasThrottled() && dpcx == 0 && dpcz == 0) return; + + int x = 0; + int z = 0; + int sideLen = this.playerViewRadius * 2 + 1; + final LongArrayList chunksToLoad = new LongArrayList(sideLen * sideLen); + + // Add all chunk coords in a spiral around the player + for (int i = 0; i < sideLen * sideLen; ++i) { + + final int cx = x + pcx; + final int cz = z + pcz; + final long key = ChunkCoordIntPair.chunkXZ2Int(cx, cz); + if (!eemp.loadedChunks().contains(key)) chunksToLoad.add(key); + + // At the same time, we can check the previous chunk grid, and remove the ones which are out of range + if (!this.overlaps(cx - dpcx, cz - dpcz, pcx, pcz, this.playerViewRadius)) { + final PlayerManager.PlayerInstance playerinstance = this + .getOrCreateChunkWatcher(cx - dpcx, cz - dpcz, false); + + if (playerinstance != null) playerinstance.removePlayer(player); + } + + if (Math.abs(x) <= Math.abs(z) && (x != z || x >= 0)) x += z >= 0 ? 1 : -1; + else z += x >= 0 ? -1 : 1; + } + + // Purge the old load queue + this.filterChunkLoadQueue(player); + + player.managedPosX = player.posX; + player.managedPosZ = player.posZ; + + // Generate nearest chunks first + // This also refills the load queue + chunksToLoad.sort(hodgepodge$comparator.setPos(pcx, pcz)); + chunksToLoad.forEach(l -> { + if (this.hodgepodge$allowChunkGen(l, pcx, pcz, eemp)) { + this.getOrCreateChunkWatcher(ChunkPosUtil.getPackedX(l), ChunkPosUtil.getPackedZ(l), true) + .addPlayer(player); + + if (!eemp.chunksToLoad().contains(l)) eemp.chunksToLoad().add(l); + } + }); + } + + /** + * Checks if a chunk may be loaded without violating the chunkgen throttle. + */ + @Unique + private boolean hodgepodge$allowChunkGen(long c, int pcx, int pcz, ExtEntityPlayerMP player) { + + final int cx = ChunkPosUtil.getPackedX(c); + final int cz = ChunkPosUtil.getPackedZ(c); + + if (cx == pcx && cz == pcz) return true; // Always load the player's chunk + + if (((FastCPS) this.theWorldServer.theChunkProviderServer).doesChunkExist(cx, cz, c)) return true; // Always + // load + // chunks + // from disk + + if (((FastWorldServer) this.theWorldServer).isThrottlingGen()) { + + // Don't generate new chunks while throttling + player.setThrottled(true); + return false; + } + + // Generate, but count it against the budget this tick + ((FastWorldServer) this.theWorldServer).spendGenBudget(1); + return true; + } + + @Redirect( + method = "isPlayerWatchingChunk", + at = @At(value = "INVOKE", target = "Ljava/util/List;contains(Ljava/lang/Object;)Z")) + private boolean hodgepodge$replaceLoadedChunks(List instance, Object o, + @Local(argsOnly = true) EntityPlayerMP player) { + return ((ExtEntityPlayerMP) player).chunksToLoad().contains(ChunkPosUtil.toLong(o)); + } +} diff --git a/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinWorldChunkManager.java b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinWorldChunkManager.java new file mode 100644 index 00000000..cb2c7cea --- /dev/null +++ b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinWorldChunkManager.java @@ -0,0 +1,187 @@ +package com.mitchej123.hodgepodge.mixins.early.minecraft.fastload; + +import java.util.List; +import java.util.Random; + +import net.minecraft.crash.CrashReport; +import net.minecraft.crash.CrashReportCategory; +import net.minecraft.util.ReportedException; +import net.minecraft.world.ChunkPosition; +import net.minecraft.world.biome.BiomeGenBase; +import net.minecraft.world.biome.WorldChunkManager; +import net.minecraft.world.gen.layer.GenLayer; + +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.CallbackInfoReturnable; + +import com.llamalad7.mixinextras.sugar.Local; +import com.mitchej123.hodgepodge.server.NewIntCache; + +@Mixin(WorldChunkManager.class) +public class MixinWorldChunkManager { + + @Shadow + private GenLayer genBiomes; + + @Redirect( + method = { "getRainfall", "getBiomesForGeneration", "areBiomesViable", + "getBiomeGenAt([Lnet/minecraft/world/biome/BiomeGenBase;IIIIZ)[Lnet/minecraft/world/biome/BiomeGenBase;", + "findBiomePosition" }, + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/gen/layer/IntCache;resetIntCache()V")) + private void hodgepodge$nukeIntCache() {} + + @Inject( + method = "getRainfall", + at = @At( + value = "INVOKE_ASSIGN", + target = "Lnet/minecraft/world/gen/layer/GenLayer;getInts(IIII)[I", + shift = At.Shift.AFTER), + cancellable = true) + private void hodgepodge$recycleCacheRain(float[] downfalls, int x, int z, int width, int height, + CallbackInfoReturnable cir, @Local(name = "aint") int[] ints) { + + for (int i = 0; i < width * height; ++i) { + try { + float f = (float) BiomeGenBase.getBiome(ints[i]).getIntRainfall() / 65536.0F; + + if (f > 1.0F) { + f = 1.0F; + } + + downfalls[i] = f; + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.makeCrashReport(throwable, "Invalid Biome id"); + CrashReportCategory crashreportcategory = crashreport.makeCategory("DownfallBlock"); + crashreportcategory.addCrashSection("biome id", i); + crashreportcategory.addCrashSection("downfalls[] size", downfalls.length); + crashreportcategory.addCrashSection("x", x); + crashreportcategory.addCrashSection("z", z); + crashreportcategory.addCrashSection("w", width); + crashreportcategory.addCrashSection("h", height); + throw new ReportedException(crashreport); + } + } + + NewIntCache.releaseCache(ints); + cir.setReturnValue(downfalls); + } + + @Inject( + method = "getBiomesForGeneration", + at = @At( + value = "INVOKE_ASSIGN", + target = "Lnet/minecraft/world/gen/layer/GenLayer;getInts(IIII)[I", + shift = At.Shift.AFTER), + cancellable = true) + private void hodgepodge$recycleCacheBiomes(BiomeGenBase[] biomes, int x, int z, int width, int height, + CallbackInfoReturnable cir, @Local(name = "aint") int[] ints) { + + try { + for (int i = 0; i < width * height; ++i) { + biomes[i] = BiomeGenBase.getBiome(ints[i]); + } + + NewIntCache.releaseCache(ints); + cir.setReturnValue(biomes); + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.makeCrashReport(throwable, "Invalid Biome id"); + CrashReportCategory crashreportcategory = crashreport.makeCategory("RawBiomeBlock"); + crashreportcategory.addCrashSection("biomes[] size", biomes.length); + crashreportcategory.addCrashSection("x", x); + crashreportcategory.addCrashSection("z", z); + crashreportcategory.addCrashSection("w", width); + crashreportcategory.addCrashSection("h", height); + throw new ReportedException(crashreport); + } + } + + @Inject( + method = "getBiomeGenAt([Lnet/minecraft/world/biome/BiomeGenBase;IIIIZ)[Lnet/minecraft/world/biome/BiomeGenBase;", + at = @At( + value = "INVOKE_ASSIGN", + target = "Lnet/minecraft/world/gen/layer/GenLayer;getInts(IIII)[I", + shift = At.Shift.AFTER), + cancellable = true) + private void hodgepodge$recycleCacheBiomeAt(BiomeGenBase[] biomes, int p_76931_2_, int p_76931_3_, int width, + int height, boolean p_76931_6_, CallbackInfoReturnable cir, + @Local(name = "aint") int[] ints) { + + for (int i = 0; i < width * height; ++i) { + biomes[i] = BiomeGenBase.getBiome(ints[i]); + } + + NewIntCache.releaseCache(ints); + cir.setReturnValue(biomes); + } + + @Inject( + method = "areBiomesViable", + at = @At( + value = "INVOKE_ASSIGN", + target = "Lnet/minecraft/world/gen/layer/GenLayer;getInts(IIII)[I", + shift = At.Shift.AFTER), + cancellable = true) + private void hodgepodge$recycleCacheViable(int x, int z, int radius, List allowed, + CallbackInfoReturnable cir, @Local(name = "l1") int areaWidth, @Local(name = "i2") int areaHeight, + @Local(ordinal = 0) int[] cache) { + + try { + for (int i = 0; i < areaWidth * areaHeight; ++i) { + BiomeGenBase biomegenbase = BiomeGenBase.getBiome(cache[i]); + + if (!allowed.contains(biomegenbase)) { + + NewIntCache.releaseCache(cache); + cir.setReturnValue(false); + return; + } + } + + NewIntCache.releaseCache(cache); + cir.setReturnValue(true); + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.makeCrashReport(throwable, "Invalid Biome id"); + CrashReportCategory crashreportcategory = crashreport.makeCategory("Layer"); + crashreportcategory.addCrashSection("Layer", this.genBiomes.toString()); + crashreportcategory.addCrashSection("x", x); + crashreportcategory.addCrashSection("z", z); + crashreportcategory.addCrashSection("radius", radius); + crashreportcategory.addCrashSection("allowed", allowed); + throw new ReportedException(crashreport); + } + } + + @Inject( + method = "findBiomePosition", + at = @At( + value = "INVOKE_ASSIGN", + target = "Lnet/minecraft/world/gen/layer/GenLayer;getInts(IIII)[I", + shift = At.Shift.AFTER), + cancellable = true) + private void hodgepodge$recycleCacheFindBiome(int x, int z, int radius, List p_150795_4_, + Random p_150795_5_, CallbackInfoReturnable cir, @Local(name = "l1") int l1, + @Local(name = "i2") int i2, @Local(name = "l") int l, @Local(name = "i1") int i1, + @Local(ordinal = 0) int[] ints) { + + ChunkPosition chunkposition = null; + int j2 = 0; + + for (int i = 0; i < l1 * i2; ++i) { + int l2 = l + i % l1 << 2; + int i3 = i1 + i / l1 << 2; + BiomeGenBase biomegenbase = BiomeGenBase.getBiome(ints[i]); + + if (p_150795_4_.contains(biomegenbase) && (chunkposition == null || p_150795_5_.nextInt(j2 + 1) == 0)) { + chunkposition = new ChunkPosition(l2, 0, i3); + ++j2; + } + } + + NewIntCache.releaseCache(ints); + cir.setReturnValue(chunkposition); + } +} diff --git a/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinWorldServer.java b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinWorldServer.java new file mode 100644 index 00000000..6687edea --- /dev/null +++ b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinWorldServer.java @@ -0,0 +1,201 @@ +package com.mitchej123.hodgepodge.mixins.early.minecraft.fastload; + +import static com.mitchej123.hodgepodge.Common.log; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.profiler.Profiler; +import net.minecraft.world.ChunkCoordIntPair; +import net.minecraft.world.World; +import net.minecraft.world.WorldProvider; +import net.minecraft.world.WorldServer; +import net.minecraft.world.WorldSettings; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.IChunkProvider; +import net.minecraft.world.chunk.storage.AnvilChunkLoader; +import net.minecraft.world.chunk.storage.IChunkLoader; +import net.minecraft.world.gen.ChunkProviderServer; +import net.minecraft.world.storage.ISaveHandler; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.mitchej123.hodgepodge.hax.LongChunkCoordIntPairSet; +import com.mitchej123.hodgepodge.mixins.interfaces.FastWorldServer; +import com.mitchej123.hodgepodge.server.FastCPS; +import com.mitchej123.hodgepodge.util.ChunkPosUtil; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; + +@Mixin(WorldServer.class) +public abstract class MixinWorldServer extends World implements FastWorldServer { + + @Shadow + public ChunkProviderServer theChunkProviderServer; + @Unique + private final Long2ObjectOpenHashMap> hodgepodge$chunksToLoad = new Long2ObjectOpenHashMap<>(); + @Unique + private final Long2ObjectOpenHashMap hodgepodge$chunks = new Long2ObjectOpenHashMap<>(); + @Unique + private int hodgepodge$numNewGen = 0; + @Unique + private final int hodgepodge$maxNewGen = 100; + @Unique + private int hodgepodge$overLoad = 0; + @Unique + private int hodgepodge$properActive = 0; + @Unique + private int hodgepodge$realActive = 0; + @Unique + private final Set hodgepodge$partialRendering = new ObjectOpenHashSet<>(); + @Unique + private boolean hodgepodge$flag = false; + @Unique + private LongChunkCoordIntPairSet hodgepodge$activeChunks2 = new LongChunkCoordIntPairSet(); + + @Override + public boolean isThrottlingGen() { + return this.hodgepodge$numNewGen >= this.hodgepodge$maxNewGen; + } + + @Override + public int remainingGenBudget() { + return this.hodgepodge$maxNewGen - this.hodgepodge$numNewGen; + } + + @Override + public void spendGenBudget(int amount) { + this.hodgepodge$numNewGen += amount; + } + + @Redirect( + method = "createChunkProvider", + at = @At( + value = "NEW", + target = "(Lnet/minecraft/world/WorldServer;Lnet/minecraft/world/chunk/storage/IChunkLoader;Lnet/minecraft/world/chunk/IChunkProvider;)Lnet/minecraft/world/gen/ChunkProviderServer;")) + private ChunkProviderServer hodgepodge$replaceChunkProvider(WorldServer server, IChunkLoader loader, + IChunkProvider backingCP) { + return new FastCPS(server, (AnvilChunkLoader) loader, backingCP); + } + + @Inject( + method = "func_147456_g", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/World;func_147456_g()V", shift = At.Shift.AFTER)) + private void hodgepodge$threadChunkGen(CallbackInfo ci) { + + if (this.hodgepodge$properActive != this.hodgepodge$realActive) { + + log.warn( + "{} active chunks last tick, should have been {}", + this.hodgepodge$realActive, + this.hodgepodge$properActive); + } + if (this.hodgepodge$overLoad != 0) { + log.warn("{} excess chunks loaded last tick", this.hodgepodge$overLoad); + this.hodgepodge$overLoad = 0; + } + + this.hodgepodge$flag = false; + + this.hodgepodge$chunks.clear(); + this.hodgepodge$chunksToLoad.clear(); + this.hodgepodge$numNewGen = 0; + this.hodgepodge$properActive = this.activeChunkSet.size(); + this.hodgepodge$realActive = 0; + this.hodgepodge$activeChunks2.clear(); + + // Queue chunks on worker threads or main + final ChunkProviderServer cps = this.theChunkProviderServer; + final AnvilChunkLoader acl = (AnvilChunkLoader) cps.currentChunkLoader; + for (LongIterator i = ((LongChunkCoordIntPairSet) this.activeChunkSet).longIterator(); i.hasNext();) { + final long key = i.nextLong(); + final int cx = ChunkPosUtil.getPackedX(key); + final int cz = ChunkPosUtil.getPackedZ(key); + + // If already loaded, just return it + if (cps.loadedChunkHashMap.containsItem(key)) { + + final CompletableFuture cf = new CompletableFuture<>(); + cf.complete((Chunk) cps.loadedChunkHashMap.getValueByKey(key)); + this.hodgepodge$activeChunks2.addLong(key); + this.hodgepodge$chunksToLoad.put(key, cf); + } else if (acl.chunkExists(this, cx, cz)) { + + // The chunk exists on disk, but needs to be loaded. Trivially threaded, forge already has functions for + // that. + final CompletableFuture cf = new CompletableFuture<>(); + ((FastCPS) this.theChunkProviderServer).queueDiskLoad(cx, cz, key, cf); + this.hodgepodge$activeChunks2.addLong(key); + this.hodgepodge$chunksToLoad.put(key, cf); + } else { + + // Throttle new generation + if (this.hodgepodge$numNewGen < this.hodgepodge$maxNewGen) { + // These chunks need to be generated; let's try that + final CompletableFuture cf = new CompletableFuture<>(); + ((FastCPS) this.theChunkProviderServer).queueGenerate(cx, cz, key, cf); + this.hodgepodge$activeChunks2.addLong(key); + this.hodgepodge$chunksToLoad.put(key, cf); + ++this.hodgepodge$numNewGen; + } + } + } + + // This little shuffle is needed to swap two variables + LongChunkCoordIntPairSet tmp = (LongChunkCoordIntPairSet) this.activeChunkSet; + this.activeChunkSet = this.hodgepodge$activeChunks2; + this.hodgepodge$activeChunks2 = tmp; + + // All mChunk updates need to finish before the tick ends, to avoid CMEs... at least for now. + // Guarantee that no chunks are still processing before moving on + Long2ObjectMaps.fastForEach(this.hodgepodge$chunksToLoad, e -> { + try { + this.hodgepodge$chunks.put(e.getLongKey(), e.getValue().get()); + } catch (InterruptedException | ExecutionException ex) { + throw new RuntimeException(ex); + } + }); + + if (this.hodgepodge$numNewGen != 0) { + log.info("Generated {} new chunks.", this.hodgepodge$numNewGen); + log.info("Loading {} chunks.", this.activeChunkSet.size()); + } + } + + @Redirect( + method = "func_147456_g", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/WorldServer;getChunkFromChunkCoords(II)Lnet/minecraft/world/chunk/Chunk;")) + private Chunk hodgepodge$threadChunkGen(WorldServer instance, int cx, int cz) { + + if (!this.hodgepodge$flag && this.isThrottlingGen()) { + this.hodgepodge$flag = true; + log.warn("{} active chunks", this.activeChunkSet.size()); + } + + ++this.hodgepodge$realActive; + final Chunk ch = this.hodgepodge$chunks.get(ChunkCoordIntPair.chunkXZ2Int(cx, cz)); + if (ch != null) return ch; + + // log.warn("New chunk at x: {} z: {}", cx, cz); + ++this.hodgepodge$overLoad; + return this.chunkProvider.provideChunk(cx, cz); + } + + public MixinWorldServer(ISaveHandler p_i45368_1_, String p_i45368_2_, WorldProvider p_i45368_3_, + WorldSettings p_i45368_4_, Profiler p_i45368_5_) { + super(p_i45368_1_, p_i45368_2_, p_i45368_3_, p_i45368_4_, p_i45368_5_); + } +} diff --git a/src/main/java/com/mitchej123/hodgepodge/mixins/interfaces/ExtEntityPlayerMP.java b/src/main/java/com/mitchej123/hodgepodge/mixins/interfaces/ExtEntityPlayerMP.java new file mode 100644 index 00000000..740801f1 --- /dev/null +++ b/src/main/java/com/mitchej123/hodgepodge/mixins/interfaces/ExtEntityPlayerMP.java @@ -0,0 +1,18 @@ +package com.mitchej123.hodgepodge.mixins.interfaces; + +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; + +public interface ExtEntityPlayerMP { + + void setThrottled(boolean val); + + void setWasThrottled(boolean val); + + boolean isThrottled(); + + boolean wasThrottled(); + + LongOpenHashSet chunksToLoad(); + + LongOpenHashSet loadedChunks(); +} diff --git a/src/main/java/com/mitchej123/hodgepodge/mixins/interfaces/FastWorldServer.java b/src/main/java/com/mitchej123/hodgepodge/mixins/interfaces/FastWorldServer.java new file mode 100644 index 00000000..60871ab2 --- /dev/null +++ b/src/main/java/com/mitchej123/hodgepodge/mixins/interfaces/FastWorldServer.java @@ -0,0 +1,20 @@ +package com.mitchej123.hodgepodge.mixins.interfaces; + +public interface FastWorldServer { + + /** + * If true, the server has hit the maximum number of chunks to load that tick and new generation should be avoided, + * where possible. + */ + boolean isThrottlingGen(); + + /** + * Returns the number of chunks left before the server starts throttling generation + */ + int remainingGenBudget(); + + /** + * Reduce the generation budget this tick by amount. + */ + void spendGenBudget(int amount); +} diff --git a/src/main/java/com/mitchej123/hodgepodge/server/ChunkAndNbt.java b/src/main/java/com/mitchej123/hodgepodge/server/ChunkAndNbt.java new file mode 100644 index 00000000..3b76d236 --- /dev/null +++ b/src/main/java/com/mitchej123/hodgepodge/server/ChunkAndNbt.java @@ -0,0 +1,23 @@ +package com.mitchej123.hodgepodge.server; + +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.world.chunk.Chunk; + +public class ChunkAndNbt { + + private final Chunk chunk; + private final NBTTagCompound nbt; + + public ChunkAndNbt(Chunk chunk, NBTTagCompound nbt) { + this.chunk = chunk; + this.nbt = nbt; + } + + public Chunk getChunk() { + return this.chunk; + } + + public NBTTagCompound getNbt() { + return this.nbt; + } +} diff --git a/src/main/java/com/mitchej123/hodgepodge/server/FastCPS.java b/src/main/java/com/mitchej123/hodgepodge/server/FastCPS.java new file mode 100644 index 00000000..e0f65e49 --- /dev/null +++ b/src/main/java/com/mitchej123/hodgepodge/server/FastCPS.java @@ -0,0 +1,637 @@ +package com.mitchej123.hodgepodge.server; + +import static com.mitchej123.hodgepodge.Common.log; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import net.minecraft.block.Block; +import net.minecraft.crash.CrashReport; +import net.minecraft.crash.CrashReportCategory; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityList; +import net.minecraft.entity.EnumCreatureType; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.ChunkCoordinates; +import net.minecraft.util.IProgressUpdate; +import net.minecraft.util.ReportedException; +import net.minecraft.world.ChunkCoordIntPair; +import net.minecraft.world.ChunkPosition; +import net.minecraft.world.MinecraftException; +import net.minecraft.world.World; +import net.minecraft.world.WorldServer; +import net.minecraft.world.biome.BiomeGenBase; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.EmptyChunk; +import net.minecraft.world.chunk.IChunkProvider; +import net.minecraft.world.chunk.storage.AnvilChunkLoader; +import net.minecraft.world.gen.ChunkProviderServer; +import net.minecraftforge.common.DimensionManager; +import net.minecraftforge.common.ForgeChunkManager; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.world.ChunkDataEvent; + +import cpw.mods.fml.common.registry.GameRegistry; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +public class FastCPS extends ChunkProviderServer { + + /** + * used by unload100OldestChunks to iterate the loadedChunkHashMap for unload (underlying assumption, first in, + * first out) + */ + private final LongOpenHashSet chunksToUnload = new LongOpenHashSet(); + private final LongOpenHashSet loadingChunks = new LongOpenHashSet(); + private final Chunk empty; + private int maxUnloadsPerTick = 500; + + private final ThreadLocal isMChunk = ThreadLocal.withInitial(() -> false); + private final ExecutorService mChunk = Executors.newSingleThreadExecutor(); + private final ExecutorService mPregen = Executors.newSingleThreadExecutor(); + private final int maxWorkers = 6; + private final ExecutorService workers = Executors.newFixedThreadPool(maxWorkers); + // A thread-local copy of the backing world generator + private final ThreadLocal localProvider = ThreadLocal + .withInitial(() -> this.worldObj.provider.createChunkGenerator()); + + public FastCPS(WorldServer worldObj, AnvilChunkLoader loader, IChunkProvider backingCP) { + super(worldObj, loader, backingCP); + + this.loadedChunks = new ObjectArrayList<>(); + + this.empty = new EmptyChunk(worldObj, 0, 0); + this.mChunk.execute(() -> this.isMChunk.set(true)); + } + + public void queueDiskLoad(int cx, int cz, long key, CompletableFuture chunkf) { + this.workers.execute(() -> { + final ChunkAndNbt cnbt = this.loadChunkFromDisk(cx, cz); + this.mChunk.execute(() -> chunkf.complete(this.finishChunkFromDisk(cnbt, cx, cz, key))); + }); + } + + public void queueGenerate(int cx, int cz, long key, CompletableFuture cf) { + + this.mPregen.execute(() -> { + final Chunk c = this.generateUndecoratedChunk(cx, cz, key); + this.mChunk.execute(() -> cf.complete(this.decorateChunk(c, cx, cz, key))); + }); + } + + /** + * Attempt to generate a chunk. Queues pregeneration to keep it on a single thread. + */ + public Chunk generateChunk(int cx, int cz, long key) { + final CompletableFuture cf = new CompletableFuture<>(); + this.mPregen.execute(() -> cf.complete(this.generateUndecoratedChunk(cx, cz, key))); + + try { + return this.decorateChunk(cf.get(), cx, cz, key); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + /** + * Attempt to generate an undecorated chunk, meant to run async. Does no safety checks, be careful! + */ + public Chunk generateUndecoratedChunk(int cx, int cz, long key) { + final Chunk chunk; + + if (this.currentChunkProvider == null) { + chunk = this.empty; + } else { + try { + chunk = this.localProvider.get().provideChunk(cx, cz); + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.makeCrashReport(throwable, "Exception generating new chunk"); + CrashReportCategory crashreportcategory = crashreport.makeCategory("Chunk to be generated"); + crashreportcategory.addCrashSection("Location", String.format("%d,%d", cx, cz)); + crashreportcategory.addCrashSection("Position hash", key); + crashreportcategory.addCrashSection("Generator", this.localProvider.get().makeString()); + throw new ReportedException(crashreport); + } + } + + return chunk; + } + + /** + * Attempt to decorate a generated chunk, meant to run on only one thread at a time (but that doesn't have to be the + * main thread). Does no safety checks, be careful! + */ + public Chunk decorateChunk(Chunk chunk, int cx, int cz, long key) { + + this.loadedChunkHashMap.add(key, chunk); + this.loadedChunks.add(chunk); + loadingChunks.remove(key); + chunk.onChunkLoad(); + chunk.populateChunk(this, this, cx, cz); + return chunk; + } + + /** + * Attempt to load a chunk from disk. Does not punt to workers, because it'd block anyways. + */ + public Chunk loadChunkFromDisk(int cx, int cz, long key) { + return this.finishChunkFromDisk(this.loadChunkFromDisk(cx, cz), cx, cz, key); + } + + /** + * Attempts to load a chunk from disk, returns null if not possible. Meant to run async, and doesn't return a full + * chunk - run {@link #finishChunkFromDisk(ChunkAndNbt, int, int, long)} to complete it. + */ + public ChunkAndNbt loadChunkFromDisk(int cx, int cz) { + try { + Object[] data = ((AnvilChunkLoader) this.currentChunkLoader).loadChunk__Async(this.worldObj, cx, cz); + if (data == null) { + return null; + } + + final Chunk chunk = (Chunk) data[0]; + final NBTTagCompound nbt = (NBTTagCompound) data[1]; + final ChunkAndNbt cnbt = new ChunkAndNbt(chunk, nbt); + + final NBTTagList entities = nbt.getTagList("Entities", 10); + + if (entities != null) { + for (int i = 0; i < entities.tagCount(); ++i) { + final NBTTagCompound entityTag = entities.getCompoundTagAt(i); + final Entity entity = EntityList.createEntityFromNBT(entityTag, this.worldObj); + chunk.hasEntities = true; + + if (entity != null) { + chunk.addEntity(entity); + Entity riderEntity = entity; + + for (NBTTagCompound tmpEntityTag = entityTag; tmpEntityTag + .hasKey("Riding", 10); tmpEntityTag = tmpEntityTag.getCompoundTag("Riding")) { + final Entity riddenEntity = EntityList + .createEntityFromNBT(tmpEntityTag.getCompoundTag("Riding"), this.worldObj); + + if (riddenEntity != null) { + + chunk.addEntity(riddenEntity); + riderEntity.mountEntity(riddenEntity); + } + + riderEntity = riddenEntity; + } + } + } + } + + final NBTTagList teTags = nbt.getTagList("TileEntities", 10); + + if (teTags != null) { + for (int i = 0; i < teTags.tagCount(); ++i) { + final NBTTagCompound teTag = teTags.getCompoundTagAt(i); + TileEntity te = TileEntity.createAndLoadEntity(teTag); + + if (te != null) { + chunk.addTileEntity(te); + } + } + } + + return cnbt; + + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + /** + * Loading a chunk from disk requires some synchronous action, do it here. Despite taking a + * {@link CompletableFuture}, this is NOT meant to be run on multiple threads at a time - but it doesn't have to be + * run on the main thread. + * + * @param cnbt If this is null, blocks and generates the chunk instead + */ + public Chunk finishChunkFromDisk(ChunkAndNbt cnbt, int cx, int cz, long key) { + + if (cnbt == null) { + return this.generateChunk(cx, cz, key); + } + + final Chunk chunk = cnbt.getChunk(); + final NBTTagCompound nbt = cnbt.getNbt(); + + // Loading tile ticks has to be done synchronously, for now. + if (nbt.hasKey("TileTicks", 9)) { + final NBTTagList tileTicks = nbt.getTagList("TileTicks", 10); + + if (tileTicks != null) { + for (int j1 = 0; j1 < tileTicks.tagCount(); ++j1) { + final NBTTagCompound tickTag = tileTicks.getCompoundTagAt(j1); + this.worldObj.func_147446_b( + tickTag.getInteger("x"), + tickTag.getInteger("y"), + tickTag.getInteger("z"), + Block.getBlockById(tickTag.getInteger("i")), + tickTag.getInteger("t"), + tickTag.getInteger("p")); + } + } + } + + // This section is very similar to decorateChunk. I won't merge them... for now. + // Don't call ChunkDataEvent.Load async + MinecraftForge.EVENT_BUS.post(new ChunkDataEvent.Load(chunk, cnbt.getNbt())); + chunk.lastSaveTime = this.worldObj.getTotalWorldTime(); + this.loadedChunkHashMap.add(key, chunk); + this.loadedChunks.add(chunk); + chunk.onChunkLoad(); + + if (this.currentChunkProvider != null) { + this.localProvider.get().recreateStructures(cx, cz); + } + + chunk.populateChunk(this, this, cx, cz); + + return chunk; + } + + @Override + public List func_152380_a() { + return this.loadedChunks; + } + + /** + * marks chunk for unload by "unload100OldestChunks" if there is no spawn point, or if the center of the chunk is + * outside 200 blocks (x or z) of the spawn + */ + @Override + public void unloadChunksIfNotNearSpawn(int cx, int cz) { + if (this.worldObj.provider.canRespawnHere() + && DimensionManager.shouldLoadSpawn(this.worldObj.provider.dimensionId)) { + final ChunkCoordinates chunkcoordinates = this.worldObj.getSpawnPoint(); + int xBlocksFromSpawn = cx * 16 + 8 - chunkcoordinates.posX; + int zBlocksFromSpawn = cz * 16 + 8 - chunkcoordinates.posZ; + + if (xBlocksFromSpawn < -128 || xBlocksFromSpawn > 128 + || zBlocksFromSpawn < -128 + || zBlocksFromSpawn > 128) { + this.chunksToUnload.add(ChunkCoordIntPair.chunkXZ2Int(cx, cz)); + } + } else { + this.chunksToUnload.add(ChunkCoordIntPair.chunkXZ2Int(cx, cz)); + } + } + + /** + * marks all chunks for unload, ignoring those near the spawn + */ + @Override + public void unloadAllChunks() { + for (int i = 0; i < loadedChunks.size(); ++i) { + final Chunk c = loadedChunks.get(i); + this.unloadChunksIfNotNearSpawn(c.xPosition, c.zPosition); + } + } + + /** + * Loads or generates the chunk at the chunk location specified. If generation happens, blocks until it's done. + */ + @Override + @Deprecated + public Chunk loadChunk(int cx, int cz) { + return loadChunk(cx, cz, null); + } + + /** + * Loads the chunk specified. If it doesn't exist on disk, it will be generated. The callback passed will be run on + * loading. Blocks until chunk is ready. This method runs on mServer, and is thus forbidden from decoration. + */ + @Override + public Chunk loadChunk(int cx, int cz, Runnable runnable) { + + long key = ChunkCoordIntPair.chunkXZ2Int(cx, cz); + this.chunksToUnload.remove(key); + + // while (this.loadingChunks.contains(key)) + // LockSupport.parkNanos(1000); + + Chunk chunk = (Chunk) this.loadedChunkHashMap.getValueByKey(key); + AnvilChunkLoader loader = (AnvilChunkLoader) this.currentChunkLoader; + + // If it's not already loaded... + if (chunk == null) { + // If it's already generated... + if (loader != null && loader.chunkExists(this.worldObj, cx, cz)) { + // If they have their own callback... + if (runnable != null) { + + // Queue the load and finish on worker and main threads; when the latter finishes, run the callback. + final CompletableFuture cf = new CompletableFuture<>(); + this.workers.execute(() -> cf.complete(this.loadChunkFromDisk(cx, cz))); + this.mChunk.execute(() -> { + try { + this.finishChunkFromDisk(cf.get(), cx, cz, key); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + runnable.run(); + }); + return null; + } else { + + // Punt to the main worker + if (this.isMChunk.get()) return this.loadChunkFromDisk(cx, cz, key); + + final CompletableFuture cf = new CompletableFuture<>(); + this.mChunk.execute(() -> cf.complete(this.loadChunkFromDisk(cx, cz, key))); + try { + chunk = cf.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + } else { + + // Punt to main generator + if (this.isMChunk.get()) return this.generateChunk(cx, cz, key); + + final CompletableFuture cf = new CompletableFuture<>(); + this.mChunk.execute(() -> cf.complete(this.generateChunk(cx, cz, key))); + try { + chunk = cf.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + } + + // If we didn't load the chunk async and have a callback run it now + if (runnable != null) runnable.run(); + + return chunk; + } + + /** + * Generate a chunk. Blocks until the chunk is done. + */ + @Override + public Chunk originalLoadChunk(int cx, int cz) { + long key = ChunkCoordIntPair.chunkXZ2Int(cx, cz); + this.chunksToUnload.remove(key); + Chunk chunk = (Chunk) this.loadedChunkHashMap.getValueByKey(key); + + if (chunk != null) return chunk; + + if (!loadingChunks.add(key)) { + cpw.mods.fml.common.FMLLog.bigWarning( + "There is an attempt to load a chunk (%d,%d) in dimension %d that is already being loaded. This will cause weird chunk breakages.", + cx, + cz, + worldObj.provider.dimensionId); + } + + chunk = ForgeChunkManager.fetchDormantChunk(key, this.worldObj); + if (chunk == null) { + chunk = this.safeLoadChunk(cx, cz); + } + + final boolean shouldGen = chunk == null; + final CompletableFuture cf = new CompletableFuture<>(); + final Chunk finalChunk = chunk; // java why do I have to do this, this isn't even a deep copy + this.mChunk.execute( + () -> cf.complete( + shouldGen ? this.generateChunk(cx, cz, key) : this.decorateChunk(finalChunk, cx, cz, key))); + + try { + return cf.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + /** + * Provides the chunk, if it's been loaded. Otherwise, if {@link World#findingSpawnPoint} or + * {@link #loadChunkOnProvideRequest}, loads/generates the chunk. If neither is true, returns empty. + */ + @Override + public Chunk provideChunk(int cx, int cz) { + Chunk chunk = (Chunk) this.loadedChunkHashMap.getValueByKey(ChunkCoordIntPair.chunkXZ2Int(cx, cz)); + return chunk != null ? chunk + : this.worldObj.findingSpawnPoint || this.loadChunkOnProvideRequest ? this.loadChunk(cx, cz) + : this.empty; + } + + /** + * Attempts to load a chunk from the save files, if not found returns null. + */ + @Override + public Chunk safeLoadChunk(int cx, int cz) { + if (this.currentChunkLoader == null) { + return null; + } else { + final CompletableFuture cf = new CompletableFuture<>(); + this.mChunk + .execute(() -> cf.complete(this.loadChunkFromDisk(cx, cz, ChunkCoordIntPair.chunkXZ2Int(cx, cz)))); + + try { + return cf.get(); + } catch (Exception exception) { + log.error("Couldn't load chunk", exception); + return null; + } + } + } + + /** + * used by saveChunks, but catches any exceptions if the save fails. + */ + private void safeSaveExtraChunkData(Chunk chunk) { + if (this.currentChunkLoader != null) { + try { + this.currentChunkLoader.saveExtraChunkData(this.worldObj, chunk); + } catch (Exception exception) { + log.error("Couldn't save entities", exception); + } + } + } + + /** + * used by saveChunks, but catches any exceptions if the save fails. + */ + private void safeSaveChunk(Chunk chunk) { + if (this.currentChunkLoader != null) { + try { + chunk.lastSaveTime = this.worldObj.getTotalWorldTime(); + this.currentChunkLoader.saveChunk(this.worldObj, chunk); + } catch (IOException ioexception) { + log.error("Couldn't save chunk", ioexception); + } catch (MinecraftException minecraftexception) { + log.error("Couldn't save chunk; already in use by another instance of Minecraft?", minecraftexception); + } + } + } + + /** + * Populates chunk with ores etc etc + */ + @Override + public void populate(IChunkProvider backingCP, int cx, int cz) { + Chunk chunk = this.provideChunk(cx, cz); + + if (!chunk.isTerrainPopulated) { + chunk.func_150809_p(); + + if (this.currentChunkProvider != null) { + this.localProvider.get().populate(backingCP, cx, cz); + GameRegistry.generateWorld(cx, cz, worldObj, this.localProvider.get(), backingCP); + chunk.setChunkModified(); + } + } + } + + /** + * Two modes of operation: if passed true, save all Chunks in one go. If passed false, save up to two chunks. Return + * true if all chunks have been saved. + */ + @Override + public boolean saveChunks(boolean oneshot, IProgressUpdate p_73151_2_) { + int i = 0; + ObjectArrayList copiedChunks = new ObjectArrayList<>(this.loadedChunks); + + for (int j = 0; j < copiedChunks.size(); ++j) { + Chunk chunk = copiedChunks.get(j); + + if (oneshot) { + this.safeSaveExtraChunkData(chunk); + } + + if (chunk.needsSaving(oneshot)) { + this.safeSaveChunk(chunk); + chunk.isModified = false; + ++i; + + if (i == 24 && !oneshot) { + return false; + } + } + } + + return true; + } + + /** + * Save extra data not associated with any Chunk. Not saved during autosave, only during world unload. Currently + * unimplemented. + */ + @Override + public void saveExtraData() { + if (this.currentChunkLoader != null) { + this.currentChunkLoader.saveExtraData(); + } + } + + /** + * Unloads chunks that are marked to be unloaded. This is not guaranteed to unload every such chunk. + */ + @Override + public boolean unloadQueuedChunks() { + if (!this.worldObj.levelSaving) { + for (ChunkCoordIntPair forced : this.worldObj.getPersistentChunks().keySet()) { + this.chunksToUnload.remove(ChunkCoordIntPair.chunkXZ2Int(forced.chunkXPos, forced.chunkZPos)); + } + + for (int i = 0; i < maxUnloadsPerTick; ++i) { + if (!this.chunksToUnload.isEmpty()) { + long key = this.chunksToUnload.iterator().nextLong(); + Chunk chunk = (Chunk) this.loadedChunkHashMap.getValueByKey(key); + + if (chunk != null) { + chunk.onChunkUnload(); + this.safeSaveChunk(chunk); + this.safeSaveExtraChunkData(chunk); + this.loadedChunks.remove(chunk); + ForgeChunkManager.putDormantChunk( + ChunkCoordIntPair.chunkXZ2Int(chunk.xPosition, chunk.zPosition), + chunk); + if (loadedChunks.isEmpty() && ForgeChunkManager.getPersistentChunksFor(this.worldObj).isEmpty() + && !DimensionManager.shouldLoadSpawn(this.worldObj.provider.dimensionId)) { + DimensionManager.unloadWorld(this.worldObj.provider.dimensionId); + return currentChunkProvider.unloadQueuedChunks(); + } + } + + this.chunksToUnload.remove(key); + this.loadedChunkHashMap.remove(key); + } + } + + if (this.currentChunkLoader != null) { + this.currentChunkLoader.chunkTick(); + } + } + + // This will always return a boolean and do no work on the server, as far as I can tell, so it doesn't matter if + // it's thread-local or not + return this.currentChunkProvider.unloadQueuedChunks(); + } + + /** + * Returns if the IChunkProvider supports saving. + */ + @Override + public boolean canSave() { + return !this.worldObj.levelSaving; + } + + /** + * Converts the instance data to a readable string. + */ + @Override + public String makeString() { + return "ServerChunkCache: " + this.loadedChunkHashMap.getNumHashElements() + + " Drop: " + + this.chunksToUnload.size(); + } + + /** + * Returns a list of creatures of the specified type that can spawn at the given location. + */ + @Override + public List getPossibleCreatures(EnumCreatureType type, int x, int y, int z) { + return this.localProvider.get().getPossibleCreatures(type, x, y, z); + } + + @Override + public ChunkPosition func_147416_a(World world, String p_147416_2_, int x, int y, int z) { + return this.localProvider.get().func_147416_a(world, p_147416_2_, x, y, z); + } + + @Override + public int getLoadedChunkCount() { + return this.loadedChunkHashMap.getNumHashElements(); + } + + /** + * See {@link #doesChunkExist(int, int, long)} + */ + public boolean doesChunkExist(ChunkCoordIntPair coords) { + + final long key = ChunkCoordIntPair.chunkXZ2Int(coords.chunkXPos, coords.chunkZPos); + return this.doesChunkExist(coords.chunkXPos, coords.chunkZPos, key); + } + + /** + * Given chunk coordinates, returns whether that chunk has been generated already. + */ + public boolean doesChunkExist(int cx, int cz, long key) { + + if (this.loadedChunkHashMap.containsItem(key)) return true; + return ((AnvilChunkLoader) this.currentChunkLoader).chunkExists(this.worldObj, cx, cz); + } + +} diff --git a/src/main/java/com/mitchej123/hodgepodge/server/NewIntCache.java b/src/main/java/com/mitchej123/hodgepodge/server/NewIntCache.java new file mode 100644 index 00000000..1a80635a --- /dev/null +++ b/src/main/java/com/mitchej123/hodgepodge/server/NewIntCache.java @@ -0,0 +1,32 @@ +package com.mitchej123.hodgepodge.server; + +import java.util.List; +import java.util.Map; + +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +public class NewIntCache { + + private static final int SMALLEST = 256; + private static final int MIN_LEVEL = 32 - Integer.numberOfLeadingZeros(SMALLEST - 1); + + private static final Map> cachedObjects = new Int2ObjectOpenHashMap<>(); + + public static synchronized int[] getCache(int size) { + + // Get the smallest power of two larger than or equal to the number + final int level = (size <= SMALLEST) ? MIN_LEVEL : 32 - Integer.numberOfLeadingZeros(size - 1); + + final List caches = cachedObjects.computeIfAbsent(level, i -> new ObjectArrayList<>()); + + if (caches.isEmpty()) return new int[2 << (level - 1)]; + return caches.remove(caches.size() - 1); + } + + public static synchronized void releaseCache(int[] cache) { + + final int level = (cache.length <= SMALLEST) ? MIN_LEVEL : 32 - Integer.numberOfLeadingZeros(cache.length - 1); + cachedObjects.computeIfAbsent(level, i -> new ObjectArrayList<>()).add(cache); + } +} diff --git a/src/main/java/com/mitchej123/hodgepodge/util/ChunkPosUtil.java b/src/main/java/com/mitchej123/hodgepodge/util/ChunkPosUtil.java index e9799b2a..02c40ea0 100644 --- a/src/main/java/com/mitchej123/hodgepodge/util/ChunkPosUtil.java +++ b/src/main/java/com/mitchej123/hodgepodge/util/ChunkPosUtil.java @@ -1,8 +1,16 @@ package com.mitchej123.hodgepodge.util; +import java.util.Comparator; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.world.ChunkCoordIntPair; + +import it.unimi.dsi.fastutil.longs.LongComparator; + public class ChunkPosUtil { - public static long INT_MASK = (1L << Integer.SIZE) - 1; + public static final long INT_MASK = (1L << Integer.SIZE) - 1; + public static final long INVALID_COORD = Long.MAX_VALUE; public static int getPackedX(long pos) { return (int) (pos & INT_MASK); @@ -16,4 +24,88 @@ public static long toLong(int x, int z) { return (long) x & 4294967295L | ((long) z & 4294967295L) << 32; } + public static long toLong(Object o) { + + if (o instanceof ChunkCoordIntPair c) return toLong(c.chunkXPos, c.chunkZPos); + + return INVALID_COORD; + } + + public static class ObjComparator implements Comparator { + + private int cx; + private int cz; + + public ObjComparator setPos(int cx, int cz) { + this.cx = cx; + this.cz = cz; + return this; + } + + /** + * Returns a negative value if the first position is closer than the second, and vice versa. Returns zero if the + * positions are identical or equally far from the set position. + */ + @Override + public int compare(ChunkCoordIntPair c1, ChunkCoordIntPair c2) { + + if (c1 == c2) return 0; + + final int dx1 = c1.chunkXPos - cx; + final int dz1 = c1.chunkZPos - cz; + final int dist1Sq = dx1 * dx1 + dz1 * dz1; + + final int dx2 = c2.chunkXPos - cx; + final int dz2 = c2.chunkZPos - cz; + final int dist2Sq = dx2 * dx2 + dz2 * dz2; + + return Integer.compare(dist1Sq, dist2Sq); + } + } + + public static class FastComparator implements LongComparator { + + private int cx; + private int cz; + + public FastComparator setPos(int cx, int cz) { + this.cx = cx; + this.cz = cz; + return this; + } + + public FastComparator setPos(EntityPlayerMP player) { + this.cx = (int) player.posX >> 4; + this.cz = (int) player.posZ >> 4; + return this; + } + + public boolean withinRadius(long key, int renderDistance) { + if (Math.abs(getPackedX(key) - this.cx) > renderDistance) return false; + + if (Math.abs(getPackedZ(key) - this.cz) > renderDistance) return false; + + return true; + } + + /** + * Returns a negative value if the first position is closer than the second, and vice versa. Returns zero if the + * positions are identical or equally far from the set position. + */ + @Override + public int compare(long c1, long c2) { + + if (c1 == c2) return 0; + + final int dx1 = getPackedX(c1) - cx; + final int dz1 = getPackedZ(c1) - cz; + final int dist1Sq = dx1 * dx1 + dz1 * dz1; + + final int dx2 = getPackedX(c2) - cx; + final int dz2 = getPackedZ(c2) - cz; + final int dist2Sq = dx2 * dx2 + dz2 * dz2; + + return Integer.compare(dist1Sq, dist2Sq); + } + } } diff --git a/src/main/resources/META-INF/hodgepodge_at.cfg b/src/main/resources/META-INF/hodgepodge_at.cfg index f1bc32a1..39500866 100644 --- a/src/main/resources/META-INF/hodgepodge_at.cfg +++ b/src/main/resources/META-INF/hodgepodge_at.cfg @@ -21,3 +21,6 @@ public net.minecraft.client.resources.FileResourcePack field_110600_d # resource public net.minecraft.client.resources.SimpleReloadableResourceManager field_110548_a # domainResourceManagers public net.minecraft.entity.EntityList field_75624_e # classToIDMapping public net.minecraft.world.WorldServer func_73053_d()V # wakeAllPlayers() +public net.minecraft.world.gen.ChunkProviderServer func_73239_e(II)Lnet/minecraft/world/chunk/Chunk; # safeLoadChunk +public net.minecraft.server.management.PlayerManager$PlayerInstance +public net.minecraft.server.management.PlayerManager$PlayerInstance field_73264_c # chunkLocation