diff --git a/dependencies.gradle b/dependencies.gradle index 15ebdab..b2bb2fc 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -4,7 +4,7 @@ dependencies { } annotationProcessor("org.projectlombok:lombok:1.18.28") - implementation("com.falsepattern:falsepatternlib-mc1.7.10:0.11.4:dev") + implementation("com.falsepattern:falsepatternlib-mc1.7.10:0.12.5:dev") compileOnly("org.jetbrains:annotations:24.0.1") diff --git a/gradle.properties b/gradle.properties index c65acde..56ec6bd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -171,7 +171,7 @@ accessTransformersFile = # Specify the core mod entry class if you use a core mod. This class must implement IFMLLoadingPlugin! # This parameter is for legacy compatibility only # Example value: coreModClass = asm.FMLPlugin + modGroup = com.falsepattern.chunk -> com.falsepattern.chunk.asm.FMLPlugin -coreModClass = +coreModClass = internal.core.CoreLoadingPlugin #endregion core diff --git a/src/main/java/com/falsepattern/chunk/api/ChunkDataManager.java b/src/main/java/com/falsepattern/chunk/api/ChunkDataManager.java index d1dfd28..ab5939d 100644 --- a/src/main/java/com/falsepattern/chunk/api/ChunkDataManager.java +++ b/src/main/java/com/falsepattern/chunk/api/ChunkDataManager.java @@ -9,6 +9,8 @@ package com.falsepattern.chunk.api; import com.falsepattern.lib.StableAPI; import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.chunk.Chunk; @@ -84,6 +86,49 @@ public interface ChunkDataManager { void readFromBuffer(Chunk chunk, int ebsMask, boolean forceUpdate, ByteBuffer buffer); } + /** + * The common superinterface for ChunkNBTDataManager and SectionNBTDataManager. + * Contains version information and messages for users attempting to upgrade/remove versions. + */ + @StableAPI(since = "0.4.0") + interface NBTDataManager extends ChunkDataManager { + /** + * @return The current version of the data manager + */ + @StableAPI.Expose + @Contract(pure = true) + @NotNull + String version(); + + /** + * @return The message to show to users when a world is opened with this mod for the first time. + * Return null to show no message, and treat the manager as fully compatible with vanilla. + */ + @StableAPI.Expose + @Contract(pure = true) + @Nullable + String newInstallDescription(); + + /** + * @return The message to show to users when this mod is removed and they try to load the world (stored in the + * world's NBT during save) + */ + @StableAPI.Expose + @Contract(pure = true) + @NotNull + String uninstallMessage(); + + /** + * @param priorVersion The version of the manager this world was saved with. + * @return A warning message to show to the user when upgrading.\ + * If null, the manager is treated as fully compatible with the old version, and no warning is shown. + */ + @StableAPI.Expose + @Contract(pure = true) + @Nullable + String versionChangeMessage(String priorVersion); + } + /** * Implement this interface if you want to save your data to disk. This is called once per chunk. * @@ -92,7 +137,7 @@ public interface ChunkDataManager { * @version 0.1.0 */ @StableAPI(since = "0.1.0") - interface ChunkNBTDataManager extends ChunkDataManager { + interface ChunkNBTDataManager extends NBTDataManager { /** * If false, the given nbt compound will be a freshly created object that gets inserted into the actual * level NBT tag under the `domain:id` name. @@ -133,7 +178,7 @@ public interface ChunkDataManager { * @version 0.1.0 */ @StableAPI(since = "0.1.0") - interface SectionNBTDataManager extends ChunkDataManager { + interface SectionNBTDataManager extends NBTDataManager { /** * If false, the given nbt compound will be a freshly created object that gets inserted into the actual * segment NBT tag under the `domain:id` name. diff --git a/src/main/java/com/falsepattern/chunk/api/ChunkDataRegistry.java b/src/main/java/com/falsepattern/chunk/api/ChunkDataRegistry.java index 2092421..1df27a0 100644 --- a/src/main/java/com/falsepattern/chunk/api/ChunkDataRegistry.java +++ b/src/main/java/com/falsepattern/chunk/api/ChunkDataRegistry.java @@ -23,7 +23,7 @@ import java.util.Set; @StableAPI(since = "0.1.0") public class ChunkDataRegistry { /** - * Registers a ChunkDataManager. Only do this during the preInit phase. + * Registers a ChunkDataManager. Only do this during the init phase. * * @param manager The manager to register. * @@ -37,7 +37,7 @@ public class ChunkDataRegistry { } /** - * Disables a ChunkDataManager. Only do this during the preInit phase. + * Disables a ChunkDataManager. Only do this during the postInit phase. * * @param id The id of the manager to disable. * diff --git a/src/main/java/com/falsepattern/chunk/internal/ChunkDataRegistryImpl.java b/src/main/java/com/falsepattern/chunk/internal/ChunkDataRegistryImpl.java index f613953..9acf5fe 100644 --- a/src/main/java/com/falsepattern/chunk/internal/ChunkDataRegistryImpl.java +++ b/src/main/java/com/falsepattern/chunk/internal/ChunkDataRegistryImpl.java @@ -11,13 +11,18 @@ import com.falsepattern.chunk.api.ChunkDataManager; import lombok.Data; import lombok.val; import lombok.var; +import org.jetbrains.annotations.Nullable; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.storage.ExtendedBlockStorage; +import net.minecraftforge.common.util.Constants; import cpw.mods.fml.common.Loader; import cpw.mods.fml.common.LoaderState; +import cpw.mods.fml.common.StartupQuery; +import cpw.mods.fml.common.ZipperUtil; +import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; @@ -26,10 +31,12 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; public class ChunkDataRegistryImpl { private static final Set managers = new HashSet<>(); private static final Map packetManagers = new HashMap<>(); + private static final Map NBTManagers = new HashMap<>(); private static final Map chunkNBTManagers = new HashMap<>(); private static final Map sectionNBTManagers = new HashMap<>(); private static final Set disabledManagers = new HashSet<>(); @@ -64,11 +71,14 @@ public class ChunkDataRegistryImpl { maxPacketSize += 4 + id.getBytes(StandardCharsets.UTF_8).length + 4 + maxSize; packetManagers.put(id, new PacketManagerInfo(maxSize, packetManager)); } - if (manager instanceof ChunkDataManager.ChunkNBTDataManager) { - chunkNBTManagers.put(id, (ChunkDataManager.ChunkNBTDataManager) manager); - } - if (manager instanceof ChunkDataManager.SectionNBTDataManager) { - sectionNBTManagers.put(id, (ChunkDataManager.SectionNBTDataManager) manager); + if (manager instanceof ChunkDataManager.NBTDataManager) { + NBTManagers.put(id, (ChunkDataManager.NBTDataManager) manager); + if (manager instanceof ChunkDataManager.ChunkNBTDataManager) { + chunkNBTManagers.put(id, (ChunkDataManager.ChunkNBTDataManager) manager); + } + if (manager instanceof ChunkDataManager.SectionNBTDataManager) { + sectionNBTManagers.put(id, (ChunkDataManager.SectionNBTDataManager) manager); + } } } @@ -89,6 +99,7 @@ public class ChunkDataRegistryImpl { } chunkNBTManagers.remove(manager); sectionNBTManagers.remove(manager); + NBTManagers.remove(manager); } //Add the manager to the list of disabled managers, in case it gets registered after this disable call. @@ -230,4 +241,166 @@ public class ChunkDataRegistryImpl { public static Set getRegisteredManagers() { return Collections.unmodifiableSet(managers); } + + public static void readLevelDat(NBTTagCompound tag) { + boolean triggerScreen = false; + boolean newlyInstalled = false; + val builder = new StringBuilder(); + if (!tag.hasKey("version")) { + newlyInstalled = true; + builder.append("The world you are trying to load is a vanilla world, or a world that was created with a very old version of ChunkAPI.\n"); + } // Compat code will be added here if needed. + val managerCompat = verifyManagerCompatibility(tag); + if (managerCompat != null) { + //Avoid duplicate messages + if (!newlyInstalled) { + builder.append("The world you are trying to load was created with a different version of ChunkAPI data managers.\n" + + "Data managers are used by other mods to save extra data in chunks, and changes to these data managers can cause worlds to become corrupted.\n"); + } + triggerScreen = true; + } + if (triggerScreen) + builder.append("ChunkAPI will attempt to load the world, but it is possible that chunks might get corrupted.\n" + + "Read the following text carefully, and make sure you understand the risks before continuing.\n\n"); + if (managerCompat != null) + builder.append(managerCompat); + + if (triggerScreen) { + builder.append("\nA world backup will be automatically created in your saves directory.\n\n"); + boolean confirmed = StartupQuery.confirm(builder.toString()); + if (!confirmed) StartupQuery.abort(); + + try { + ZipperUtil.backupWorld(); + } catch (IOException e) { + StartupQuery.notify("The world backup couldn't be created.\n\n"+e); + StartupQuery.abort(); + } + } + } + + private static String verifyManagerCompatibility(NBTTagCompound tag) { + boolean compatWarning = false; + StringBuilder builder = new StringBuilder(); + val saveManagers = readManagers(tag); + val removedManagers = new HashSet<>(saveManagers.keySet()); + removedManagers.removeAll(managers); + if (!removedManagers.isEmpty()) { + compatWarning = true; + builder.append("\nThe following data managers are no longer present:\n"); + for (val manager : removedManagers) { + val saveManager = saveManagers.get(manager); + builder.append(manager); + if (saveManager.version != null) + builder.append(' ').append(saveManager.version); + builder.append("\nUninstall information: "); + if (saveManager.uninstallMessage != null) + builder.append(saveManager.getUninstallMessage()); + else + builder.append("No uninstall information available."); + builder.append('\n'); + } + } + val addedManagers = managers.stream() + .filter(manager -> !manager.startsWith("minecraft:")) + .filter(manager -> !saveManagers.containsKey(manager)) + .collect(Collectors.toSet()); + if (!addedManagers.isEmpty()) { + compatWarning = true; + builder.append("\nThe following data managers have been newly added:\n"); + for (val manager: addedManagers) { + val addedManager = NBTManagers.get(manager); + builder.append(manager) + .append(' ').append(addedManager.version()).append('\n'); + val installMessage = addedManager.newInstallDescription(); + if (installMessage != null) + builder.append("Install information: ").append(addedManager.newInstallDescription()).append('\n'); + } + } + boolean changePrompt = false; + for (val savedManager: saveManagers.entrySet()) { + val managerName = savedManager.getKey(); + if (managers.contains(managerName)) { + val manager = savedManager.getValue(); + val currentManager = NBTManagers.get(managerName); + val extraMessage = currentManager.versionChangeMessage(manager.version); + if (extraMessage != null) { + compatWarning = true; + if (!changePrompt) { + builder.append("\nThe following data managers have changed versions in an incompatible way:\n"); + changePrompt = true; + } + builder.append(managerName) + .append(" has changed versions from ") + .append(manager.version) + .append(" to ") + .append(currentManager.version()) + .append(".\n"); + builder.append("Upgrade information: ").append(extraMessage).append('\n'); + } + } + } + if (!compatWarning) + return null; + return "The following managers have changed:\n" + + builder; + } + + public static void writeLevelDat(NBTTagCompound tag) { + val managers = new NBTTagCompound(); + tag.setTag("managers", managers); + tag.setString("version", Tags.VERSION); + for (val manager: NBTManagers.entrySet()) { + val name = manager.getKey(); + if (name.startsWith("minecraft:")) + continue; + val value = manager.getValue(); + managers.setTag(name, SaveManagerInfo.fromManager(value).toNBT()); + } + } + + private static Map readManagers(NBTTagCompound tag) { + val managers = new HashMap(); + if (!tag.hasKey("managers")) return managers; + if (tag.func_150299_b("managers") != Constants.NBT.TAG_COMPOUND ) return managers; + val managerTag = tag.getCompoundTag("managers"); + //noinspection unchecked + for (val key : (Set)managerTag.func_150296_c()) { + managers.put(key, SaveManagerInfo.fromNBT(managerTag.getCompoundTag(key))); + } + return managers; + } + + @Data + private static class SaveManagerInfo { + @Nullable + public final String version; + @Nullable + public final String uninstallMessage; + + public NBTTagCompound toNBT() { + val tag = new NBTTagCompound(); + if (version != null) + tag.setString("version", version); + if (uninstallMessage != null) + tag.setString("uninstallMessage", uninstallMessage); + return tag; + } + + public static SaveManagerInfo fromNBT(NBTTagCompound tag) { + return new SaveManagerInfo(getNBTStringNullable(tag, "version"), getNBTStringNullable(tag, "uninstallMessage")); + } + + private static String getNBTStringNullable(NBTTagCompound tag, String key) { + if (tag.hasKey(key) && tag.func_150299_b(key) == Constants.NBT.TAG_STRING) { + return tag.getString(key); + } else { + return null; + } + } + + public static SaveManagerInfo fromManager(ChunkDataManager.NBTDataManager manager) { + return new SaveManagerInfo(manager.version(), manager.uninstallMessage()); + } + } } diff --git a/src/main/java/com/falsepattern/chunk/internal/core/ChunkAPICoreModContainer.java b/src/main/java/com/falsepattern/chunk/internal/core/ChunkAPICoreModContainer.java new file mode 100644 index 0000000..bc94119 --- /dev/null +++ b/src/main/java/com/falsepattern/chunk/internal/core/ChunkAPICoreModContainer.java @@ -0,0 +1,56 @@ +package com.falsepattern.chunk.internal.core; + +import com.falsepattern.chunk.internal.ChunkDataRegistryImpl; +import com.falsepattern.chunk.internal.Tags; +import com.google.common.eventbus.EventBus; +import lombok.Data; +import lombok.val; + +import net.minecraft.nbt.NBTBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.world.storage.SaveHandler; +import net.minecraft.world.storage.WorldInfo; +import net.minecraftforge.common.util.Constants; +import cpw.mods.fml.common.DummyModContainer; +import cpw.mods.fml.common.LoadController; +import cpw.mods.fml.common.ModMetadata; +import cpw.mods.fml.common.StartupQuery; +import cpw.mods.fml.common.WorldAccessContainer; +import cpw.mods.fml.common.versioning.DefaultArtifactVersion; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class ChunkAPICoreModContainer extends DummyModContainer implements WorldAccessContainer { + public ChunkAPICoreModContainer() { + super(createMetadata()); + } + + private static ModMetadata createMetadata() { + val meta = new ModMetadata(); + meta.modId = Tags.MODID + "core"; + meta.name = Tags.MODNAME + " Core"; + meta.version = Tags.VERSION; + meta.dependencies.add(new DefaultArtifactVersion(Tags.MODID, Tags.VERSION)); + meta.parent = Tags.MODID; + return meta; + } + + @Override + public boolean registerBus(EventBus bus, LoadController controller) { + return true; + } + + @Override + public NBTTagCompound getDataForWriting(SaveHandler handler, WorldInfo info) { + val tag = new NBTTagCompound(); + ChunkDataRegistryImpl.writeLevelDat(tag); + return tag; + } + + @Override + public void readData(SaveHandler handler, WorldInfo info, Map propertyMap, NBTTagCompound tag) { + ChunkDataRegistryImpl.readLevelDat(tag); + } +} diff --git a/src/main/java/com/falsepattern/chunk/internal/core/CoreLoadingPlugin.java b/src/main/java/com/falsepattern/chunk/internal/core/CoreLoadingPlugin.java new file mode 100644 index 0000000..1a9da25 --- /dev/null +++ b/src/main/java/com/falsepattern/chunk/internal/core/CoreLoadingPlugin.java @@ -0,0 +1,35 @@ +package com.falsepattern.chunk.internal.core; + +import com.falsepattern.chunk.internal.Tags; + +import cpw.mods.fml.relauncher.IFMLLoadingPlugin; + +import java.util.Map; + +@IFMLLoadingPlugin.TransformerExclusions(Tags.GROUPNAME + ".internal.core") +public class CoreLoadingPlugin implements IFMLLoadingPlugin { + @Override + public String[] getASMTransformerClass() { + return new String[0]; + } + + @Override + public String getModContainerClass() { + return Tags.GROUPNAME + ".internal.core.ChunkAPICoreModContainer"; + } + + @Override + public String getSetupClass() { + return null; + } + + @Override + public void injectData(Map data) { + + } + + @Override + public String getAccessTransformerClass() { + return null; + } +} diff --git a/src/main/java/com/falsepattern/chunk/internal/vanilla/VanillaManager.java b/src/main/java/com/falsepattern/chunk/internal/vanilla/VanillaManager.java index 38a9f17..e4f384f 100644 --- a/src/main/java/com/falsepattern/chunk/internal/vanilla/VanillaManager.java +++ b/src/main/java/com/falsepattern/chunk/internal/vanilla/VanillaManager.java @@ -8,10 +8,28 @@ package com.falsepattern.chunk.internal.vanilla; import com.falsepattern.chunk.api.ChunkDataManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public abstract class VanillaManager implements ChunkDataManager { @Override public String domain() { return "minecraft"; } + + public @NotNull String version() { + return ""; + } + + public @Nullable String newInstallDescription() { + return null; + } + + public @NotNull String uninstallMessage() { + return ""; + } + + public @Nullable String versionChangeMessage(String priorVersion) { + return null; + } }