Merge remote-tracking branch 'origin/2.X' into 2.X

This commit is contained in:
Pyrbu 2024-08-19 17:45:49 +02:00
commit 71f52a987b
14 changed files with 268 additions and 14 deletions

View file

@ -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;
@ -127,7 +129,7 @@ public class ZNpcsPlus {
PacketFactory packetFactory = setupPacketFactory(scheduler, propertyRegistry, configManager);
propertyRegistry.registerTypes(bootstrap, packetFactory, textSerializer);
propertyRegistry.registerTypes(bootstrap, packetFactory, textSerializer, scheduler);
BungeeConnector bungeeConnector = new BungeeConnector(bootstrap);
ActionRegistryImpl actionRegistry = new ActionRegistryImpl();
@ -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<CommandContext> 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))

View file

@ -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 <from> <to> [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<NpcEntryImpl> 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<NpcEntryImpl> 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<NpcEntryImpl> toSave = entries.stream().filter(e -> existingEntries.stream().noneMatch(e2 -> e2.getId().equals(e.getId()))).collect(Collectors.toList());
Collection<NpcEntryImpl> 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> 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<String> 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();
}
}

View file

@ -30,9 +30,14 @@ public interface DatabaseConfig {
@DefaultString("znpcsplus")
String databaseName();
@ConfKey("use-ssl")
@ConfComments("Should SSL be used when connecting to the database?")
@DefaultBoolean(false)
boolean useSSL();
default String createConnectionURL(String dbType) {
if (dbType.equalsIgnoreCase("mysql")) {
return "jdbc:mysql://" + host() + ":" + port() + "/" + databaseName() + "?useSSL=false&user=" + username() + "&password=" + password();
return "jdbc:mysql://" + host() + ":" + port() + "/" + databaseName() + "?useSSL=" + useSSL();
} else {
throw new IllegalArgumentException("Unsupported database type: " + dbType);
}

View file

@ -20,6 +20,7 @@ import lol.pyr.znpcsplus.entity.properties.villager.VillagerProfessionProperty;
import lol.pyr.znpcsplus.entity.properties.villager.VillagerTypeProperty;
import lol.pyr.znpcsplus.entity.serializers.*;
import lol.pyr.znpcsplus.packets.PacketFactory;
import lol.pyr.znpcsplus.scheduling.TaskScheduler;
import lol.pyr.znpcsplus.skin.cache.MojangSkinCache;
import lol.pyr.znpcsplus.util.*;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
@ -104,7 +105,7 @@ public class EntityPropertyRegistryImpl implements EntityPropertyRegistry {
*/
}
public void registerTypes(ZNpcsPlusBootstrap plugin, PacketFactory packetFactory, LegacyComponentSerializer textSerializer) {
public void registerTypes(ZNpcsPlusBootstrap plugin, PacketFactory packetFactory, LegacyComponentSerializer textSerializer, TaskScheduler taskScheduler) {
ServerVersion ver = PacketEvents.getAPI().getServerManager().getVersion();
boolean legacyBooleans = ver.isOlderThan(ServerVersion.V_1_9);
boolean legacyNames = ver.isOlderThan(ServerVersion.V_1_9);
@ -127,7 +128,7 @@ public class EntityPropertyRegistryImpl implements EntityPropertyRegistry {
register(new DummyProperty<>("permission_required", false));
register(new ForceBodyRotationProperty(plugin));
register(new ForceBodyRotationProperty(plugin, taskScheduler));
register(new DummyProperty<>("player_knockback", false));
register(new DummyProperty<>("player_knockback_exempt_permission", String.class));

View file

@ -3,24 +3,26 @@ package lol.pyr.znpcsplus.entity.properties;
import com.github.retrooper.packetevents.protocol.entity.data.EntityData;
import lol.pyr.znpcsplus.ZNpcsPlusBootstrap;
import lol.pyr.znpcsplus.entity.PacketEntity;
import org.bukkit.Bukkit;
import lol.pyr.znpcsplus.scheduling.TaskScheduler;
import org.bukkit.entity.Player;
import java.util.Map;
public class ForceBodyRotationProperty extends DummyProperty<Boolean> {
private final ZNpcsPlusBootstrap plugin;
private final TaskScheduler scheduler;
public ForceBodyRotationProperty(ZNpcsPlusBootstrap plugin) {
public ForceBodyRotationProperty(ZNpcsPlusBootstrap plugin, TaskScheduler scheduler) {
super("force_body_rotation", false);
this.plugin = plugin;
this.scheduler = scheduler;
}
@Override
public void apply(Player player, PacketEntity entity, boolean isSpawned, Map<Integer, EntityData> properties) {
if (entity.getProperty(this)) {
Bukkit.getScheduler().runTaskLater(plugin, () -> entity.swingHand(player, false), 2L);
Bukkit.getScheduler().runTaskLater(plugin, () -> entity.swingHand(player, false), 6L);
scheduler.runLaterAsync(() -> entity.swingHand(player, false), 2L);
scheduler.runLaterAsync(() -> entity.swingHand(player, false), 6L);
}
}
}

View file

@ -207,5 +207,10 @@ public class NpcRegistryImpl implements NpcRegistry {
public void unload() {
npcList.forEach(npcEntry -> npcEntry.getNpc().delete());
storage.close();
}
public NpcStorage getStorage() {
return storage;
}
}

View file

@ -8,4 +8,7 @@ public interface NpcStorage {
Collection<NpcEntryImpl> loadNpcs();
void saveNpcs(Collection<NpcEntryImpl> npcs);
void deleteNpc(NpcEntryImpl npc);
default void close() {
}
}

View file

@ -13,4 +13,6 @@ public abstract class Database {
public abstract Connection getSQLConnection();
public abstract void load();
public abstract void close();
}

View file

@ -10,10 +10,14 @@ import java.util.logging.Logger;
public class MySQL extends Database {
private final String connectionURL;
private final String username;
private final String password;
public MySQL(String connectionURL, Logger logger) {
public MySQL(String connectionURL, String username, String password, Logger logger) {
super(logger);
this.connectionURL = connectionURL;
this.username = username;
this.password = password;
}
@Override
@ -25,7 +29,7 @@ public class MySQL extends Database {
return connection;
}
Class.forName("com.mysql.jdbc.Driver");
connection = java.sql.DriverManager.getConnection(connectionURL);
connection = java.sql.DriverManager.getConnection(connectionURL, username, password);
return connection;
} catch (ClassNotFoundException ex) {
logger.severe("MySQL JDBC library not found" + ex);
@ -56,6 +60,18 @@ public class MySQL extends Database {
connection = getSQLConnection();
}
@Override
public void close() {
try {
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
logger.severe("An error occurred while closing the connection");
e.printStackTrace();
}
}
public boolean tableExists(String tableName) {
try {
Statement s = connection.createStatement();

View file

@ -46,7 +46,8 @@ public class MySQLStorage implements NpcStorage {
this.typeRegistry = typeRegistry;
this.propertyRegistry = propertyRegistry;
this.textSerializer = textSerializer;
this.database = new MySQL(configManager.getConfig().databaseConfig().createConnectionURL("mysql"), logger);
this.database = new MySQL(configManager.getConfig().databaseConfig().createConnectionURL("mysql"),
configManager.getConfig().databaseConfig().username(), configManager.getConfig().databaseConfig().password(), logger);
database.load();
if (database.getSQLConnection() == null) {
throw new RuntimeException("Failed to initialize MySQL Storage");
@ -313,4 +314,9 @@ public class MySQLStorage implements NpcStorage {
exception.printStackTrace();
}
}
@Override
public void close() {
database.close();
}
}

View file

@ -41,6 +41,18 @@ public class SQLite extends Database{
connection = getSQLConnection();
}
@Override
public void close() {
try {
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
logger.severe("An error occurred while closing the connection");
e.printStackTrace();
}
}
public boolean tableExists(String tableName) {
try {
Statement s = connection.createStatement();

View file

@ -312,4 +312,9 @@ public class SQLiteStorage implements NpcStorage {
exception.printStackTrace();
}
}
@Override
public void close() {
database.close();
}
}

View file

@ -0,0 +1,16 @@
<gray>Usage <gold>» <yellow>/npc storage migrate <gold><from> <to> [force]
<gray>Storage Types:
<gold>* <yellow>YAML <gray>- Npcs are stored in yaml files
<gold>* <yellow>SQLite <gray>- Npcs are stored in a SQLite database
<gold>* <yellow>MySQL <gray>- Npcs are stored in a MySQL database
<gray>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.
<gray>This will also not overwrite any existing npcs in the new storage
type, unless the <gold>force <gray>argument is set to <gold>true<gray>.
<red>Warning: <bold>force</bold> will overwrite any existing npcs with the same id
in the new storage type and CANNOT be undone.

View file

@ -5,4 +5,5 @@
<hover:show_text:'{@storage-hover/save}'><gold>* <yellow>/npc storage save</hover>
<hover:show_text:'{@storage-hover/reload}'><gold>* <yellow>/npc storage reload</hover>
<hover:show_text:'{@storage-hover/import}'><gold>* <yellow>/npc storage import <importer></hover>
<hover:show_text:'{@storage-hover/migrate}'><gold>* <yellow>/npc storage migrate <from> <to> [force]</hover>