diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/Vector3i.java b/api/src/main/java/lol/pyr/znpcsplus/util/Vector3i.java new file mode 100644 index 0000000..ae6a933 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/Vector3i.java @@ -0,0 +1,46 @@ +package lol.pyr.znpcsplus.util; + +public class Vector3i { + private final int x; + private final int y; + private final int z; + + public Vector3i(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + public int getX() { + return this.x; + } + + public int getY() { + return this.y; + } + + public int getZ() { + return this.z; + } + + public String toString() { + return this.x + "," + this.y + "," + this.z; + } + + public String toPrettyString() { + return "(" + this.x + ", " + this.y + ", " + this.z + ")"; + } + + public static Vector3i fromString(String s) { + String[] split = s.split(","); + if (split.length < 3) { + return null; + } else { + try { + return new Vector3i(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2])); + } catch (NumberFormatException var3) { + return null; + } + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java index e076bc5..fe06753 100644 --- a/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java +++ b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java @@ -251,6 +251,7 @@ public class ZNpcsPlus extends JavaPlugin { manager.registerParser(Color.class, new ColorParser(incorrectUsageMessage)); manager.registerParser(Vector3f.class, new Vector3fParser(incorrectUsageMessage)); manager.registerParser(String.class, new StringParser(incorrectUsageMessage)); + manager.registerParser(Vector3i.class, new Vector3iParser(incorrectUsageMessage)); // TODO: Need to find a better way to do this registerEnumParser(manager, NpcPose.class, incorrectUsageMessage); diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertySetCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertySetCommand.java index 0380025..d5ff9dd 100644 --- a/plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertySetCommand.java +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertySetCommand.java @@ -19,6 +19,9 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Color; import com.github.retrooper.packetevents.protocol.item.ItemStack; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; import java.util.Arrays; import java.util.Collections; @@ -117,9 +120,20 @@ public class PropertySetCommand implements CommandHandler { value = context.parse(type); valueName = value == null ? "NONE" : ((NpcEntryImpl) value).getId(); } - else { + else if (type == Vector3i.class) { value = context.parse(type); - valueName = String.valueOf(value); + valueName = value == null ? "NONE" : ((Vector3i) value).toPrettyString(); + } + else { + try { + value = context.parse(type); + valueName = String.valueOf(value); + } catch (NullPointerException e) { + context.send(Component.text("An error occurred while trying to parse the value. Please report this to the plugin author.", + NamedTextColor.RED)); + e.printStackTrace(); + return; + } } npc.UNSAFE_setProperty(property, value); @@ -144,6 +158,17 @@ public class PropertySetCommand implements CommandHandler { context.suggestEnum(Arrays.stream(SpellType.values()).filter(spellType -> spellType.ordinal() <= 3).toArray(SpellType[]::new)) : context.suggestEnum(SpellType.values()); + if (type == Vector3i.class) { + if (context.getSender() instanceof Player) { + Player player = (Player) context.getSender(); + Block targetBlock = player.getTargetBlock(Collections.singleton(Material.AIR), 5); + if (targetBlock.getType().equals(Material.AIR)) return Collections.emptyList(); + return context.suggestLiteral( + targetBlock.getX() + "", + targetBlock.getX() + " " + targetBlock.getY(), + targetBlock.getX() + " " + targetBlock.getY() + " " + targetBlock.getZ()); + } + } // Suggest enum values directly if (type.isEnum()) { return context.suggestEnum((Enum[]) type.getEnumConstants()); @@ -154,6 +179,25 @@ public class PropertySetCommand implements CommandHandler { // TODO: suggest block with nbt like minecraft setblock command return context.suggestionParse(2, String.class).equals("block") ? context.suggestStream(StateTypes.values().stream().map(StateType::getName)) : Collections.emptyList(); } + if (type == Vector3i.class) { + if (context.getSender() instanceof Player) { + Player player = (Player) context.getSender(); + Block targetBlock = player.getTargetBlock(Collections.singleton(Material.AIR), 5); + if (targetBlock.getType().equals(Material.AIR)) return Collections.emptyList(); + return context.suggestLiteral( + targetBlock.getY() + "", + targetBlock.getY() + " " + targetBlock.getZ()); + } + } + } else if (context.argSize() == 5) { + if (type == Vector3i.class) { + if (context.getSender() instanceof Player) { + Player player = (Player) context.getSender(); + Block targetBlock = player.getTargetBlock(Collections.singleton(Material.AIR), 5); + if (targetBlock.getType().equals(Material.AIR)) return Collections.emptyList(); + return context.suggestLiteral(targetBlock.getZ() + ""); + } + } } } return Collections.emptyList(); diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/config/MainConfig.java b/plugin/src/main/java/lol/pyr/znpcsplus/config/MainConfig.java index 5699da0..1b19a22 100644 --- a/plugin/src/main/java/lol/pyr/znpcsplus/config/MainConfig.java +++ b/plugin/src/main/java/lol/pyr/znpcsplus/config/MainConfig.java @@ -28,7 +28,7 @@ public interface MainConfig { boolean debugEnabled(); @ConfKey("storage-type") - @ConfComments("The storage type to use. Available storage types: YAML") + @ConfComments("The storage type to use. Available storage types: YAML, SQLITE") @DefaultString("YAML") NpcStorageType storageType(); diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyRegistryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyRegistryImpl.java index 08ede74..15fe88f 100644 --- a/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyRegistryImpl.java +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyRegistryImpl.java @@ -56,6 +56,7 @@ public class EntityPropertyRegistryImpl implements EntityPropertyRegistry { registerSerializer(new Vector3fPropertySerializer()); registerSerializer(new BlockStatePropertySerializer()); registerSerializer(new LookTypeSerializer()); + registerSerializer(new GenericSerializer<>(Vector3i::toString, Vector3i::fromString, Vector3i.class)); registerEnumSerializer(NpcPose.class); registerEnumSerializer(DyeColor.class); @@ -119,6 +120,7 @@ public class EntityPropertyRegistryImpl implements EntityPropertyRegistry { register(new EquipmentProperty(packetFactory, "offhand", EquipmentSlot.OFF_HAND)); register(new NameProperty(legacyNames, optionalComponents)); + register(new DummyProperty<>("display_name", String.class)); register(new DinnerboneProperty(legacyNames, optionalComponents)); register(new DummyProperty<>("look", LookType.FIXED)); @@ -251,6 +253,17 @@ public class EntityPropertyRegistryImpl implements EntityPropertyRegistry { register(new BitsetProperty("is_rearing", horseIndex, horseEating << 1, false, legacyBooleans)); register(new BitsetProperty("has_mouth_open", horseIndex, horseEating << 2, false, legacyBooleans)); + // End Crystal + if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) { + int endCrystalIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) endCrystalIndex = 8; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) endCrystalIndex = 7; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) endCrystalIndex = 6; + else endCrystalIndex = 5; + register(new OptionalBlockPosProperty("beam_target", null, endCrystalIndex++)); + register(new BooleanProperty("show_base", endCrystalIndex, true, false)); + } + // Horse if (ver.isNewerThanOrEquals(ServerVersion.V_1_8) && ver.isOlderThan(ServerVersion.V_1_9)) { register(new EncodedByteProperty<>("horse_type", HorseType.HORSE, 19, obj -> (byte) obj.ordinal())); @@ -367,7 +380,7 @@ public class EntityPropertyRegistryImpl implements EntityPropertyRegistry { else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) witherIndex = 11; else witherIndex = 17; witherIndex += 3; // skip the first 3 indexes, will be used for the other properties later - register(new IntegerProperty("invulnerable_time", witherIndex++, 0, false)); + register(new IntegerProperty("invulnerable_time", witherIndex, 0, false)); if (!ver.isNewerThanOrEquals(ServerVersion.V_1_9)) return; // Shulker diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/OptionalBlockPosProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/OptionalBlockPosProperty.java new file mode 100644 index 0000000..c9eb536 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/OptionalBlockPosProperty.java @@ -0,0 +1,28 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.Vector3i; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.Optional; + +public class OptionalBlockPosProperty extends EntityPropertyImpl { + private final int index; + + public OptionalBlockPosProperty(String name, Vector3i defaultValue, int index) { + super(name, defaultValue, Vector3i.class); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map properties) { + Vector3i value = entity.getProperty(this); + if (value == null) properties.put(index, new EntityData(index, EntityDataTypes.OPTIONAL_BLOCK_POSITION, Optional.empty())); + else properties.put(index, new EntityData(index, EntityDataTypes.OPTIONAL_BLOCK_POSITION, + Optional.of(new com.github.retrooper.packetevents.util.Vector3i(value.getX(), value.getY(), value.getZ())))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/GenericSerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/GenericSerializer.java new file mode 100644 index 0000000..3ad3a5d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/GenericSerializer.java @@ -0,0 +1,32 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; + +import java.util.function.Function; + +public class GenericSerializer implements PropertySerializer { + private final Function encoder; + private final Function decoder; + private final Class typeClass; + + public GenericSerializer(Function encoder, Function decoder, Class typeClass) { + this.encoder = encoder; + this.decoder = decoder; + this.typeClass = typeClass; + } + + @Override + public String serialize(T property) { + return encoder.apply(property); + } + + @Override + public T deserialize(String property) { + return decoder.apply(property); + } + + @Override + public Class getTypeClass() { + return typeClass; + } +} 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 641302b..1220234 100644 --- a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcRegistryImpl.java +++ b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcRegistryImpl.java @@ -10,15 +10,17 @@ import lol.pyr.znpcsplus.interaction.ActionRegistry; import lol.pyr.znpcsplus.packets.PacketFactory; import lol.pyr.znpcsplus.scheduling.TaskScheduler; import lol.pyr.znpcsplus.storage.NpcStorage; +import lol.pyr.znpcsplus.storage.NpcStorageType; import lol.pyr.znpcsplus.util.NpcLocation; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Bukkit; import org.bukkit.World; import java.util.*; import java.util.stream.Collectors; public class NpcRegistryImpl implements NpcRegistry { - private final NpcStorage storage; + private NpcStorage storage; private final PacketFactory packetFactory; private final ConfigManager configManager; private final LegacyComponentSerializer textSerializer; @@ -32,6 +34,10 @@ public class NpcRegistryImpl implements NpcRegistry { this.textSerializer = textSerializer; this.propertyRegistry = propertyRegistry; storage = configManager.getConfig().storageType().create(configManager, plugin, packetFactory, actionRegistry, typeRegistry, propertyRegistry, textSerializer); + if (storage == null) { + Bukkit.getLogger().warning("Failed to initialize storage, falling back to YAML"); + storage = NpcStorageType.YAML.create(configManager, plugin, packetFactory, actionRegistry, typeRegistry, propertyRegistry, textSerializer); + } this.packetFactory = packetFactory; this.configManager = configManager; @@ -57,7 +63,9 @@ public class NpcRegistryImpl implements NpcRegistry { } private void unregisterAll() { - for (NpcEntryImpl entry : npcList) entry.getNpc().delete(); + for (NpcEntryImpl entry : getAll()) { + if (entry.isSave()) entry.getNpc().delete(); + } npcList.clear(); npcIdLookupMap.clear(); npcUuidLookupMap.clear(); diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeImpl.java index b9c6884..3a4527e 100644 --- a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeImpl.java +++ b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeImpl.java @@ -115,7 +115,7 @@ public class NpcTypeImpl implements NpcType { public NpcTypeImpl build() { ServerVersion version = PacketEvents.getAPI().getServerManager().getVersion(); addProperties("fire", "invisible", "silent", "look", "look_distance", "view_distance", - "potion_color", "potion_ambient"); + "potion_color", "potion_ambient", "display_name"); if (!type.equals(EntityTypes.PLAYER)) addProperties("dinnerbone"); // TODO: make this look nicer after completing the rest of the properties if (version.isNewerThanOrEquals(ServerVersion.V_1_9)) addProperties("glow"); diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeRegistryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeRegistryImpl.java index 4a8f77b..5048100 100644 --- a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeRegistryImpl.java +++ b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeRegistryImpl.java @@ -73,6 +73,10 @@ public class NpcTypeRegistryImpl implements NpcTypeRegistry { .setHologramOffset(-0.275) .addProperties("creeper_state", "creeper_charged")); + register(builder(p, "end_crystal", EntityTypes.END_CRYSTAL) + .setHologramOffset(0.025) + .addProperties("beam_target", "show_base")); + register(builder(p, "ender_dragon", EntityTypes.ENDER_DRAGON) .setHologramOffset(6.0245)); @@ -206,8 +210,7 @@ public class NpcTypeRegistryImpl implements NpcTypeRegistry { .addEquipmentProperties()); register(builder(p, "evoker", EntityTypes.EVOKER) - .setHologramOffset(-0.025) - .addProperties("evoker_spell")); + .setHologramOffset(-0.025)); register(builder(p, "llama", EntityTypes.LLAMA) .setHologramOffset(-0.105) diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/StringParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/StringParser.java index a14ac42..c47ee6d 100644 --- a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/StringParser.java +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/StringParser.java @@ -14,6 +14,6 @@ public class StringParser extends ParserType { @Override public String parse(Deque deque) throws CommandExecutionException { - return deque.pollFirst(); + return String.join(" ", deque); } } diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3iParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3iParser.java new file mode 100644 index 0000000..286729d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3iParser.java @@ -0,0 +1,30 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.util.Vector3i; + +import java.util.Deque; + +public class Vector3iParser extends ParserType { + public Vector3iParser(Message message) { + super(message); + } + + @Override + public Vector3i parse(Deque deque) throws CommandExecutionException { + if (deque.size() == 0) { + return null; + } + try { + return new Vector3i( + Integer.parseInt(deque.pop()), + Integer.parseInt(deque.pop()), + Integer.parseInt(deque.pop())); + } catch (NumberFormatException e) { + throw new CommandExecutionException(); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorageType.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorageType.java index 91ff9b7..341767d 100644 --- a/plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorageType.java +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorageType.java @@ -6,6 +6,7 @@ import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; import lol.pyr.znpcsplus.interaction.ActionRegistry; import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.storage.sqlite.SQLiteStorage; import lol.pyr.znpcsplus.storage.yaml.YamlStorage; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; @@ -17,6 +18,17 @@ public enum NpcStorageType { public NpcStorage create(ConfigManager configManager, ZNpcsPlus plugin, PacketFactory packetFactory, ActionRegistry actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer) { return new YamlStorage(packetFactory, configManager, actionRegistry, typeRegistry, propertyRegistry, textSerializer, new File(plugin.getDataFolder(), "data")); } + }, + SQLITE { + @Override + public NpcStorage create(ConfigManager configManager, ZNpcsPlus plugin, PacketFactory packetFactory, ActionRegistry actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer) { + try { + return new SQLiteStorage(packetFactory, configManager, actionRegistry, typeRegistry, propertyRegistry, textSerializer, new File(plugin.getDataFolder(), "znpcsplus.sqlite")); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } }; public abstract NpcStorage create(ConfigManager configManager, ZNpcsPlus plugin, PacketFactory packetFactory, ActionRegistry actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer); diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/database/Database.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/database/Database.java new file mode 100644 index 0000000..1794abd --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/database/Database.java @@ -0,0 +1,16 @@ +package lol.pyr.znpcsplus.storage.database; + +import java.sql.Connection; +import java.util.logging.Logger; + +public abstract class Database { + protected final Logger logger; + protected Connection connection; + public Database(Logger logger){ + this.logger = logger; + } + + public abstract Connection getSQLConnection(); + + public abstract void load(); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLite.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLite.java new file mode 100644 index 0000000..344e038 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLite.java @@ -0,0 +1,101 @@ +package lol.pyr.znpcsplus.storage.sqlite; + +import lol.pyr.znpcsplus.storage.database.Database; + +import java.io.File; +import java.io.IOException; +import java.sql.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class SQLite extends Database{ + private final File dbFile; + public SQLite(File file, Logger logger){ + super(logger); + dbFile = file; + } + + public Connection getSQLConnection() { + if (!dbFile.exists()){ + try { + dbFile.createNewFile(); + } catch (IOException e) { + logger.log(Level.SEVERE, "File write error: "+dbFile.getName()); + } + } + try { + if(connection!=null&&!connection.isClosed()){ + return connection; + } + Class.forName("org.sqlite.JDBC"); + connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath()); + return connection; + } catch (SQLException ex) { + logger.log(Level.SEVERE,"SQLite exception on initialize", ex); + } catch (ClassNotFoundException ex) { + logger.log(Level.SEVERE, "SQLite JDBC library not found", ex); + } + return null; + } + + public void load() { + connection = getSQLConnection(); + } + + public boolean tableExists(String tableName) { + try { + Statement s = connection.createStatement(); + s.executeQuery("SELECT * FROM " + tableName + ";"); + s.close(); + return true; + } catch (SQLException e) { + return false; + } + } + + public boolean columnExists(String tableName, String columnName) { + try { + Statement s = connection.createStatement(); + s.executeQuery("SELECT " + columnName + " FROM " + tableName + ";"); + s.close(); + return true; + } catch (SQLException e) { + return false; + } + } + + public boolean addColumn(String tableName, String columnName, String type) { + if (columnExists(tableName, columnName)) return false; + try { + Statement s = connection.createStatement(); + s.executeQuery("ALTER TABLE " + tableName + " ADD COLUMN " + columnName + " " + type + ";"); + s.close(); + } catch (SQLException e) { + return false; + } + return true; + } + + public ResultSet executeQuery(String query) { + try { + Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery(query); + s.close(); + return rs; + } catch (SQLException e) { + return null; + } + } + + public int executeUpdate(String query) { + try { + Statement s = connection.createStatement(); + int rowCount = s.executeUpdate(query); + s.close(); + return rowCount; + } catch (SQLException e) { + e.printStackTrace(); + return -1; + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLiteStorage.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLiteStorage.java new file mode 100644 index 0000000..0519236 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLiteStorage.java @@ -0,0 +1,315 @@ +package lol.pyr.znpcsplus.storage.sqlite; + +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistry; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.storage.NpcStorage; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.io.File; +import java.math.BigDecimal; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class SQLiteStorage implements NpcStorage { + private final static Logger logger = Logger.getLogger("SQLiteStorage"); + + private final PacketFactory packetFactory; + private final ConfigManager configManager; + private final ActionRegistry actionRegistry; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final LegacyComponentSerializer textSerializer; + private final SQLite database; + + private final String TABLE_NPCS; + private final String TABLE_NPCS_PROPERTIES; + private final String TABLE_NPCS_HOLOGRAMS; + private final String TABLE_NPCS_ACTIONS; + + public SQLiteStorage(PacketFactory packetFactory, ConfigManager configManager, ActionRegistry actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, File file) { + this.packetFactory = packetFactory; + this.configManager = configManager; + this.actionRegistry = actionRegistry; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.textSerializer = textSerializer; + this.database = new SQLite(file, logger); + database.load(); + if (database.getSQLConnection() == null) { + throw new RuntimeException("Failed to initialize SQLite Storage."); + } + TABLE_NPCS = "npcs"; + TABLE_NPCS_PROPERTIES = "npcs_properties"; + TABLE_NPCS_HOLOGRAMS = "npcs_holograms"; + TABLE_NPCS_ACTIONS = "npcs_actions"; + validateTables(); + } + + private void validateTables() { + if (!database.tableExists(TABLE_NPCS)) { + logger.info("Creating table " + TABLE_NPCS + "..."); + createNpcsTable(); + } + if (!database.tableExists(TABLE_NPCS_PROPERTIES)) { + logger.info("Creating table " + TABLE_NPCS_PROPERTIES + "..."); + createNpcsPropertiesTable(); + } + if (!database.tableExists(TABLE_NPCS_HOLOGRAMS)) { + logger.info("Creating table " + TABLE_NPCS_HOLOGRAMS + "..."); + createNpcsHologramsTable(); + } + if (!database.tableExists(TABLE_NPCS_ACTIONS)) { + logger.info("Creating table " + TABLE_NPCS_ACTIONS + "..."); + createNpcsActionsTable(); + } + updateTables(); + } + + private void createNpcsTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS + " " + + "(id TEXT PRIMARY KEY, isProcessed BOOLEAN, allowCommands BOOLEAN, enabled BOOLEAN, " + + "uuid TEXT, world TEXT, x REAL, y REAL, z REAL, yaw REAL, pitch REAL, type TEXT, hologramOffset REAL, hologramRefreshDelay INTEGER)") != -1) { + logger.info("Table " + TABLE_NPCS + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS + "."); + } + } + + private void createNpcsPropertiesTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS_PROPERTIES + " " + + "(npc_id TEXT, property TEXT, value TEXT, PRIMARY KEY (npc_id, property))") != -1) { + logger.info("Table " + TABLE_NPCS_PROPERTIES + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS_PROPERTIES + "."); + } + } + + private void createNpcsHologramsTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS_HOLOGRAMS + " " + + "(npc_id TEXT, line INTEGER, text TEXT, PRIMARY KEY (npc_id, line))") != -1) { + logger.info("Table " + TABLE_NPCS_HOLOGRAMS + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS_HOLOGRAMS + "."); + } + } + + private void createNpcsActionsTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS_ACTIONS + " " + + "(npc_id TEXT, action_id INTEGER, action_data TEXT, PRIMARY KEY (npc_id, action_id))") != -1) { + logger.info("Table " + TABLE_NPCS_ACTIONS + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS_ACTIONS + "."); + } + } + + private void updateTables() { + // Any table updates go here + } + + @Override + public Collection loadNpcs() { + Map npcMap = new HashMap<>(); + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS); + ResultSet rs = st.executeQuery(); + while (rs.next()) { + NpcImpl npc = new NpcImpl(UUID.fromString(rs.getString("uuid")), propertyRegistry, configManager, packetFactory, textSerializer, + rs.getString("world"), typeRegistry.getByName(rs.getString("type")), + new NpcLocation(rs.getDouble("x"), rs.getDouble("y"), rs.getDouble("z"), rs.getFloat("yaw"), rs.getFloat("pitch"))); + + if (!rs.getBoolean("enabled")) npc.setEnabled(false); + + npc.getHologram().setOffset(rs.getDouble("hologramOffset")); + if (rs.getBigDecimal("hologramRefreshDelay") != null) npc.getHologram().setRefreshDelay(rs.getBigDecimal("hologramRefreshDelay").longValue()); + + NpcEntryImpl entry = new NpcEntryImpl(rs.getString("id"), npc); + entry.setProcessed(rs.getBoolean("isProcessed")); + entry.setAllowCommandModification(rs.getBoolean("allowCommands")); + entry.setSave(true); + npcMap.put(rs.getString("id"), entry); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS_PROPERTIES); + ResultSet rs = st.executeQuery(); + while (rs.next()) { + NpcEntryImpl entry = npcMap.get(rs.getString("npc_id")); + String key = rs.getString("property"); + if (entry != null) { + EntityPropertyImpl property = propertyRegistry.getByName(key); + if (property == null) { + logger.warning("Unknown property '" + key + "' for npc '" + rs.getString("npc_id") + "'. skipping ..."); + continue; + } + PropertySerializer serializer = propertyRegistry.getSerializer(property.getType()); + if (serializer == null) { + logger.warning("Unknown serializer for property '" + key + "' for npc '" + rs.getString("npc_id") + "'. skipping ..."); + continue; + } + Object value = serializer.deserialize(rs.getString("value")); + if (value == null) { + logger.warning("Failed to deserialize property '" + key + "' for npc '" + rs.getString("npc_id") + "'. Resetting to default ..."); + value = property.getDefaultValue(); + } + entry.getNpc().UNSAFE_setProperty(property, value); + npcMap.put(rs.getString("npc_id"), entry); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS_HOLOGRAMS + " ORDER BY line ASC"); + ResultSet rs = st.executeQuery(); + + while (rs.next()) { + NpcEntryImpl entry = npcMap.get(rs.getString("npc_id")); + if (entry != null) { + entry.getNpc().getHologram().insertLine(rs.getInt("line"), rs.getString("text")); + } + npcMap.put(rs.getString("npc_id"), entry); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS_ACTIONS + " ORDER BY action_id ASC"); + ResultSet rs = st.executeQuery(); + + while (rs.next()) { + NpcEntryImpl entry = npcMap.get(rs.getString("npc_id")); + if (entry != null) { + entry.getNpc().addAction(actionRegistry.deserialize(rs.getString("action_data"))); + } + npcMap.put(rs.getString("npc_id"), entry); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + return npcMap.values().stream().filter(Objects::nonNull).collect(Collectors.toList()); + } + + @Override + public void saveNpcs(Collection npcs) { + long start = System.currentTimeMillis(); + for (NpcEntryImpl entry : npcs) try { + PreparedStatement ps; + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS + " (id, isProcessed, allowCommands, enabled, uuid, world, x, y, z, yaw, pitch, type, hologramOffset, hologramRefreshDelay) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); + ps.setString(1, entry.getId()); + ps.setBoolean(2, entry.isProcessed()); + ps.setBoolean(3, entry.isAllowCommandModification()); + NpcImpl npc = entry.getNpc(); + ps.setBoolean(4, npc.isEnabled()); + ps.setString(5, npc.getUuid().toString()); + ps.setString(6, npc.getWorldName()); + ps.setDouble(7, npc.getLocation().getX()); + ps.setDouble(8, npc.getLocation().getY()); + ps.setDouble(9, npc.getLocation().getZ()); + ps.setFloat(10, npc.getLocation().getYaw()); + ps.setFloat(11, npc.getLocation().getPitch()); + ps.setString(12, npc.getType().getName()); + HologramImpl hologram = npc.getHologram(); + ps.setDouble(13, hologram.getOffset()); + if(hologram.getRefreshDelay() != -1) ps.setBigDecimal(14, new BigDecimal(hologram.getRefreshDelay())); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_PROPERTIES + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + for (EntityProperty property : npc.getAllProperties()) try { + PropertySerializer serializer = propertyRegistry.getSerializer(((EntityPropertyImpl) property).getType()); + if (serializer == null) { + logger.warning("Unknown serializer for property '" + property.getName() + "' for npc '" + entry.getId() + "'. skipping ..."); + continue; + } + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS_PROPERTIES + " (npc_id, property, value) VALUES(?,?,?)"); + ps.setString(1, entry.getId()); + ps.setString(2, property.getName()); + ps.setString(3, serializer.UNSAFE_serialize(npc.getProperty(property))); + ps.executeUpdate(); + } catch (Exception exception) { + logger.severe("Failed to serialize property " + property.getName() + " for npc with id " + entry.getId()); + exception.printStackTrace(); + } + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_HOLOGRAMS + " WHERE npc_id = ? AND line > ?"); + ps.setString(1, entry.getId()); + ps.setInt(2, hologram.getLines().size() - 1); + ps.executeUpdate(); + + for (int i = 0; i < hologram.getLines().size(); i++) { + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS_HOLOGRAMS + " (npc_id, line, text) VALUES(?,?,?)"); + ps.setString(1, entry.getId()); + ps.setInt(2, i); + ps.setString(3, hologram.getLine(i)); + ps.executeUpdate(); + } + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_ACTIONS + " WHERE npc_id = ? AND action_id > ?"); + ps.setString(1, entry.getId()); + ps.setInt(2, npc.getActions().size() - 1); + ps.executeUpdate(); + + for (int i = 0; i < npc.getActions().size(); i++) { + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS_ACTIONS + " (npc_id, action_id, action_data) VALUES(?,?,?)"); + ps.setString(1, entry.getId()); + ps.setInt(2, i); + String action = actionRegistry.serialize(npc.getActions().get(i)); + if (action == null) continue; + ps.setString(3, action); + ps.executeUpdate(); + } + } catch (SQLException exception) { + logger.severe("Failed to save npc with id " + entry.getId()); + exception.printStackTrace(); + } + if (configManager.getConfig().debugEnabled()) { + logger.info("Saved " + npcs.size() + " npcs in " + (System.currentTimeMillis() - start) + "ms"); + } + } + + @Override + public void deleteNpc(NpcEntryImpl entry) { + try { + PreparedStatement ps; + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS + " WHERE id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_PROPERTIES + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_HOLOGRAMS + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_ACTIONS + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + } catch (SQLException exception) { + logger.severe("Failed to delete npc with id " + entry.getId()); + exception.printStackTrace(); + } + } +}