diff --git a/src/main/java/net/minecraftforge/common/DimensionManager.java b/src/main/java/net/minecraftforge/common/DimensionManager.java index eed21257b..98891bd61 100644 --- a/src/main/java/net/minecraftforge/common/DimensionManager.java +++ b/src/main/java/net/minecraftforge/common/DimensionManager.java @@ -19,6 +19,7 @@ package net.minecraftforge.common; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -27,6 +28,8 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; @@ -44,7 +47,9 @@ import com.google.common.collect.Multiset; import io.netty.buffer.Unpooled; import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.INBT; import net.minecraft.nbt.ListNBT; +import net.minecraft.nbt.StringNBT; import net.minecraft.network.PacketBuffer; import net.minecraft.server.MinecraftServer; import net.minecraft.server.management.PlayerList; @@ -58,6 +63,8 @@ import net.minecraft.world.chunk.listener.IChunkStatusListener; import net.minecraft.world.server.ServerMultiWorld; import net.minecraft.world.server.ServerWorld; import net.minecraft.world.dimension.DimensionType; +import net.minecraft.world.storage.SaveHandler; +import net.minecraftforge.common.util.Constants; import net.minecraftforge.event.world.RegisterDimensionsEvent; import net.minecraftforge.event.world.WorldEvent; import net.minecraftforge.fml.StartupQuery; @@ -65,6 +72,7 @@ import net.minecraftforge.fml.server.ServerModLoader; import net.minecraftforge.registries.ClearableRegistry; import net.minecraftforge.registries.ForgeRegistries; +import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.Validate; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -85,6 +93,7 @@ public class DimensionManager private static final ConcurrentMap weakWorldMap = new MapMaker().weakKeys().weakValues().makeMap(); private static final Multiset leakedWorlds = HashMultiset.create(); private static final Map savedEntries = new HashMap<>(); + private static final List foldersScheduledForDeletion = new ArrayList<>(); private static volatile Set playerWorlds = new HashSet<>(); /** @@ -100,7 +109,8 @@ public class DimensionManager * @param magnifier The biome generation processor * @return the DimensionType for the dimension. */ - public static DimensionType registerOrGetDimension(ResourceLocation name, ModDimension type, PacketBuffer data, boolean hasSkyLight) { + public static DimensionType registerOrGetDimension(ResourceLocation name, ModDimension type, PacketBuffer data, boolean hasSkyLight) + { return REGISTRY.getValue(name).orElseGet(()->registerDimension(name, type, data, hasSkyLight)); } /** @@ -199,14 +209,49 @@ public class DimensionManager return ret; } + /** + * Marks the given dimension for deletion upon next reload of the save. + * The corresponding data directory will be deleted as well. + * + * Note that the dimension need not be currently unloaded. The dimension will stay functioning as normal + * until the save is reloaded. + * + * Vanilla dimensions are not supported and will throw an exception. + * + * @param dim the dimension to delete + */ + public static void markForDeletion(DimensionType dim) + { + if (dim.isVanilla()) + { + throw new IllegalArgumentException("Cannot delete vanilla dimensions."); + } + getData(dim).markedForDeletion = true; + + Path base = Paths.get(".").toAbsolutePath().normalize(); + Path directory = dim.getDirectory(base.toFile()).toPath().toAbsolutePath().normalize(); + if (!directory.startsWith(base)) + { + LOGGER.warn("DimensionType {} returned save directory outside of base directory. This is bad. The directory will not be deleted.", dim); + } + else + { + String relative = base.relativize(directory).toString(); + foldersScheduledForDeletion.add(relative); + } + } + //========================================================================================================== // FORGE INTERNAL //========================================================================================================== + /** + * Deprecated, unregistering dimensions at runtime is not supported. + * @see DimensionManager#markForDeletion(DimensionType) + */ + @Deprecated public static void unregisterDimension(int id) { - Validate.isTrue(dimensions.containsKey(id), String.format("Failed to unregister dimension for id %d; No provider registered", id)); - dimensions.remove(id); } public static DimensionType registerDimensionInternal(int id, ResourceLocation name, ModDimension type, PacketBuffer data, boolean hasSkyLight) @@ -347,7 +392,7 @@ public class DimensionManager data.putInt("version", 1); List list = new ArrayList<>(); for (DimensionType type : REGISTRY) - list.add(new SavedEntry(type)); + list.add(new SavedEntry(type, getData(type).markedForDeletion)); savedEntries.values().forEach(list::add); Collections.sort(list, (a, b) -> a.id - b.id); @@ -355,6 +400,10 @@ public class DimensionManager list.forEach(e -> lst.add(e.write())); data.put("entries", lst); + + ListNBT deleteLst = new ListNBT(); + foldersScheduledForDeletion.forEach(folder -> deleteLst.add(StringNBT.valueOf(folder))); + data.put("delete_dirs", deleteLst); } public static void readRegistry(CompoundNBT data) @@ -396,7 +445,7 @@ public class DimensionManager continue; } } - else + else if (!entry.markedForDeletion) { ModDimension mod = ForgeRegistries.MOD_DIMENSIONS.getValue(entry.type); if (mod == null) @@ -408,6 +457,46 @@ public class DimensionManager registerDimensionInternal(entry.id, entry.name, mod, entry.data == null ? null : new PacketBuffer(Unpooled.wrappedBuffer(entry.data)), entry.skyLight()); } } + + foldersScheduledForDeletion.clear(); + + ListNBT folderList = data.getList("delete_dirs", Constants.NBT.TAG_STRING); + folderList.stream() + .map(INBT::getString) + .forEach(foldersScheduledForDeletion::add); + } + + public static void processScheduledDeletions(SaveHandler saveHandler) + { + List toDelete = new ArrayList<>(foldersScheduledForDeletion); + foldersScheduledForDeletion.clear(); + if (!toDelete.isEmpty()) + { + StringBuilder text = new StringBuilder(); + text.append("The following dimensions are marked for deletion by their owning mod. Proceed?\n\n"); + for (String folder : toDelete) + { + text.append(folder).append('\n'); + } + if (!StartupQuery.confirm(text.toString())) + { + StartupQuery.abort(); + return; + } + } + for (String folderName : toDelete) + { + File folder = new File(saveHandler.getWorldDirectory(), folderName); + try + { + FileUtils.deleteDirectory(folder); + LOGGER.info(DIMMGR, "Modded dimension directory {} scheduled for deletion was deleted.", folderName); + } + catch (Exception e) + { + LOGGER.error(DIMMGR, "Failed to delete modded dimension directory {} that was scheduled for deletion.", folderName, e); + } + } } public static void fireRegister() @@ -436,6 +525,7 @@ public class DimensionManager { int ticksWaited = 0; boolean keepLoaded = false; + boolean markedForDeletion = false; } public static class SavedEntry @@ -445,6 +535,7 @@ public class DimensionManager ResourceLocation type; byte[] data; boolean skyLight; + boolean markedForDeletion; public int getId() { @@ -480,9 +571,10 @@ public class DimensionManager this.type = data.contains("type", 8) ? new ResourceLocation(data.getString("type")) : null; this.data = data.contains("data", 7) ? data.getByteArray("data") : null; this.skyLight = data.contains("sky_light", 99) ? data.getBoolean("sky_light") : true; + this.markedForDeletion = data.getBoolean("marked_for_deletion"); } - private SavedEntry(DimensionType data) + private SavedEntry(DimensionType data, boolean markedForDeletion) { this.id = REGISTRY.getId(data); this.name = REGISTRY.getKey(data); @@ -491,6 +583,7 @@ public class DimensionManager if (data.getData() != null) this.data = data.getData().array(); this.skyLight = data.hasSkyLight(); + this.markedForDeletion = markedForDeletion; } private CompoundNBT write() @@ -503,6 +596,7 @@ public class DimensionManager if (data != null) ret.putByteArray("data", data); ret.putBoolean("sky_light", skyLight); + ret.putBoolean("marked_for_deletion", markedForDeletion); return ret; } } diff --git a/src/main/java/net/minecraftforge/common/ForgeMod.java b/src/main/java/net/minecraftforge/common/ForgeMod.java index 4752f8167..19447b9ae 100644 --- a/src/main/java/net/minecraftforge/common/ForgeMod.java +++ b/src/main/java/net/minecraftforge/common/ForgeMod.java @@ -158,6 +158,7 @@ public class ForgeMod implements WorldPersistenceHooks.WorldPersistenceHook { if (tag.contains("dims", 10)) DimensionManager.readRegistry(tag.getCompound("dims")); + DimensionManager.processScheduledDeletions(handler); } public void mappingChanged(FMLModIdMappingEvent evt) diff --git a/src/test/java/net/minecraftforge/debug/world/MarkDimensionForDeletionTest.java b/src/test/java/net/minecraftforge/debug/world/MarkDimensionForDeletionTest.java new file mode 100644 index 000000000..7c3d7ace1 --- /dev/null +++ b/src/test/java/net/minecraftforge/debug/world/MarkDimensionForDeletionTest.java @@ -0,0 +1,186 @@ +package net.minecraftforge.debug.world; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.ServerPlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.INBT; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Direction; +import net.minecraft.util.Hand; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.text.StringTextComponent; +import net.minecraft.world.World; +import net.minecraft.world.dimension.DimensionType; +import net.minecraft.world.dimension.OverworldDimension; +import net.minecraft.world.server.ServerWorld; +import net.minecraftforge.common.DimensionManager; +import net.minecraftforge.common.ModDimension; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.CapabilityInject; +import net.minecraftforge.common.capabilities.CapabilityManager; +import net.minecraftforge.common.capabilities.ICapabilitySerializable; +import net.minecraftforge.common.util.Constants; +import net.minecraftforge.common.util.ITeleporter; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.event.AttachCapabilitiesEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +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 javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.UUID; +import java.util.function.Function; + +@Mod(MarkDimensionForDeletionTest.MODID) +@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD, modid = MarkDimensionForDeletionTest.MODID) +public class MarkDimensionForDeletionTest +{ + + public static final String MODID = "mark_dimension_for_deletion_test"; + + private static final DeferredRegister DIMENSIONS = new DeferredRegister<>(ForgeRegistries.MOD_DIMENSIONS, MODID); + private static final DeferredRegister ITEMS = new DeferredRegister<>(ForgeRegistries.ITEMS, MODID); + + private static final RegistryObject DYNAMIC_DIMENSION_TYPE = DIMENSIONS.register( + "dynamic_dimension", + () -> ModDimension.withFactory(OverworldDimension::new) + ); + private static final RegistryObject DIM_ITEM = ITEMS.register("dim_item", () -> new Item(new Item.Properties().group(ItemGroup.MISC)) + { + @Override + public ActionResult onItemRightClick(World worldIn, PlayerEntity playerIn, Hand handIn) + { + if (!worldIn.isRemote) + { + if (playerIn.isShiftKeyDown()) + { + playerIn.sendMessage(new StringTextComponent("You are in dimension " + worldIn.dimension.getType().getRegistryName())); + } + else + { + DynamicDimensionCap cap = playerIn.getCapability(CAP).orElseThrow(IllegalStateException::new); + ITeleporter teleporter = new ITeleporter() + { + @Override + public Entity placeEntity(Entity entity, ServerWorld currentWorld, ServerWorld destWorld, float yaw, Function repositionEntity) + { + return repositionEntity.apply(false); + } + }; + if (cap.dimension == null) + { + ResourceLocation dimName = new ResourceLocation(MODID, "dynamic_" + playerIn.getUniqueID().toString() + "_" + UUID.randomUUID().toString()); + cap.dimension = DimensionManager.registerDimension(dimName, DYNAMIC_DIMENSION_TYPE.get(), null, true); + DimensionManager.initWorld(worldIn.getServer(), cap.dimension); + playerIn.changeDimension(cap.dimension, teleporter); + } + else if (playerIn.dimension == cap.dimension) + { + playerIn.changeDimension(DimensionType.OVERWORLD, teleporter); + DimensionManager.markForDeletion(cap.dimension); + } + else + { + playerIn.changeDimension(cap.dimension, teleporter); + } + } + } + return ActionResult.resultSuccess(playerIn.getHeldItem(handIn)); + } + }); + + @CapabilityInject(DynamicDimensionCap.class) + public static Capability CAP; + + public MarkDimensionForDeletionTest() + { + DIMENSIONS.register(FMLJavaModLoadingContext.get().getModEventBus()); + ITEMS.register(FMLJavaModLoadingContext.get().getModEventBus()); + } + + @SubscribeEvent + public static void commonSetup(FMLCommonSetupEvent event) + { + CapabilityManager.INSTANCE.register(DynamicDimensionCap.class, new DynamicDimensionCap.Storage(), DynamicDimensionCap::new); + } + + @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.FORGE, modid = MODID) + public static class ForgeEventSubscriber + { + + @SubscribeEvent + public static void attachCaps(AttachCapabilitiesEvent event) + { + if (event.getObject() instanceof ServerPlayerEntity) + { + event.addCapability(new ResourceLocation(MODID, "dynamic_dimension"), new DynamicDimensionCap()); + } + } + + } + + public static class DynamicDimensionCap implements ICapabilitySerializable + { + + static class Storage implements Capability.IStorage + { + + @Nullable + @Override + public INBT writeNBT(Capability capability, DynamicDimensionCap instance, Direction side) + { + return instance.serializeNBT(); + } + + @Override + public void readNBT(Capability capability, DynamicDimensionCap instance, Direction side, INBT nbt) + { + instance.deserializeNBT((CompoundNBT) nbt); + } + } + + public DimensionType dimension = null; + + private final LazyOptional instance = LazyOptional.of(() -> this); + + @Nonnull + @Override + public LazyOptional getCapability(@Nonnull Capability cap, @Nullable Direction side) + { + return CAP.orEmpty(cap, instance); + } + + @Override + public CompoundNBT serializeNBT() + { + CompoundNBT result = new CompoundNBT(); + if (dimension != null && dimension.getRegistryName() != null) + { + result.putString("dimension", dimension.getRegistryName().toString()); + } + return result; + } + + @Override + public void deserializeNBT(CompoundNBT nbt) + { + if (nbt.contains("dimension", Constants.NBT.TAG_STRING)) + { + dimension = DimensionType.byName(new ResourceLocation(nbt.getString("dimension"))); + } + else + { + dimension = null; + } + } + } +} diff --git a/src/test/resources/META-INF/mods.toml b/src/test/resources/META-INF/mods.toml index ca66e67f8..31d64569b 100644 --- a/src/test/resources/META-INF/mods.toml +++ b/src/test/resources/META-INF/mods.toml @@ -1,6 +1,8 @@ modLoader="javafml" loaderVersion="[28,)" +[[mods]] + modId="mark_dimension_for_deletion_test" [[mods]] modId="containertypetest" [[mods]] @@ -10,6 +12,7 @@ loaderVersion="[28,)" [[mods]] modId="farmland_trample_test" [[mods]] + modId="neighbor_notify_event_test" [[mods]] modId="block_place_event_test"