diff --git a/src/main/java/de/jaschastarke/minecraft/limitedcreative/MainCommand.java b/src/main/java/de/jaschastarke/minecraft/limitedcreative/MainCommand.java index f52571e..6d676ef 100644 --- a/src/main/java/de/jaschastarke/minecraft/limitedcreative/MainCommand.java +++ b/src/main/java/de/jaschastarke/minecraft/limitedcreative/MainCommand.java @@ -26,6 +26,7 @@ public class MainCommand extends BukkitCommand implements IHelpDescribed, IMetho public MainCommand() { } public MainCommand(LimitedCreative plugin) { + super(plugin); this.plugin = plugin; } diff --git a/src/main/java/de/jaschastarke/minecraft/limitedcreative/blockstate/BlockStateCommand.java b/src/main/java/de/jaschastarke/minecraft/limitedcreative/blockstate/BlockStateCommand.java index 21b3be4..c1c593f 100644 --- a/src/main/java/de/jaschastarke/minecraft/limitedcreative/blockstate/BlockStateCommand.java +++ b/src/main/java/de/jaschastarke/minecraft/limitedcreative/blockstate/BlockStateCommand.java @@ -2,6 +2,7 @@ package de.jaschastarke.minecraft.limitedcreative.blockstate; import java.util.Date; +import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; @@ -12,6 +13,7 @@ import com.sk89q.worldedit.bukkit.WorldEditPlugin; import com.sk89q.worldedit.bukkit.selections.Selection; import de.jaschastarke.LocaleString; +import de.jaschastarke.bukkit.lib.chat.ChatFormattings; import de.jaschastarke.bukkit.lib.commands.BukkitCommand; import de.jaschastarke.bukkit.lib.commands.CommandContext; import de.jaschastarke.bukkit.lib.commands.CommandException; @@ -20,6 +22,10 @@ import de.jaschastarke.bukkit.lib.commands.IHelpDescribed; import de.jaschastarke.bukkit.lib.commands.MissingPermissionCommandException; import de.jaschastarke.bukkit.lib.commands.annotations.IsCommand; import de.jaschastarke.bukkit.lib.commands.annotations.Usages; +import de.jaschastarke.bukkit.lib.commands.parser.DefinedParameterParser; +import de.jaschastarke.bukkit.lib.database.DBHelper; +import de.jaschastarke.database.DatabaseConfigurationException; +import de.jaschastarke.database.db.Database; import de.jaschastarke.maven.ArchiveDocComments; import de.jaschastarke.maven.PluginCommand; import de.jaschastarke.minecraft.lib.permissions.IAbstractPermission; @@ -27,6 +33,7 @@ import de.jaschastarke.minecraft.limitedcreative.ModBlockStates; import de.jaschastarke.minecraft.limitedcreative.blockstate.BlockState.Source; import de.jaschastarke.minecraft.limitedcreative.blockstate.DBModel.Cuboid; import de.jaschastarke.minecraft.limitedcreative.blockstate.DBModel.DBTransaction; +import de.jaschastarke.modularize.ModuleEntry.ModuleState; /** * LimitedCreative-BlockState-Command: modify blockstate database to prevent drops of selected blocks (requires WorldEdit) @@ -43,7 +50,8 @@ public class BlockStateCommand extends BukkitCommand implements IHelpDescribed { this.help = this.getDefaultHelpCommand(); } public BlockStateCommand(ModBlockStates mod) { - this(); + super(mod.getPlugin()); + this.help = this.getDefaultHelpCommand(); this.mod = mod; } @@ -68,7 +76,7 @@ public class BlockStateCommand extends BukkitCommand implements IHelpDescribed { @Override public CharSequence[] getUsages() { - return null; + return new String[]{"..."}; } @Override @@ -80,6 +88,12 @@ public class BlockStateCommand extends BukkitCommand implements IHelpDescribed { public CharSequence getPackageName() { return mod.getPlugin().getName() + " - " + mod.getName(); } + + public boolean execute(final CommandContext context, final String[] args) throws MissingPermissionCommandException, CommandException { + if (mod.getModuleEntry().getState() != ModuleState.ENABLED) + throw new CommandException("Module " + mod.getName() + " is disabled"); + return super.execute(context, args); + } /** * Modifies the Block-GameMode-Database and sets all blocks in the selection to the provided gamemode. Set it @@ -89,7 +103,6 @@ public class BlockStateCommand extends BukkitCommand implements IHelpDescribed { * @throws MissingPermissionCommandException */ @IsCommand("set") - //@NeedsPermission("region") @Usages("") public boolean setGameMode(final CommandContext context, String... args) throws CommandException, MissingPermissionCommandException { if (!mod.getPlugin().getServer().getPluginManager().isPluginEnabled("WorldEdit")) { @@ -169,6 +182,60 @@ public class BlockStateCommand extends BukkitCommand implements IHelpDescribed { }); return true; } + + /** + * Imports BlockState Data from a given Database to the current active Database. + * A Server-Restart is needed after migration! + * Parameters: + * -u --update Don't delete existing records / only overwrite if newer + */ + @IsCommand("migrate") + @Usages("-u [username] [password]") + public boolean migrateDatabase(final CommandContext context, String... args) throws CommandException, MissingPermissionCommandException { + DefinedParameterParser params = new DefinedParameterParser(args, new String[]{"debug", "d", "update", "u", "confirm"}); + if (params.getArgumentCount() < 1) {// doesn't count parameters + return false; + } + + if (Bukkit.getServer().getOnlinePlayers().length > (context.isPlayer() ? 1 : 0)) { + context.responseFormatted(ChatFormattings.ERROR, L("command.blockstate.migrate_useronline_error")); + return true; + } + + Database source; + Database target; + try { + + if (params.getArgumentCount() < 2) + source = DBHelper.createConnection(params.getArgument(0)); + else if (params.getArgumentCount() < 3) + source = DBHelper.createConnection(params.getArgument(0), params.getArgument(1), null); + else + source = DBHelper.createConnection(params.getArgument(0), params.getArgument(1), params.getArgument(2)); + + target = mod.getPlugin().getDatabaseConnection(); + } catch (DatabaseConfigurationException e) { + context.responseFormatted(ChatFormattings.ERROR, L("command.blockstate.migrate_connect_error", e.getMessage())); + return true; + } + + if (!params.getFlags().contains("confirm")) { + context.responseFormatted(ChatFormattings.INFO, L("command.blockstate.migrate_confirm", "--confirm")); + return true; + } + + mod.getModuleEntry().disable(); + DatabaseMigrationThread thread = new DatabaseMigrationThread(mod, context, source, target); + if (params.getFlags().contains("update") || params.getFlags().contains("u")) { + thread.setMode(DatabaseMigrationThread.Mode.UPDATE); + } + if (params.getFlags().contains("debug") || params.getFlags().contains("d")) { + thread.setDebug(true); + } + thread.start(); + context.response(L("command.blockstate.migrate_started", source.getType(), target.getType())); + return true; + } private String L(String msg, Object... args) { return mod.getPlugin().getLocale().trans(msg, args); diff --git a/src/main/java/de/jaschastarke/minecraft/limitedcreative/blockstate/DBQueries.java b/src/main/java/de/jaschastarke/minecraft/limitedcreative/blockstate/DBQueries.java index 525ca1e..322faa6 100644 --- a/src/main/java/de/jaschastarke/minecraft/limitedcreative/blockstate/DBQueries.java +++ b/src/main/java/de/jaschastarke/minecraft/limitedcreative/blockstate/DBQueries.java @@ -10,23 +10,24 @@ import java.util.List; import org.bukkit.GameMode; import org.bukkit.Location; +import de.jaschastarke.bukkit.lib.database.ResultIterator; import de.jaschastarke.database.Type; import de.jaschastarke.database.db.Database; -import de.jaschastarke.minecraft.limitedcreative.ModBlockStates; import de.jaschastarke.minecraft.limitedcreative.blockstate.BlockState.Source; +import de.jaschastarke.utils.IDebugLogHolder; public class DBQueries { private Database db; - private ModBlockStates mod; - public DBQueries(ModBlockStates mod, Database db) { - this.mod = mod; + private IDebugLogHolder dbg; + public DBQueries(IDebugLogHolder mod, Database db) { + this.dbg = mod; this.db = db; } private PreparedStatement find = null; public BlockState find(Location loc) throws SQLException { - if (mod.isDebug()) - mod.getLog().debug("DBQuery: find: " + loc.toString()); + if (dbg.isDebug()) + dbg.getLog().debug("DBQuery: find: " + loc.toString()); if (find == null) { find = db.prepare("SELECT * FROM lc_block_state WHERE x = ? AND y = ? AND z = ? AND world = ?"); } @@ -42,15 +43,17 @@ public class DBQueries { bs.setGameMode(getGameMode(rs)); bs.setPlayerName(rs.getString("player")); bs.setSource(getSource(rs)); + rs.close(); return bs; } + rs.close(); return null; } private PreparedStatement findall = null; public List findAllIn(DBModel.Cuboid c) throws SQLException { - if (mod.isDebug()) - mod.getLog().debug("DBQuery: findAllIn: " + c.toString()); + if (dbg.isDebug()) + dbg.getLog().debug("DBQuery: findAllIn: " + c.toString()); List blocks = new ArrayList(); if (findall == null) { findall = db.prepare("SELECT * FROM lc_block_state WHERE x >= ? AND x <= ? AND y >= ? AND y <= ? AND z >= ? AND z <= ? AND world = ?"); @@ -72,16 +75,44 @@ public class DBQueries { bs.setSource(getSource(rs)); blocks.add(bs); } + rs.close(); return blocks; } + public Iterable iterateAllIn(final DBModel.Cuboid c) throws SQLException { + if (dbg.isDebug()) + dbg.getLog().debug("DBQuery: iterateAllIn: " + c.toString()); + if (findall == null) { + findall = db.prepare("SELECT * FROM lc_block_state WHERE x >= ? AND x <= ? AND y >= ? AND y <= ? AND z >= ? AND z <= ? AND world = ?"); + } + findall.setInt(1, c.getMinX()); + findall.setInt(2, c.getMaxX()); + findall.setInt(3, c.getMinY()); + findall.setInt(4, c.getMaxY()); + findall.setInt(5, c.getMinZ()); + findall.setInt(6, c.getMaxZ()); + findall.setString(7, c.getWorld().getUID().toString()); + ResultSet rs = findall.executeQuery(); + return new ResultIterator(rs) { + @Override + protected BlockState fetch(ResultSet rs) throws SQLException { + BlockState bs = new BlockState(); + bs.setLocation(new Location(c.getWorld(), rs.getInt("x"), rs.getInt("y"), rs.getInt("z"))); + bs.setDate(rs.getTimestamp("cdate")); + bs.setGameMode(getGameMode(rs)); + bs.setPlayerName(rs.getString("player")); + bs.setSource(getSource(rs)); + return bs; + } + }; + } private PreparedStatement delete = null; public boolean delete(BlockState s) throws SQLException { return delete(s.getLocation()); } public boolean delete(Location loc) throws SQLException { - if (mod.isDebug()) - mod.getLog().debug("DBQuery: delete: " + loc.toString()); + if (dbg.isDebug()) + dbg.getLog().debug("DBQuery: delete: " + loc.toString()); if (delete == null) { delete = db.prepare("DELETE FROM lc_block_state WHERE x = ? AND y = ? AND z = ? AND world = ?"); } @@ -94,8 +125,8 @@ public class DBQueries { private PreparedStatement update = null; public boolean update(BlockState s) throws SQLException { - if (mod.isDebug()) - mod.getLog().debug("DBQuery: update: " + s.toString()); + if (dbg.isDebug()) + dbg.getLog().debug("DBQuery: update: " + s.toString()); if (update == null) { update = db.prepare("UPDATE lc_block_state SET gm = ?, player = ?, cdate = ?, source = ?"+ "WHERE x = ? AND y = ? AND z = ? AND world = ? "); @@ -128,8 +159,8 @@ public class DBQueries { } public boolean move(Location oldLoc, Location newLoc) throws SQLException { - if (mod.isDebug()) - mod.getLog().debug("DBQuery: move: " + oldLoc.toString() + ", " + newLoc.toString()); + if (dbg.isDebug()) + dbg.getLog().debug("DBQuery: move: " + oldLoc.toString() + ", " + newLoc.toString()); if (move == null) { move = db.prepare("UPDATE lc_block_state SET x = ?, y = ?, z = ? "+ "WHERE x = ? AND y = ? AND z = ? AND world = ?"); @@ -156,8 +187,8 @@ public class DBQueries { }*/ private PreparedStatement insert = null; public boolean insert(BlockState s) throws SQLException { - if (mod.isDebug()) - mod.getLog().debug("DBQuery: insert: " + s.toString()); + if (dbg.isDebug()) + dbg.getLog().debug("DBQuery: insert: " + s.toString()); if (insert == null) { insert = db.prepare("INSERT INTO lc_block_state (x, y, z, world, gm, player, cdate, source)"+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); @@ -220,8 +251,8 @@ public class DBQueries { switch (db.getType()) { case SQLite: if (!db.getDDL().tableExists("lc_block_state")) { - if (mod.isDebug()) - mod.getLog().debug("DBQuery: initTable SQLite: lc_block_state"); + if (dbg.isDebug()) + dbg.getLog().debug("DBQuery: initTable SQLite: lc_block_state"); db.execute( "CREATE TABLE lc_block_state ("+ "x integer,"+ @@ -236,14 +267,14 @@ public class DBQueries { "constraint ck_lc_block_state_gm check (gm in (0,1,2)),"+ "constraint ck_lc_block_state_source check (source in (0,1,2,3,4))"+ ")" - ); + ).close(); db.getLogger().info("Created SQLite-Table: lc_block_state"); } break; case MySQL: if (!db.getDDL().tableExists("lc_block_state")) { - if (mod.isDebug()) - mod.getLog().debug("DBQuery: initTable MySQL: lc_block_state"); + if (dbg.isDebug()) + dbg.getLog().debug("DBQuery: initTable MySQL: lc_block_state"); db.execute( "CREATE TABLE IF NOT EXISTS lc_block_state ("+ "x INT NOT NULL,"+ @@ -256,7 +287,7 @@ public class DBQueries { "source ENUM('SEED','PLAYER','EDIT','COMMAND','UNKNOWN') NOT NULL,"+ "PRIMARY KEY (x, y, z, world)"+ ")" - ); + ).close(); db.getLogger().info("Created MySQL-Table: lc_block_state"); } break; diff --git a/src/main/java/de/jaschastarke/minecraft/limitedcreative/blockstate/DatabaseMigrationThread.java b/src/main/java/de/jaschastarke/minecraft/limitedcreative/blockstate/DatabaseMigrationThread.java new file mode 100644 index 0000000..d45a9b1 --- /dev/null +++ b/src/main/java/de/jaschastarke/minecraft/limitedcreative/blockstate/DatabaseMigrationThread.java @@ -0,0 +1,178 @@ +package de.jaschastarke.minecraft.limitedcreative.blockstate; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; + +import de.jaschastarke.bukkit.lib.chat.ChatFormattings; +import de.jaschastarke.bukkit.lib.commands.CommandContext; +import de.jaschastarke.database.db.Database; +import de.jaschastarke.minecraft.limitedcreative.ModBlockStates; +import de.jaschastarke.minecraft.limitedcreative.blockstate.DBModel.Cuboid; +import de.jaschastarke.utils.IDebugLogHolder; +import de.jaschastarke.utils.ISimpleLogger; + +public class DatabaseMigrationThread extends Thread implements IDebugLogHolder { + private static final int CHUNK_SIZE = 512; + private ModBlockStates mod; + private CommandContext context; + private Database source; + private Database target; + private Mode mode = Mode.REPLACE; + private boolean debug = false; + + public static enum Mode { + REPLACE, + UPDATE + } + + public DatabaseMigrationThread(ModBlockStates mod, CommandContext context, Database source, Database target) { + this.mod = mod; + this.context = context; + this.source = source; + this.target = target; + setName("LC BlockState Database-Migration"); + setPriority(MIN_PRIORITY); + } + + public Mode getMode() { + return mode; + } + public void setMode(Mode mode) { + this.mode = mode; + } + public void setDebug(boolean debug) { + this.debug = debug; + } + + @Override + public boolean isDebug() { + return debug; + } + + @Override + public ISimpleLogger getLog() { + return mod.getLog(); + } + + @Override + public void run() { + try { + if (!target.isInTransaction()) + target.startTransaction(); + + int rowCount = 0; + Connection sourceConnection = source.getConnection(); + Connection targetConnection = target.getConnection(); + + if (mode == Mode.REPLACE) { + targetConnection.createStatement().execute("DELETE FROM lc_block_state"); + } + + DBQueries sourceDB = new DBQueries(this, source); + DBQueries targetDB = new DBQueries(this, target); + + List worldBounds = new ArrayList(); + ResultSet fetchBounds = sourceConnection.createStatement().executeQuery("SELECT world, MIN(x), MIN(z), MAX(x), MAX(z) FROM lc_block_state GROUP BY world"); + while (fetchBounds.next()) { + worldBounds.add(new WorldSize(fetchBounds.getString("world"), + fetchBounds.getInt(2), + fetchBounds.getInt(3), + fetchBounds.getInt(4), + fetchBounds.getInt(5))); + } + fetchBounds.close(); + + for (WorldSize bounds : worldBounds) { + World world = mod.getPlugin().getServer().getWorld(bounds.getWorld()); + if (world != null) { + long time = System.currentTimeMillis(); + int itCount = 0; + if (mod.isDebug()) + mod.getLog().debug("Processing world " + world.getName() + " with bounds: " + bounds); + + for (int x = bounds.getMinX(); x <= bounds.getMaxX(); x += CHUNK_SIZE + 1) { + for (int z = bounds.getMinZ(); z <= bounds.getMaxZ(); z += CHUNK_SIZE + 1) { + Cuboid c = new Cuboid(); + c.add(new Location(world, x, 0, z)); + c.add(new Location(world, x + CHUNK_SIZE, world.getMaxHeight(), z + CHUNK_SIZE)); + System.out.println("Fetching Cuboid: " + c.toString()); + + for (BlockState bs : sourceDB.iterateAllIn(c)) { + if (mode == Mode.UPDATE) { + BlockState xs = targetDB.find(bs.getLocation()); + if (xs == null) { + targetDB.insert(bs); + } else if (xs.getDate().before(bs.getDate())) { + targetDB.update(bs); + } + } else { + targetDB.insert(bs); + } + rowCount++; + itCount++; + } + + Thread.yield(); + } + } + String region = "Region{world = " + world.getName() + ", x = [" + bounds.getMinX() + "; " + (bounds.getMinX() + CHUNK_SIZE) + "], z = [" + bounds.getMinZ() + "; " + (bounds.getMinZ() + CHUNK_SIZE) + "]}"; + mod.getLog().info("Migration processed " + itCount + " BlockStates in " + region + " within " + ((System.currentTimeMillis() - time) / 1000.0) + " seconds"); + } + } + + target.endTransaction(); + context.responseFormatted(ChatFormattings.SUCCESS, L("command.blockstate.migration_finished", rowCount) + " " + + context.getFormatter().formatString(ChatFormattings.ERROR, L("command.blockstate.migration_finished_restart"))); + } catch (SQLException e) { + try { + target.revertTransaction(); + } catch (SQLException e1) {} + context.responseFormatted(ChatFormattings.ERROR, L("command.blockstate.migration_error", e.getMessage())); + } + } + + private String L(String msg, Object... args) { + return mod.getPlugin().getLocale().trans(msg, args); + } + + private static class WorldSize { + UUID w; + int minX, minZ, maxX, maxZ; + + public WorldSize(String w, int minX, int minZ, int maxX, int maxZ) { + this.w = UUID.fromString(w); + this.minX = minX; + this.minZ = minZ; + this.maxX = maxX; + this.maxZ = maxZ; + } + public String toString() { + World world = Bukkit.getServer().getWorld(w); + String wn = world == null ? w.toString() : world.getName(); + return getClass().getSimpleName() + "{world = " + wn + ", minX = " + minX + ", minZ = " + minZ + ", maxX = " + maxX + ", maxZ = " + maxZ + "}"; + } + public UUID getWorld() { + return w; + } + public int getMinX() { + return minX; + } + public int getMinZ() { + return minZ; + } + public int getMaxX() { + return maxX; + } + public int getMaxZ() { + return maxZ; + } + } +} diff --git a/src/main/java/de/jaschastarke/minecraft/limitedcreative/regions/RegionsCommand.java b/src/main/java/de/jaschastarke/minecraft/limitedcreative/regions/RegionsCommand.java index 703081f..c626187 100644 --- a/src/main/java/de/jaschastarke/minecraft/limitedcreative/regions/RegionsCommand.java +++ b/src/main/java/de/jaschastarke/minecraft/limitedcreative/regions/RegionsCommand.java @@ -30,6 +30,7 @@ import de.jaschastarke.minecraft.limitedcreative.ModRegions; import de.jaschastarke.minecraft.limitedcreative.regions.worldguard.FlagList; import de.jaschastarke.minecraft.limitedcreative.regions.worldguard.FlagValue; import de.jaschastarke.minecraft.limitedcreative.regions.worldguard.Region; +import de.jaschastarke.modularize.ModuleEntry.ModuleState; /** * LimitedCreative-Region-Command: configure creative regions @@ -48,7 +49,8 @@ public class RegionsCommand extends BukkitCommand implements IHelpDescribed { this.help = this.getDefaultHelpCommand(); } public RegionsCommand(ModRegions mod) { - this(); + super(mod.getPlugin()); + this.help = this.getDefaultHelpCommand(); this.mod = mod; this.wg = (WorldGuardPlugin) mod.getPlugin().getServer().getPluginManager().getPlugin(WorldGuardIntegration.PLUGIN_NAME); } @@ -62,6 +64,12 @@ public class RegionsCommand extends BukkitCommand implements IHelpDescribed { public String[] getAliases() { return new String[]{"/region"}; } + + public boolean execute(final CommandContext context, final String[] args) throws MissingPermissionCommandException, CommandException { + if (mod.getModuleEntry().getState() != ModuleState.ENABLED) + throw new CommandException("Module " + mod.getName() + " is disabled"); + return super.execute(context, args); + } /** * @internal has no effect, as not tested by any command handler diff --git a/src/main/resources/lang/messages.properties b/src/main/resources/lang/messages.properties index 5b87b84..8d508dc 100644 --- a/src/main/resources/lang/messages.properties +++ b/src/main/resources/lang/messages.properties @@ -30,6 +30,13 @@ command.blockstate.requires_worldedit: This command requires WorldEdit-Plugin command.blockstate.worledit_selection_empty: Select a region with WorldEdit first command.blockstate.command_updated: Successfully updated {0} blocks. command.blockstate.command_failed: Failed to update blocks. See server.log for details. +command.blockstate.migrate_started: Database migration from {0} to {1} started... +command.blockstate.migrate_connect_error: Connection to database failed with error: {0} +command.blockstate.migration_finished: Database migration of {0} records successful. +command.blockstate.migration_finished_restart: A Server-Restart is required! +command.blockstate.migration_error: Migration failed with error: {0} +command.blockstate.migrate_useronline_error: There are players on the Server. The migration shouldn't be run with active players. +command.blockstate.migrate_confirm: Are you sure you want to start the Migration? It may take a very long time and much CPU. To confirm execution run the command with the following added: {0} cmdblock.blocked: This command is blocked while in creative mode. cmdblock.blocked.requires_worldedit: WorlEdit is required to use this command