diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java index fdbf3d4..2b667eb 100644 --- a/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java +++ b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java @@ -23,6 +23,7 @@ import lol.pyr.znpcsplus.commands.property.PropertyRemoveCommand; import lol.pyr.znpcsplus.commands.property.PropertySetCommand; import lol.pyr.znpcsplus.commands.storage.ImportCommand; import lol.pyr.znpcsplus.commands.storage.LoadAllCommand; +import lol.pyr.znpcsplus.commands.storage.MigrateCommand; import lol.pyr.znpcsplus.commands.storage.SaveAllCommand; import lol.pyr.znpcsplus.config.ConfigManager; import lol.pyr.znpcsplus.conversion.DataImporterRegistry; @@ -39,6 +40,7 @@ import lol.pyr.znpcsplus.scheduling.SpigotScheduler; import lol.pyr.znpcsplus.scheduling.TaskScheduler; import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; import lol.pyr.znpcsplus.skin.cache.SkinCacheCleanTask; +import lol.pyr.znpcsplus.storage.NpcStorageType; import lol.pyr.znpcsplus.tasks.HologramRefreshTask; import lol.pyr.znpcsplus.tasks.NpcProcessorTask; import lol.pyr.znpcsplus.tasks.ViewableHideOnLeaveListener; @@ -157,7 +159,7 @@ public class ZNpcsPlus { pluginManager.registerEvents(new UserListener(userManager), bootstrap); registerCommands(npcRegistry, skinCache, adventure, actionRegistry, - typeRegistry, propertyRegistry, importerRegistry, configManager); + typeRegistry, propertyRegistry, importerRegistry, configManager, packetFactory); log(ChatColor.WHITE + " * Starting tasks..."); if (configManager.getConfig().checkForUpdates()) { @@ -243,7 +245,7 @@ public class ZNpcsPlus { private void registerCommands(NpcRegistryImpl npcRegistry, MojangSkinCache skinCache, BukkitAudiences adventure, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, DataImporterRegistry importerRegistry, - ConfigManager configManager) { + ConfigManager configManager, PacketFactory packetFactory) { Message incorrectUsageMessage = context -> context.send(Component.text("Incorrect usage: /" + context.getUsage(), NamedTextColor.RED)); CommandManager manager = new CommandManager(bootstrap, adventure, incorrectUsageMessage); @@ -292,6 +294,7 @@ public class ZNpcsPlus { registerEnumParser(manager, Sound.class, incorrectUsageMessage); registerEnumParser(manager, ArmadilloState.class, incorrectUsageMessage); registerEnumParser(manager, WoldVariant.class, incorrectUsageMessage); + registerEnumParser(manager, NpcStorageType.class, incorrectUsageMessage); manager.registerCommand("npc", new MultiCommand(bootstrap.loadHelpMessage("root")) .addSubcommand("center", new CenterCommand(npcRegistry)) @@ -316,7 +319,8 @@ public class ZNpcsPlus { .addSubcommand("storage", new MultiCommand(bootstrap.loadHelpMessage("storage")) .addSubcommand("save", new SaveAllCommand(npcRegistry)) .addSubcommand("reload", new LoadAllCommand(npcRegistry)) - .addSubcommand("import", new ImportCommand(npcRegistry, importerRegistry))) + .addSubcommand("import", new ImportCommand(npcRegistry, importerRegistry)) + .addSubcommand("migrate", new MigrateCommand(configManager, this, packetFactory, actionRegistry, typeRegistry, propertyRegistry, textSerializer, npcRegistry.getStorage(), configManager.getConfig().storageType(), npcRegistry))) .addSubcommand("holo", new MultiCommand(bootstrap.loadHelpMessage("holo")) .addSubcommand("add", new HoloAddCommand(npcRegistry)) .addSubcommand("additem", new HoloAddItemCommand(npcRegistry)) diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/MigrateCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/MigrateCommand.java new file mode 100644 index 0000000..cda3b3c --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/MigrateCommand.java @@ -0,0 +1,176 @@ +package lol.pyr.znpcsplus.commands.storage; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.ZNpcsPlus; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.storage.NpcStorage; +import lol.pyr.znpcsplus.storage.NpcStorageType; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +public class MigrateCommand implements CommandHandler { + private final ConfigManager configManager; + private final ZNpcsPlus plugin; + private final PacketFactory packetFactory; + private final ActionRegistryImpl actionRegistry; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final LegacyComponentSerializer textSerializer; + private final NpcStorage currentStorage; + private final NpcStorageType currentStorageType; + private final NpcRegistryImpl npcRegistry; + + public MigrateCommand(ConfigManager configManager, ZNpcsPlus plugin, PacketFactory packetFactory, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, NpcStorage currentStorage, NpcStorageType currentStorageType, NpcRegistryImpl npcRegistry) { + this.configManager = configManager; + this.plugin = plugin; + this.packetFactory = packetFactory; + this.actionRegistry = actionRegistry; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.textSerializer = textSerializer; + this.currentStorage = currentStorage; + this.currentStorageType = currentStorageType; + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " storage migrate [force]"); + NpcStorageType from = context.parse(NpcStorageType.class); + NpcStorageType to = context.parse(NpcStorageType.class); + boolean force = context.argSize() > 2 && context.parse(Boolean.class); + if (from.equals(to)) { + context.halt(Component.text("The storage types must be different.", NamedTextColor.RED)); + return; + } + NpcStorage fromStorage; + if (currentStorageType == from) { + fromStorage = currentStorage; + } else { + fromStorage = from.create(configManager, plugin, packetFactory, actionRegistry, typeRegistry, propertyRegistry, textSerializer); + if (fromStorage == null) { + context.halt(Component.text("Failed to initialize the source storage. Please check the console for more information.", NamedTextColor.RED)); + return; + } + } + Collection entries; + try { + entries = fromStorage.loadNpcs(); + } catch (Exception e) { + context.halt(Component.text("Failed to load NPCs from the source storage.", NamedTextColor.RED)); + return; + } + if (entries.isEmpty()) { + context.send(Component.text("No NPCs to migrate.", NamedTextColor.YELLOW)); + return; + } + NpcStorage toStorage; + if (currentStorageType == to) { + toStorage = currentStorage; + } else { + toStorage = to.create(configManager, plugin, packetFactory, actionRegistry, typeRegistry, propertyRegistry, textSerializer); + if (toStorage == null) { + context.halt(Component.text("Failed to initialize the destination storage. Please check the console for more information.", NamedTextColor.RED)); + return; + } + } + + Collection existingEntries; + try { + existingEntries = toStorage.loadNpcs(); + } catch (Exception e) { + context.halt(Component.text("Failed to load NPCs from the destination storage.", NamedTextColor.RED)); + return; + } + if (existingEntries.isEmpty()) { + toStorage.saveNpcs(entries); + context.send(Component.text("Migrated " + entries.size() + " NPCs from the source storage (", NamedTextColor.GREEN) + .append(Component.text(from.name(), NamedTextColor.GOLD)) + .append(Component.text(") to the destination storage (", NamedTextColor.GREEN)) + .append(Component.text(to.name(), NamedTextColor.GOLD)) + .append(Component.text(").", NamedTextColor.GREEN))); + if (currentStorageType == to) { + npcRegistry.reload(); + } else { + toStorage.close(); + } + return; + } + if (!force) { + Collection toSave = entries.stream().filter(e -> existingEntries.stream().noneMatch(e2 -> e2.getId().equals(e.getId()))).collect(Collectors.toList()); + Collection idExists = entries.stream().filter(e -> existingEntries.stream().anyMatch(e2 -> e2.getId().equals(e.getId()))).collect(Collectors.toList()); + if (toSave.isEmpty()) { + context.send(Component.text("No NPCs to migrate.", NamedTextColor.YELLOW)); + if (currentStorageType != to) { + toStorage.close(); + } + } else { + toStorage.saveNpcs(toSave); + context.send(Component.text("Migrated " + toSave.size() + " NPCs from the source storage (", NamedTextColor.GREEN) + .append(Component.text(from.name(), NamedTextColor.GOLD)) + .append(Component.text(") to the destination storage (", NamedTextColor.GREEN)) + .append(Component.text(to.name(), NamedTextColor.GOLD)) + .append(Component.text(").", NamedTextColor.GREEN))); + if (currentStorageType == to) { + npcRegistry.reload(); + } else { + toStorage.close(); + } + } + if (!idExists.isEmpty()) { + AtomicReference component = new AtomicReference<>(Component.text("The following NPCs were not migrated because their IDs already exist in the destination storage:").color(NamedTextColor.YELLOW)); + idExists.forEach(e -> { + component.set(component.get().append(Component.newline()).append(Component.text(e.getId(), NamedTextColor.RED))); + }); + component.set(component.get().append(Component.newline()) + .append(Component.text("Use the ", NamedTextColor.YELLOW)) + .append(Component.text("force", NamedTextColor.GOLD)) + .append(Component.text(" argument to overwrite them.", NamedTextColor.YELLOW))); + context.send(component.get()); + } + } else { + toStorage.saveNpcs(entries); + context.send(Component.text("Force migrated " + entries.size() + " NPCs from the source storage (", NamedTextColor.GREEN) + .append(Component.text(from.name(), NamedTextColor.GOLD)) + .append(Component.text(") to the destination storage (", NamedTextColor.GREEN)) + .append(Component.text(to.name(), NamedTextColor.GOLD)) + .append(Component.text(").", NamedTextColor.GREEN))); + if (currentStorageType == to) { + npcRegistry.reload(); + } else { + toStorage.close(); + } + } + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) { + return context.suggestEnum(NpcStorageType.values()); + } else if (context.argSize() == 2) { + NpcStorageType from = context.suggestionParse(0, NpcStorageType.class); + if (from == null) return Collections.emptyList(); + return context.suggestCollection(Arrays.stream(NpcStorageType.values()) + .filter(t -> t != from).map(Enum::name).collect(Collectors.toList())); + } else if (context.argSize() == 3) { + return context.suggestLiteral("true"); + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcRegistryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcRegistryImpl.java index da716b0..e95cecd 100644 --- a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcRegistryImpl.java +++ b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcRegistryImpl.java @@ -209,4 +209,8 @@ public class NpcRegistryImpl implements NpcRegistry { npcList.forEach(npcEntry -> npcEntry.getNpc().delete()); storage.close(); } + + public NpcStorage getStorage() { + return storage; + } } diff --git a/plugin/src/main/resources/messages/storage-hover/migrate.txt b/plugin/src/main/resources/messages/storage-hover/migrate.txt new file mode 100644 index 0000000..371148c --- /dev/null +++ b/plugin/src/main/resources/messages/storage-hover/migrate.txt @@ -0,0 +1,16 @@ +Usage ยป /npc storage migrate [force] + +Storage Types: + * YAML - Npcs are stored in yaml files + * SQLite - Npcs are stored in a SQLite database + * MySQL - Npcs are stored in a MySQL database + +Command used to migrate npcs from one storage type to another. + +This command will NOT delete the original storage files or database, +but will copy the npcs to the new storage type. + +This will also not overwrite any existing npcs in the new storage +type, unless the force argument is set to true. +Warning: force will overwrite any existing npcs with the same id +in the new storage type and CANNOT be undone. \ No newline at end of file diff --git a/plugin/src/main/resources/messages/storage.txt b/plugin/src/main/resources/messages/storage.txt index b5721b3..6cfcc3f 100644 --- a/plugin/src/main/resources/messages/storage.txt +++ b/plugin/src/main/resources/messages/storage.txt @@ -5,4 +5,5 @@ * /npc storage save * /npc storage reload * /npc storage import + * /npc storage migrate [force]