From 1d7da4c595e868f04d44d943d8fc8b9298faba2c Mon Sep 17 00:00:00 2001 From: Sara Freimer Date: Tue, 9 Feb 2021 17:19:07 -0500 Subject: [PATCH] Reintroduce forge's chunk manager to allow mods to force chunks with either a block position or entity (UUID) "owner" and have them properly persist between restarts (#7642) --- .../server/MinecraftServer.java.patch | 42 +- .../world/ForcedChunksSaveData.java.patch | 33 + .../server/ServerChunkProvider.java.patch | 15 + .../world/server/ServerWorld.java.patch | 9 + .../world/server/TicketManager.java.patch | 17 +- .../common/world/ForgeChunkManager.java | 587 ++++++++++++++++++ src/main/resources/forge.exc | 1 + .../debug/world/ForgeChunkManagerTest.java | 122 ++++ src/test/resources/META-INF/mods.toml | 2 + .../blockstates/chunk_loader.json | 7 + .../models/block/chunk_loader.json | 8 + .../models/item/chunk_loader.json | 3 + 12 files changed, 828 insertions(+), 18 deletions(-) create mode 100644 patches/minecraft/net/minecraft/world/ForcedChunksSaveData.java.patch create mode 100644 src/main/java/net/minecraftforge/common/world/ForgeChunkManager.java create mode 100644 src/test/java/net/minecraftforge/debug/world/ForgeChunkManagerTest.java create mode 100644 src/test/resources/assets/forge_chunk_manager_test/blockstates/chunk_loader.json create mode 100644 src/test/resources/assets/forge_chunk_manager_test/models/block/chunk_loader.json create mode 100644 src/test/resources/assets/forge_chunk_manager_test/models/item/chunk_loader.json diff --git a/patches/minecraft/net/minecraft/server/MinecraftServer.java.patch b/patches/minecraft/net/minecraft/server/MinecraftServer.java.patch index 61e393461b..b8b173f574 100644 --- a/patches/minecraft/net/minecraft/server/MinecraftServer.java.patch +++ b/patches/minecraft/net/minecraft/server/MinecraftServer.java.patch @@ -33,7 +33,15 @@ ServerWorld serverworld = this.func_241755_D_(); field_147145_h.info("Preparing start region for dimension {}", (Object)serverworld.func_234923_W_().func_240901_a_()); BlockPos blockpos = serverworld.func_241135_u_(); -@@ -566,6 +569,7 @@ +@@ -479,6 +482,7 @@ + ChunkPos chunkpos = new ChunkPos(i); + serverworld1.func_72863_F().func_217206_a(chunkpos, true); + } ++ net.minecraftforge.common.world.ForgeChunkManager.reinstatePersistentChunks(serverworld1, forcedchunkssavedata); + } + } + +@@ -566,6 +570,7 @@ for(ServerWorld serverworld1 : this.func_212370_w()) { if (serverworld1 != null) { try { @@ -41,7 +49,7 @@ serverworld1.close(); } catch (IOException ioexception1) { field_147145_h.error("Exception closing the level", (Throwable)ioexception1); -@@ -614,6 +618,7 @@ +@@ -614,6 +619,7 @@ protected void func_240802_v_() { try { if (this.func_71197_b()) { @@ -49,7 +57,7 @@ this.field_211151_aa = Util.func_211177_b(); this.field_147147_p.func_151315_a(new StringTextComponent(this.field_71286_C)); this.field_147147_p.func_151321_a(new ServerStatusResponse.Version(SharedConstants.func_215069_a().getName(), SharedConstants.func_215069_a().getProtocolVersion())); -@@ -643,7 +648,10 @@ +@@ -643,7 +649,10 @@ this.func_240795_b_(longtickdetector); this.field_71296_Q = true; } @@ -60,7 +68,7 @@ this.func_71228_a((CrashReport)null); } } catch (Throwable throwable1) { -@@ -662,6 +670,7 @@ +@@ -662,6 +671,7 @@ field_147145_h.error("We were unable to save this crash report to disk."); } @@ -68,7 +76,7 @@ this.func_71228_a(crashreport); } finally { try { -@@ -670,6 +679,7 @@ +@@ -670,6 +680,7 @@ } catch (Throwable throwable) { field_147145_h.error("Exception stopping the server", throwable); } finally { @@ -76,7 +84,7 @@ this.func_71240_o(); } -@@ -771,6 +781,7 @@ +@@ -771,6 +782,7 @@ protected void func_71217_p(BooleanSupplier p_71217_1_) { long i = Util.func_211178_c(); @@ -84,7 +92,7 @@ ++this.field_71315_w; this.func_71190_q(p_71217_1_); if (i - this.field_147142_T >= 5000000000L) { -@@ -785,6 +796,7 @@ +@@ -785,6 +797,7 @@ Collections.shuffle(Arrays.asList(agameprofile)); this.field_147147_p.func_151318_b().func_151330_a(agameprofile); @@ -92,7 +100,7 @@ } if (this.field_71315_w % 6000 == 0) { -@@ -812,6 +824,7 @@ +@@ -812,6 +825,7 @@ long i1 = Util.func_211178_c(); this.field_213215_ap.func_181747_a(i1 - i); this.field_71304_b.func_76319_b(); @@ -100,7 +108,7 @@ } protected void func_71190_q(BooleanSupplier p_71190_1_) { -@@ -819,7 +832,8 @@ +@@ -819,7 +833,8 @@ this.func_193030_aL().func_73660_a(); this.field_71304_b.func_219895_b("levels"); @@ -110,7 +118,7 @@ this.field_71304_b.func_194340_a(() -> { return serverworld + " " + serverworld.func_234923_W_().func_240901_a_(); }); -@@ -830,6 +844,7 @@ +@@ -830,6 +845,7 @@ } this.field_71304_b.func_76320_a("tick"); @@ -118,7 +126,7 @@ try { serverworld.func_72835_b(p_71190_1_); -@@ -838,9 +853,11 @@ +@@ -838,9 +854,11 @@ serverworld.func_72914_a(crashreport); throw new ReportedException(crashreport); } @@ -130,7 +138,7 @@ } this.field_71304_b.func_219895_b("connection"); -@@ -915,7 +932,7 @@ +@@ -915,7 +933,7 @@ } public String getServerModName() { @@ -139,7 +147,7 @@ } public CrashReport func_71230_b(CrashReport p_71230_1_) { -@@ -928,6 +945,7 @@ +@@ -928,6 +946,7 @@ p_71230_1_.func_85056_g().func_189529_a("Data Packs", () -> { StringBuilder stringbuilder = new StringBuilder(); @@ -147,7 +155,7 @@ for(ResourcePackInfo resourcepackinfo : this.field_195577_ad.func_198980_d()) { if (stringbuilder.length() > 0) { stringbuilder.append(", "); -@@ -1280,6 +1298,7 @@ +@@ -1280,6 +1299,7 @@ this.func_184103_al().func_193244_w(); this.field_200258_al.func_240946_a_(this.field_195576_ac.func_240960_a_()); this.field_240765_ak_.func_195410_a(this.field_195576_ac.func_240970_h_()); @@ -155,7 +163,7 @@ }, this); if (this.func_213162_bc()) { this.func_213161_c(completablefuture::isDone); -@@ -1289,10 +1308,13 @@ +@@ -1289,10 +1309,13 @@ } public static DatapackCodec func_240772_a_(ResourcePackList p_240772_0_, DatapackCodec p_240772_1_, boolean p_240772_2_) { @@ -171,7 +179,7 @@ } else { Set set = Sets.newLinkedHashSet(); -@@ -1442,6 +1464,31 @@ +@@ -1442,6 +1465,31 @@ public abstract boolean func_213199_b(GameProfile p_213199_1_); @@ -203,7 +211,7 @@ public void func_223711_a(Path p_223711_1_) throws IOException { Path path = p_223711_1_.resolve("levels"); -@@ -1570,6 +1617,10 @@ +@@ -1570,6 +1618,10 @@ return this.field_240768_i_; } diff --git a/patches/minecraft/net/minecraft/world/ForcedChunksSaveData.java.patch b/patches/minecraft/net/minecraft/world/ForcedChunksSaveData.java.patch new file mode 100644 index 0000000000..079fbce4cd --- /dev/null +++ b/patches/minecraft/net/minecraft/world/ForcedChunksSaveData.java.patch @@ -0,0 +1,33 @@ +--- a/net/minecraft/world/ForcedChunksSaveData.java ++++ b/net/minecraft/world/ForcedChunksSaveData.java +@@ -14,14 +14,30 @@ + + public void func_76184_a(CompoundNBT p_76184_1_) { + this.field_212439_a = new LongOpenHashSet(p_76184_1_.func_197645_o("Forced")); ++ this.blockForcedChunks = new net.minecraftforge.common.world.ForgeChunkManager.TicketTracker<>(); ++ this.entityForcedChunks = new net.minecraftforge.common.world.ForgeChunkManager.TicketTracker<>(); ++ net.minecraftforge.common.world.ForgeChunkManager.readForgeForcedChunks(p_76184_1_, this.blockForcedChunks, this.entityForcedChunks); + } + + public CompoundNBT func_189551_b(CompoundNBT p_189551_1_) { + p_189551_1_.func_197644_a("Forced", this.field_212439_a.toLongArray()); ++ net.minecraftforge.common.world.ForgeChunkManager.writeForgeForcedChunks(p_189551_1_, this.blockForcedChunks, this.entityForcedChunks); + return p_189551_1_; + } + + public LongSet func_212438_a() { + return this.field_212439_a; + } ++ ++ /* ======================================== FORGE START =====================================*/ ++ private net.minecraftforge.common.world.ForgeChunkManager.TicketTracker blockForcedChunks = new net.minecraftforge.common.world.ForgeChunkManager.TicketTracker<>(); ++ private net.minecraftforge.common.world.ForgeChunkManager.TicketTracker entityForcedChunks = new net.minecraftforge.common.world.ForgeChunkManager.TicketTracker<>(); ++ ++ public net.minecraftforge.common.world.ForgeChunkManager.TicketTracker getBlockForcedChunks() { ++ return this.blockForcedChunks; ++ } ++ ++ public net.minecraftforge.common.world.ForgeChunkManager.TicketTracker getEntityForcedChunks() { ++ return this.entityForcedChunks; ++ } + } diff --git a/patches/minecraft/net/minecraft/world/server/ServerChunkProvider.java.patch b/patches/minecraft/net/minecraft/world/server/ServerChunkProvider.java.patch index 5e49dbc521..00354966ef 100644 --- a/patches/minecraft/net/minecraft/world/server/ServerChunkProvider.java.patch +++ b/patches/minecraft/net/minecraft/world/server/ServerChunkProvider.java.patch @@ -9,3 +9,18 @@ chunk.func_177415_c(chunk.func_177416_w() + j); if (flag1 && (this.field_217246_l || this.field_217247_m) && this.field_73251_h.func_175723_af().func_177730_a(chunk.func_76632_l())) { WorldEntitySpawner.func_234979_a_(this.field_73251_h, chunk, worldentityspawner$entitydensitymanager, this.field_217247_m, this.field_217246_l, flag2); +@@ -429,6 +429,14 @@ + this.field_217240_d.func_219362_d(p_217222_1_, p_217222_2_, p_217222_3_, p_217222_4_); + } + ++ public void registerTickingTicket(TicketType type, ChunkPos pos, int distance, T value) { ++ this.field_217240_d.registerTicking(type, pos, distance, value); ++ } ++ ++ public void releaseTickingTicket(TicketType type, ChunkPos pos, int distance, T value) { ++ this.field_217240_d.releaseTicking(type, pos, distance, value); ++ } ++ + public void func_217206_a(ChunkPos p_217206_1_, boolean p_217206_2_) { + this.field_217240_d.func_219364_a(p_217206_1_, p_217206_2_); + } diff --git a/patches/minecraft/net/minecraft/world/server/ServerWorld.java.patch b/patches/minecraft/net/minecraft/world/server/ServerWorld.java.patch index 67bba4810c..3c6a3add38 100644 --- a/patches/minecraft/net/minecraft/world/server/ServerWorld.java.patch +++ b/patches/minecraft/net/minecraft/world/server/ServerWorld.java.patch @@ -61,6 +61,15 @@ } this.func_229856_ab_(); +@@ -338,7 +343,7 @@ + this.func_147488_Z(); + this.field_211159_Q = false; + iprofiler.func_219895_b("entities"); +- boolean flag3 = !this.field_217491_A.isEmpty() || !this.func_217469_z().isEmpty(); ++ boolean flag3 = !this.field_217491_A.isEmpty() || net.minecraftforge.common.world.ForgeChunkManager.hasForcedChunks(this); //Forge: Replace vanilla's has forced chunk check with forge's that checks both the vanilla and forge added ones + if (flag3) { + this.func_82742_i(); + } @@ -395,7 +400,7 @@ } diff --git a/patches/minecraft/net/minecraft/world/server/TicketManager.java.patch b/patches/minecraft/net/minecraft/world/server/TicketManager.java.patch index 9252a6e530..12b88eb923 100644 --- a/patches/minecraft/net/minecraft/world/server/TicketManager.java.patch +++ b/patches/minecraft/net/minecraft/world/server/TicketManager.java.patch @@ -34,7 +34,22 @@ } public void func_219356_a(TicketType p_219356_1_, ChunkPos p_219356_2_, int p_219356_3_, T p_219356_4_) { -@@ -242,6 +255,11 @@ +@@ -175,6 +188,14 @@ + this.func_219349_b(p_219362_2_.func_201841_a(), ticket); + } + ++ public void registerTicking(TicketType type, ChunkPos pos, int distance, T value) { ++ this.func_219347_a(pos.func_201841_a(), new Ticket<>(type, 33 - distance, value, true)); ++ } ++ ++ public void releaseTicking(TicketType type, ChunkPos pos, int distance, T value) { ++ this.func_219349_b(pos.func_201841_a(), new Ticket<>(type, 33 - distance, value, true)); ++ } ++ + private SortedArraySet> func_229848_e_(long p_229848_1_) { + return this.field_219377_e.computeIfAbsent(p_229848_1_, (p_229851_0_) -> { + return SortedArraySet.func_226172_a_(4); +@@ -242,6 +263,11 @@ return this.field_219384_l.func_225396_a(); } diff --git a/src/main/java/net/minecraftforge/common/world/ForgeChunkManager.java b/src/main/java/net/minecraftforge/common/world/ForgeChunkManager.java new file mode 100644 index 0000000000..2bfd56a829 --- /dev/null +++ b/src/main/java/net/minecraftforge/common/world/ForgeChunkManager.java @@ -0,0 +1,587 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2021. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.common.world; + +import com.mojang.datafixers.util.Pair; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.Function; +import javax.annotation.ParametersAreNonnullByDefault; +import net.minecraft.entity.Entity; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.INBT; +import net.minecraft.nbt.ListNBT; +import net.minecraft.nbt.NBTUtil; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.ForcedChunksSaveData; +import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.server.TicketType; +import net.minecraftforge.common.util.Constants; +import net.minecraftforge.fml.ModList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@ParametersAreNonnullByDefault +public class ForgeChunkManager +{ + private static final Logger LOGGER = LogManager.getLogger(); + private static final TicketType> BLOCK = TicketType.create("forge:block", Comparator.comparing(info -> info)); + private static final TicketType> BLOCK_TICKING = TicketType.create("forge:block_ticking", Comparator.comparing(info -> info)); + private static final TicketType> ENTITY = TicketType.create("forge:entity", Comparator.comparing(info -> info)); + private static final TicketType> ENTITY_TICKING = TicketType.create("forge:entity_ticking", Comparator.comparing(info -> info)); + private static final Map callbacks = new HashMap<>(); + + /** + * Sets the forced chunk loading validation callback for the given mod. This allows for validating and removing no longer valid tickets on world load. + * + * @apiNote This method should be called from a {@link net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent} using one of the {@link + * net.minecraftforge.fml.event.lifecycle.ParallelDispatchEvent} enqueueWork methods. + */ + public static void setForcedChunkLoadingCallback(String modId, LoadingValidationCallback callback) + { + if (ModList.get().isLoaded(modId)) + callbacks.put(modId, callback); + else + LOGGER.warn("A mod attempted to set the forced chunk validation loading callback for an unloaded mod of id: {}", modId); + } + + /** + * Checks if a world has any forced chunks. Mainly used for seeing if a world should continue ticking with no players in it. + */ + public static boolean hasForcedChunks(ServerWorld world) + { + ForcedChunksSaveData data = world.getSavedData().get(ForcedChunksSaveData::new, "chunks"); + if (data == null) return false; + return !data.getChunks().isEmpty() || !data.getBlockForcedChunks().isEmpty() || !data.getEntityForcedChunks().isEmpty(); + } + + /** + * Forces a chunk to be loaded for the given mod with the "owner" of the ticket being a given block position. + * + * @param add {@code true} to force the chunk, {@code false} to unforce the chunk. + * @param ticking {@code true} to make the chunk receive full chunk ticks even if there is no player nearby. + */ + public static boolean forceChunk(ServerWorld world, String modId, BlockPos owner, int chunkX, int chunkZ, boolean add, boolean ticking) + { + return forceChunk(world, modId, owner, chunkX, chunkZ, add, ticking, ticking ? BLOCK_TICKING : BLOCK, ForcedChunksSaveData::getBlockForcedChunks); + } + + /** + * Forces a chunk to be loaded for the given mod with the "owner" of the ticket being the UUID of the given entity. + * + * @param add {@code true} to force the chunk, {@code false} to unforce the chunk. + * @param ticking {@code true} to make the chunk receive full chunk ticks even if there is no player nearby. + */ + public static boolean forceChunk(ServerWorld world, String modId, Entity owner, int chunkX, int chunkZ, boolean add, boolean ticking) + { + return forceChunk(world, modId, owner.getUniqueID(), chunkX, chunkZ, add, ticking); + } + + /** + * Forces a chunk to be loaded for the given mod with the "owner" of the ticket being a given UUID. + * + * @param add {@code true} to force the chunk, {@code false} to unforce the chunk. + * @param ticking {@code true} to make the chunk receive full chunk ticks even if there is no player nearby. + */ + public static boolean forceChunk(ServerWorld world, String modId, UUID owner, int chunkX, int chunkZ, boolean add, boolean ticking) + { + return forceChunk(world, modId, owner, chunkX, chunkZ, add, ticking, ticking ? ENTITY_TICKING : ENTITY, ForcedChunksSaveData::getEntityForcedChunks); + } + + /** + * Forces a chunk to be loaded for the given mod with the given "owner". + * + * @param add {@code true} to force the chunk, {@code false} to unforce the chunk. + * + * @implNote Based on {@link ServerWorld#forceChunk(int, int, boolean)} + */ + private static > boolean forceChunk(ServerWorld world, String modId, T owner, int chunkX, int chunkZ, boolean add, boolean ticking, + TicketType> type, Function> ticketGetter) + { + if (!ModList.get().isLoaded(modId)) + { + LOGGER.warn("A mod attempted to force a chunk for an unloaded mod of id: {}", modId); + return false; + } + ForcedChunksSaveData saveData = world.getSavedData().getOrCreate(ForcedChunksSaveData::new, "chunks"); + ChunkPos pos = new ChunkPos(chunkX, chunkZ); + long chunk = pos.asLong(); + TicketTracker tickets = ticketGetter.apply(saveData); + TicketOwner ticketOwner = new TicketOwner<>(modId, owner); + boolean success; + if (add) + { + success = tickets.add(ticketOwner, chunk, ticking); + if (success) + world.getChunk(chunkX, chunkZ); + } + else + { + success = tickets.remove(ticketOwner, chunk, ticking); + } + if (success) + { + saveData.setDirty(true); + forceChunk(world, pos, type, ticketOwner, add, ticking); + } + return success; + } + + /** + * Adds/Removes a ticket from the world's chunk provider with the proper levels to match the forced chunks. + * + * @param add {@code true} to force the chunk, {@code false} to unforce the chunk. + * @param ticking {@code true} to make the chunk receive full chunk ticks even if there is no player nearby. + * + * @implNote We use distance 2 for what we pass, as when using register/releaseTicket the ticket's level is set to 33 - distance and the level that forced chunks use + * is 31. + */ + private static > void forceChunk(ServerWorld world, ChunkPos pos, TicketType> type, TicketOwner owner, boolean add, + boolean ticking) + { + if (add) + { + if (ticking) + world.getChunkProvider().registerTickingTicket(type, pos, 2, owner); + else + world.getChunkProvider().registerTicket(type, pos, 2, owner); + } + else if (ticking) + world.getChunkProvider().releaseTickingTicket(type, pos, 2, owner); + else + world.getChunkProvider().releaseTicket(type, pos, 2, owner); + } + + /** + * Reinstates forge's forced chunks when vanilla initially loads a world and reinstates their forced chunks. This method also will validate all of forge's forced + * chunks using and registered {@link LoadingValidationCallback}. + * + * @apiNote Internal + */ + public static void reinstatePersistentChunks(ServerWorld world, ForcedChunksSaveData saveData) + { + if (!callbacks.isEmpty()) + { + //If we have any callbacks, gather all owned tickets by modid for both blocks and entities + Map>> blockTickets = gatherTicketsByModId(saveData.getBlockForcedChunks()); + Map>> entityTickets = gatherTicketsByModId(saveData.getEntityForcedChunks()); + //Fire the callbacks allowing them to remove any tickets they don't want anymore + for (Map.Entry entry : callbacks.entrySet()) + { + String modId = entry.getKey(); + boolean hasBlockTicket = blockTickets.containsKey(modId); + boolean hasEntityTicket = entityTickets.containsKey(modId); + if (hasBlockTicket || hasEntityTicket) + { + Map> ownedBlockTickets = hasBlockTicket ? Collections.unmodifiableMap(blockTickets.get(modId)) : Collections.emptyMap(); + Map> ownedEntityTickets = hasEntityTicket ? Collections.unmodifiableMap(entityTickets.get(modId)) : Collections.emptyMap(); + entry.getValue().validateTickets(world, new TicketHelper(saveData, modId, ownedBlockTickets, ownedEntityTickets)); + } + } + } + //Reinstate the chunks that we want to load + reinstatePersistentChunks(world, BLOCK, saveData.getBlockForcedChunks().chunks, false); + reinstatePersistentChunks(world, BLOCK_TICKING, saveData.getBlockForcedChunks().tickingChunks, true); + reinstatePersistentChunks(world, ENTITY, saveData.getEntityForcedChunks().chunks, false); + reinstatePersistentChunks(world, ENTITY_TICKING, saveData.getEntityForcedChunks().tickingChunks, true); + } + + /** + * Gathers tickets into a mod filtered map for use in providing all tickets a mod has registered to its {@link LoadingValidationCallback}. + */ + private static > Map>> gatherTicketsByModId(TicketTracker tickets) + { + Map>> modSortedOwnedChunks = new HashMap<>(); + gatherTicketsByModId(tickets.chunks, Pair::getFirst, modSortedOwnedChunks); + gatherTicketsByModId(tickets.tickingChunks, Pair::getSecond, modSortedOwnedChunks); + return modSortedOwnedChunks; + } + + /** + * Gathers tickets into a mod filtered map for use in providing all tickets a mod has registered to its {@link LoadingValidationCallback}. + */ + private static > void gatherTicketsByModId(Map, LongSet> tickets, Function, LongSet> typeGetter, + Map>> modSortedOwnedChunks) + { + for (Map.Entry, LongSet> entry : tickets.entrySet()) + { + Pair pair = modSortedOwnedChunks.computeIfAbsent(entry.getKey().modId, modId -> new HashMap<>()) + .computeIfAbsent(entry.getKey().owner, owner -> new Pair<>(new LongOpenHashSet(), new LongOpenHashSet())); + typeGetter.apply(pair).addAll(entry.getValue()); + } + } + + /** + * Adds back any persistent forced chunks to the world's chunk provider. + */ + private static > void reinstatePersistentChunks(ServerWorld world, TicketType> type, + Map, LongSet> tickets, boolean ticking) + { + for (Map.Entry, LongSet> entry : tickets.entrySet()) + { + for (long chunk : entry.getValue()) + { + forceChunk(world, new ChunkPos(chunk), type, entry.getKey(), true, ticking); + } + } + } + + /** + * Writes the forge forced chunks into the NBT compound. Format is List{modid, List{ChunkPos, List{BlockPos}, List{UUID}}} + * + * @apiNote Internal + */ + public static void writeForgeForcedChunks(CompoundNBT nbt, TicketTracker blockForcedChunks, TicketTracker entityForcedChunks) + { + if (!blockForcedChunks.isEmpty() || !entityForcedChunks.isEmpty()) + { + Map> forcedEntries = new HashMap<>(); + writeForcedChunkOwners(forcedEntries, blockForcedChunks, "Blocks", Constants.NBT.TAG_COMPOUND, (pos, forcedBlocks) -> forcedBlocks.add(NBTUtil.writeBlockPos(pos))); + writeForcedChunkOwners(forcedEntries, entityForcedChunks, "Entities", Constants.NBT.TAG_INT_ARRAY, (uuid, forcedEntities) -> forcedEntities.add(NBTUtil.func_240626_a_(uuid))); + ListNBT forcedChunks = new ListNBT(); + for (Map.Entry> entry : forcedEntries.entrySet()) + { + CompoundNBT forcedEntry = new CompoundNBT(); + forcedEntry.putString("Mod", entry.getKey()); + ListNBT modForced = new ListNBT(); + modForced.addAll(entry.getValue().values()); + forcedEntry.put("ModForced", modForced); + forcedChunks.add(forcedEntry); + } + nbt.put("ForgeForced", forcedChunks); + } + } + + private static > void writeForcedChunkOwners(Map> forcedEntries, TicketTracker tracker, + String listKey, int listType, BiConsumer ownerWriter) + { + writeForcedChunkOwners(forcedEntries, tracker.chunks, listKey,listType, ownerWriter); + writeForcedChunkOwners(forcedEntries, tracker.tickingChunks, "Ticking" + listKey, listType, ownerWriter); + } + + private static > void writeForcedChunkOwners(Map> forcedEntries, + Map, LongSet> forcedChunks, String listKey, int listType, BiConsumer ownerWriter) + { + for (Map.Entry, LongSet> entry : forcedChunks.entrySet()) + { + Long2ObjectMap modForced = forcedEntries.computeIfAbsent(entry.getKey().modId, modId -> new Long2ObjectOpenHashMap<>()); + for (long chunk : entry.getValue()) + { + CompoundNBT modEntry = modForced.computeIfAbsent(chunk, chunkPos -> { + CompoundNBT baseEntry = new CompoundNBT(); + baseEntry.putLong("Chunk", chunkPos); + return baseEntry; + }); + ListNBT ownerList = modEntry.getList(listKey, listType); + ownerWriter.accept(entry.getKey().owner, ownerList); + //Note: As getList returns a new list in the case the data is of the wrong type, + // we need to mimic was vanilla does in various places and put our list back in + // the CompoundNBT regardless. + modEntry.put(listKey, ownerList); + } + } + } + + /** + * Reads the forge forced chunks from the NBT compound. Format is List{modid, List{ChunkPos, List{BlockPos}, List{UUID}}} + * + * @apiNote Internal + */ + public static void readForgeForcedChunks(CompoundNBT nbt, TicketTracker blockForcedChunks, TicketTracker entityForcedChunks) + { + ListNBT forcedChunks = nbt.getList("ForgeForced", Constants.NBT.TAG_COMPOUND); + for (int i = 0; i < forcedChunks.size(); i++) + { + CompoundNBT forcedEntry = forcedChunks.getCompound(i); + String modId = forcedEntry.getString("Mod"); + if (ModList.get().isLoaded(modId)) + { + ListNBT modForced = forcedEntry.getList("ModForced", Constants.NBT.TAG_COMPOUND); + for (int j = 0; j < modForced.size(); j++) + { + CompoundNBT modEntry = modForced.getCompound(j); + long chunkPos = modEntry.getLong("Chunk"); + readBlockForcedChunks(modId, chunkPos, modEntry, "Blocks", blockForcedChunks.chunks); + readBlockForcedChunks(modId, chunkPos, modEntry, "TickingBlocks", blockForcedChunks.tickingChunks); + readEntityForcedChunks(modId, chunkPos, modEntry, "Entities", entityForcedChunks.chunks); + readEntityForcedChunks(modId, chunkPos, modEntry, "TickingEntities", entityForcedChunks.tickingChunks); + } + } + else + { + LOGGER.warn("Found chunk loading data for mod {} which is currently not available or active - it will be removed from the world save.", modId); + } + } + } + + /** + * Reads the forge block forced chunks. + */ + private static void readBlockForcedChunks(String modId, long chunkPos, CompoundNBT modEntry, String key, Map, LongSet> blockForcedChunks) + { + ListNBT forcedBlocks = modEntry.getList(key, Constants.NBT.TAG_COMPOUND); + for (int k = 0; k < forcedBlocks.size(); k++) + { + blockForcedChunks.computeIfAbsent(new TicketOwner<>(modId, NBTUtil.readBlockPos(forcedBlocks.getCompound(k))), owner -> new LongOpenHashSet()).add(chunkPos); + } + } + + /** + * Reads the forge entity forced chunks. + */ + private static void readEntityForcedChunks(String modId, long chunkPos, CompoundNBT modEntry, String key, Map, LongSet> entityForcedChunks) + { + ListNBT forcedEntities = modEntry.getList(key, Constants.NBT.TAG_INT_ARRAY); + for (INBT uuid : forcedEntities) + { + entityForcedChunks.computeIfAbsent(new TicketOwner<>(modId, NBTUtil.readUniqueId(uuid)), owner -> new LongOpenHashSet()).add(chunkPos); + } + } + + @FunctionalInterface + public interface LoadingValidationCallback { + /** + * Called back when tickets are about to be loaded and reinstated to allow mods to invalidate and remove specific tickets that may no longer be valid. + * + * @param world The world + * @param ticketHelper Ticket helper to remove any invalid tickets. + */ + void validateTickets(ServerWorld world, TicketHelper ticketHelper); + } + + /** + * Class to help mods remove no longer valid tickets. + */ + public static class TicketHelper + { + private final Map> blockTickets; + private final Map> entityTickets; + private final ForcedChunksSaveData saveData; + private final String modId; + + private TicketHelper(ForcedChunksSaveData saveData, String modId, Map> blockTickets, Map> entityTickets) + { + this.saveData = saveData; + this.modId = modId; + this.blockTickets = blockTickets; + this.entityTickets = entityTickets; + } + + /** + * Gets all "BLOCK" tickets this mod had registered and which block positions are forcing which chunks. First element of the pair is the non-fully ticking + * tickets, second element is the fully ticking tickets. + * + * @apiNote This map is unmodifiable and does not update to reflect removed tickets so it is safe to call the remove methods while iterating it. + */ + public Map> getBlockTickets() + { + return blockTickets; + } + + /** + * Gets all "ENTITY" tickets this mod had registered and which entity (UUID) is forcing which chunks. First element of the pair is the non-fully ticking + * tickets, second element is the fully ticking tickets. + * + * @apiNote This map is unmodifiable and does not update to reflect removed tickets so it is safe to call the remove methods while iterating it. + */ + public Map> getEntityTickets() + { + return entityTickets; + } + + /** + * Removes all tickets that a given block was responsible for; both ticking and not ticking. + * + * @param owner Block that was responsible. + */ + public void removeAllTickets(BlockPos owner) + { + removeAllTickets(saveData.getBlockForcedChunks(), owner); + } + + /** + * Removes all tickets that a given entity (UUID) was responsible for; both ticking and not ticking. + * + * @param owner Entity (UUID) that was responsible. + */ + public void removeAllTickets(UUID owner) + { + removeAllTickets(saveData.getEntityForcedChunks(), owner); + } + + /** + * Removes all tickets that a given owner was responsible for; both ticking and not ticking. + */ + private > void removeAllTickets(TicketTracker tickets, T owner) + { + TicketOwner ticketOwner = new TicketOwner<>(modId, owner); + if (tickets.chunks.containsKey(ticketOwner) || tickets.tickingChunks.containsKey(ticketOwner)) + { + tickets.chunks.remove(ticketOwner); + tickets.tickingChunks.remove(ticketOwner); + saveData.setDirty(true); + } + } + + /** + * Removes the ticket for the given chunk that a given block was responsible for. + * + * @param owner Block that was responsible. + * @param chunk Chunk to remove ticket of. + * @param ticking Whether or not the ticket to remove represents a ticking set of tickets or not. + */ + public void removeTicket(BlockPos owner, long chunk, boolean ticking) + { + removeTicket(saveData.getBlockForcedChunks(), owner, chunk, ticking); + } + + /** + * Removes the ticket for the given chunk that a given entity (UUID) was responsible for. + * + * @param owner Entity (UUID) that was responsible. + * @param chunk Chunk to remove ticket of. + * @param ticking Whether or not the ticket to remove represents a ticking set of tickets or not. + */ + public void removeTicket(UUID owner, long chunk, boolean ticking) + { + removeTicket(saveData.getEntityForcedChunks(), owner, chunk, ticking); + } + + private > void removeTicket(TicketTracker tickets, T owner, long chunk, boolean ticking) + { + if (tickets.remove(new TicketOwner<>(modId, owner), chunk, ticking)) + saveData.setDirty(true); + } + } + + /** + * Helper class to keep track of a ticket owner by modid and owner object + */ + public static class TicketOwner> implements Comparable> + { + private final String modId; + private final T owner; + + private TicketOwner(String modId, T owner) + { + this.modId = modId; + this.owner = owner; + } + + @Override + public int compareTo(TicketOwner other) + { + int res = modId.compareTo(other.modId); + return res == 0 ? owner.compareTo(other.owner) : res; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TicketOwner that = (TicketOwner) o; + return Objects.equals(modId, that.modId) && Objects.equals(owner, that.owner); + } + + @Override + public int hashCode() + { + return Objects.hash(modId, owner); + } + } + + /** + * Helper class to manage tracking and handling loaded tickets. + */ + public static class TicketTracker> + { + private final Map, LongSet> chunks = new HashMap<>(); + private final Map, LongSet> tickingChunks = new HashMap<>(); + + /** + * Gets an unmodifiable view of the tracked chunks. + */ + public Map, LongSet> getChunks() + { + return Collections.unmodifiableMap(chunks); + } + + /** + * Gets an unmodifiable view of the tracked fully ticking chunks. + */ + public Map, LongSet> getTickingChunks() + { + return Collections.unmodifiableMap(tickingChunks); + } + + /** + * Checks if this tracker is empty. + * + * @return {@code true} if there are no chunks or ticking chunks being tracked. + */ + public boolean isEmpty() + { + return chunks.isEmpty() && tickingChunks.isEmpty(); + } + + private Map, LongSet> getTickets(boolean ticking) + { + return ticking ? tickingChunks : chunks; + } + + /** + * @return {@code true} if the state changed. + */ + private boolean remove(TicketOwner owner, long chunk, boolean ticking) + { + Map, LongSet> tickets = getTickets(ticking); + if (tickets.containsKey(owner)) + { + LongSet ticketChunks = tickets.get(owner); + if (ticketChunks.remove(chunk)) + { + if (ticketChunks.isEmpty()) + tickets.remove(owner); + return true; + } + } + return false; + } + + /** + * @return {@code true} if the state changed. + */ + private boolean add(TicketOwner owner, long chunk, boolean ticking) + { + return getTickets(ticking).computeIfAbsent(owner, o -> new LongOpenHashSet()).add(chunk); + } + } +} \ No newline at end of file diff --git a/src/main/resources/forge.exc b/src/main/resources/forge.exc index 42050c82da..0a9059969b 100644 --- a/src/main/resources/forge.exc +++ b/src/main/resources/forge.exc @@ -58,5 +58,6 @@ net/minecraft/world/gen/feature/template/Template.processEntityInfos(Lnet/minecr net/minecraft/world/server/ServerWorld.removeEntity(Lnet/minecraft/entity/Entity;Z)V=|p_217467_1_,keepData net/minecraft/world/server/ServerWorld.removeEntityComplete(Lnet/minecraft/entity/Entity;Z)V=|p_217484_1_,keepData net/minecraft/world/server/ServerWorld.removePlayer(Lnet/minecraft/entity/player/ServerPlayerEntity;Z)V=|p_217434_1_,keepData +net/minecraft/world/server/Ticket.(Lnet/minecraft/world/server/TicketType;ILjava/lang/Object;Z)V=|p_i226095_1_,p_i226095_2_,p_i226095_3_,forceTicks net/minecraft/world/spawner/WorldEntitySpawner.canSpawnAtBody(Lnet/minecraft/entity/EntitySpawnPlacementRegistry$PlacementType;Lnet/minecraft/world/IWorldReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/entity/EntityType;)Z=|p_209382_0_,p_209382_1_,p_209382_2_,p_209382_3_ net/minecraft/world/storage/SaveFormat.getReader(Lcom/mojang/serialization/DynamicOps;Lnet/minecraft/util/datafix/codec/DatapackCodec;Lnet/minecraft/world/storage/SaveFormat$LevelSave;)Ljava/util/function/BiFunction;=|p_237270_0_,p_237270_1_,levelSave \ No newline at end of file diff --git a/src/test/java/net/minecraftforge/debug/world/ForgeChunkManagerTest.java b/src/test/java/net/minecraftforge/debug/world/ForgeChunkManagerTest.java new file mode 100644 index 0000000000..22bcbac481 --- /dev/null +++ b/src/test/java/net/minecraftforge/debug/world/ForgeChunkManagerTest.java @@ -0,0 +1,122 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2021. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.debug.world; + +import com.mojang.datafixers.util.Pair; +import it.unimi.dsi.fastutil.longs.LongSet; +import java.util.Map; +import java.util.UUID; +import net.minecraft.block.AbstractBlock; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.material.Material; +import net.minecraft.item.BlockItem; +import net.minecraft.item.Item; +import net.minecraft.item.ItemGroup; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.World; +import net.minecraft.world.server.ServerWorld; +import net.minecraftforge.common.world.ForgeChunkManager; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.fml.RegistryObject; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.ForgeRegistries; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@Mod(ForgeChunkManagerTest.MODID) +public class ForgeChunkManagerTest +{ + public static final String MODID = "forge_chunk_manager_test"; + private static final Logger LOGGER = LogManager.getLogger(MODID); + private static final DeferredRegister BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, MODID); + private static final DeferredRegister ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, MODID); + private static final RegistryObject CHUNK_LOADER_BLOCK = BLOCKS.register("chunk_loader", () -> new ChunkLoaderBlock(AbstractBlock.Properties.create(Material.ROCK))); + private static final RegistryObject CHUNK_LOADER_ITEM = ITEMS.register("chunk_loader", () -> new BlockItem(CHUNK_LOADER_BLOCK.get(), new Item.Properties().group(ItemGroup.MISC))); + + public ForgeChunkManagerTest() + { + IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus(); + modEventBus.addListener(this::commonSetup); + BLOCKS.register(modEventBus); + ITEMS.register(modEventBus); + } + + private void commonSetup(FMLCommonSetupEvent event) + { + event.enqueueWork(() -> ForgeChunkManager.setForcedChunkLoadingCallback(MODID, (world, ticketHelper) -> { + for (Map.Entry> entry : ticketHelper.getBlockTickets().entrySet()) + { + BlockPos key = entry.getKey(); + int ticketCount = entry.getValue().getFirst().size(); + int tickingTicketCount = entry.getValue().getSecond().size(); + if (world.getBlockState(key).isIn(CHUNK_LOADER_BLOCK.get())) + LOGGER.info("Allowing {} chunk tickets and {} ticking chunk tickets to be reinstated for position: {}.", ticketCount, tickingTicketCount, key); + else + { + ticketHelper.removeAllTickets(key); + LOGGER.info("Removing {} chunk tickets and {} ticking chunk tickets for no longer valid position: {}.", ticketCount, tickingTicketCount, key); + } + } + for (Map.Entry> entry : ticketHelper.getEntityTickets().entrySet()) + { + UUID key = entry.getKey(); + int ticketCount = entry.getValue().getFirst().size(); + int tickingTicketCount = entry.getValue().getSecond().size(); + LOGGER.info("Allowing {} chunk tickets and {} ticking chunk tickets to be reinstated for entity: {}.", ticketCount, tickingTicketCount, key); + } + })); + } + + private static class ChunkLoaderBlock extends Block + { + + public ChunkLoaderBlock(Properties properties) + { + super(properties); + } + + @Override + public void onBlockAdded(BlockState state, World worldIn, BlockPos pos, BlockState oldState, boolean isMoving) + { + super.onBlockAdded(state, worldIn, pos, oldState, isMoving); + if (worldIn instanceof ServerWorld) + { + ChunkPos chunkPos = new ChunkPos(pos); + ForgeChunkManager.forceChunk((ServerWorld) worldIn, MODID, pos, chunkPos.x, chunkPos.z, true, false); + } + } + + @Deprecated + public void onReplaced(BlockState state, World worldIn, BlockPos pos, BlockState newState, boolean isMoving) + { + super.onReplaced(state, worldIn, pos, newState, isMoving); + if (worldIn instanceof ServerWorld && !state.isIn(newState.getBlock())) + { + ChunkPos chunkPos = new ChunkPos(pos); + ForgeChunkManager.forceChunk((ServerWorld) worldIn, MODID, pos, chunkPos.x, chunkPos.z, false, false); + } + } + } +} diff --git a/src/test/resources/META-INF/mods.toml b/src/test/resources/META-INF/mods.toml index d360239f8f..46ead716a5 100644 --- a/src/test/resources/META-INF/mods.toml +++ b/src/test/resources/META-INF/mods.toml @@ -90,6 +90,8 @@ license="LGPL v2.1" modId="custom_tag_types_test" [[mods]] modId="biome_loading_event_test" +[[mods]] + modId="forge_chunk_manager_test" [[mods]] modId="living_conversion_event_test" [[mods]] diff --git a/src/test/resources/assets/forge_chunk_manager_test/blockstates/chunk_loader.json b/src/test/resources/assets/forge_chunk_manager_test/blockstates/chunk_loader.json new file mode 100644 index 0000000000..95060976ee --- /dev/null +++ b/src/test/resources/assets/forge_chunk_manager_test/blockstates/chunk_loader.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "forge_chunk_manager_test:block/chunk_loader" + } + } +} \ No newline at end of file diff --git a/src/test/resources/assets/forge_chunk_manager_test/models/block/chunk_loader.json b/src/test/resources/assets/forge_chunk_manager_test/models/block/chunk_loader.json new file mode 100644 index 0000000000..658da9dab7 --- /dev/null +++ b/src/test/resources/assets/forge_chunk_manager_test/models/block/chunk_loader.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/cube_bottom_top", + "textures": { + "top": "minecraft:block/respawn_anchor_top", + "bottom": "minecraft:block/respawn_anchor_bottom", + "side": "minecraft:block/respawn_anchor_side4" + } +} \ No newline at end of file diff --git a/src/test/resources/assets/forge_chunk_manager_test/models/item/chunk_loader.json b/src/test/resources/assets/forge_chunk_manager_test/models/item/chunk_loader.json new file mode 100644 index 0000000000..065c8807f1 --- /dev/null +++ b/src/test/resources/assets/forge_chunk_manager_test/models/item/chunk_loader.json @@ -0,0 +1,3 @@ +{ + "parent": "forge_chunk_manager_test:block/chunk_loader" +} \ No newline at end of file