diff --git a/README.md b/README.md index f481b537..bc1cfada 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ **⭐ Modern conveniences** — Beautiful clickable menus and glowing display entity visualisation. Make groups to manage trust in bulk. -**⭐ Easy to import & configure** — Import existing player claims and profiles from GriefPrevention. Has a robust, [extensible API](https://william278.net/docs/huskclaims/api). +**⭐ Easy to import & configure** — Import existing player claims and profiles from GriefPrevention. Seamlessly migrate between MySQL and SQLite. Has a robust, [extensible API](https://william278.net/docs/huskclaims/api). **Ready?** [Let the claims begin!](https://william278.net/docs/huskclaims/setup) diff --git a/bukkit/src/main/java/net/william278/huskclaims/hook/BukkitHookProvider.java b/bukkit/src/main/java/net/william278/huskclaims/hook/BukkitHookProvider.java index ca1b087e..d5bc722f 100644 --- a/bukkit/src/main/java/net/william278/huskclaims/hook/BukkitHookProvider.java +++ b/bukkit/src/main/java/net/william278/huskclaims/hook/BukkitHookProvider.java @@ -52,6 +52,9 @@ default List getAvailableHooks() { // Add bukkit importers hooks.add(new BukkitGriefPreventionImporter(getPlugin())); + + // Add database importer + hooks.add(new DatabaseImporter(getPlugin())); return hooks; } diff --git a/common/src/main/java/net/william278/huskclaims/command/HuskClaimsCommand.java b/common/src/main/java/net/william278/huskclaims/command/HuskClaimsCommand.java index 1da9ec46..cce77739 100644 --- a/common/src/main/java/net/william278/huskclaims/command/HuskClaimsCommand.java +++ b/common/src/main/java/net/william278/huskclaims/command/HuskClaimsCommand.java @@ -40,6 +40,7 @@ import net.william278.huskclaims.user.SavedUser; import net.william278.huskclaims.util.StatusLine; import net.william278.paginedown.PaginatedList; +import net.william278.huskclaims.hook.DatabaseImporter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -177,8 +178,25 @@ public List suggest(@NotNull CommandUser user, @NotNull String[] args) { case 0, 1 -> SUB_COMMANDS.keySet().stream().filter(n -> user.hasPermission(getPermission(n))).toList(); default -> switch (args[0].toLowerCase(Locale.ENGLISH)) { case "import" -> switch (args.length - 2) { - case 0 -> plugin.getImporters().stream().map(Importer::getName).toList(); - case 1 -> List.of("start", "set", "reset"); + case 0 -> plugin.getImporters().stream() + .map(Importer::getName) + .collect(Collectors.toList()); + case 1 -> { + if (args[1].equalsIgnoreCase("database")) { + yield List.of("mysql", "sqlite", "mariadb"); + } + yield List.of("start", "set", "reset"); + } + case 2 -> { + if (args[1].equalsIgnoreCase("database")) { + yield List.of("mysql", "sqlite", "mariadb"); + } + yield plugin.getImporters().stream().filter(i -> i.getName().equalsIgnoreCase(args[1])) + .flatMap(i -> i.getRequiredParameters().keySet().stream() + .filter(p -> parseKeyValues(args, 3).keySet().stream() + .noneMatch(p::equalsIgnoreCase)) + .map("%s:"::formatted)).toList(); + } default -> plugin.getImporters().stream().filter(i -> i.getName().equalsIgnoreCase(args[1])) .flatMap(i -> i.getRequiredParameters().keySet().stream() .filter(p -> parseKeyValues(args, 3).keySet().stream() @@ -194,6 +212,48 @@ public List suggest(@NotNull CommandUser user, @NotNull String[] args) { } private void handleImportCommand(@NotNull CommandUser executor, @NotNull String[] args) { + // Special handling for database import command + if (args.length >= 3 && args[0].equalsIgnoreCase("database")) { + final String sourceType = args[1].toLowerCase(Locale.ENGLISH); + final String targetType = args[2].toLowerCase(Locale.ENGLISH); + + // Validate database types + if (!isValidDatabaseType(sourceType)) { + plugin.getLocales().getLocale("error_invalid_database_type", sourceType) + .ifPresent(executor::sendMessage); + return; + } + if (!isValidDatabaseType(targetType)) { + plugin.getLocales().getLocale("error_invalid_database_type", targetType) + .ifPresent(executor::sendMessage); + return; + } + + // Compare normalized types (treat mariadb as mysql) + String normalizedSource = normalizeDbType(sourceType); + String normalizedTarget = normalizeDbType(targetType); + if (normalizedSource.equalsIgnoreCase(normalizedTarget)) { + plugin.getLocales().getLocale("error_same_database_type") + .ifPresent(executor::sendMessage); + return; + } + + // Get the database importer + final Optional optionalImporter = plugin.getImporterByName("database"); + if (optionalImporter.isEmpty()) { + plugin.getLocales().getLocale("error_importer_not_found", "database") + .ifPresent(executor::sendMessage); + return; + } + + // Set source and target database types and start import + final DatabaseImporter databaseImporter = (DatabaseImporter) optionalImporter.get(); + databaseImporter.setDatabaseTypes(sourceType, targetType); + databaseImporter.start(executor); + return; + } + + // Standard importer handling final Optional optionalImporter = parseStringArg(args, 0).flatMap(plugin::getImporterByName); if (optionalImporter.isEmpty()) { plugin.getLocales().getLocale("available_importers", plugin.getImporters().stream() @@ -213,6 +273,22 @@ private void handleImportCommand(@NotNull CommandUser executor, @NotNull String[ } } + /** + * Check if a database type is valid (mysql, mariadb, or sqlite) + */ + private boolean isValidDatabaseType(String dbType) { + return dbType.equalsIgnoreCase("mysql") || + dbType.equalsIgnoreCase("mariadb") || + dbType.equalsIgnoreCase("sqlite"); + } + + /** + * Normalize database type names (treats mariadb as mysql) + */ + private String normalizeDbType(String dbType) { + return dbType.equalsIgnoreCase("mariadb") ? "mysql" : dbType; + } + private void handleLogsCommand(@NotNull CommandUser executor, @NotNull String[] args) { final Optional optionalUser = parseStringArg(args, 0).flatMap(plugin.getDatabase()::getUser); if (optionalUser.isEmpty()) { diff --git a/common/src/main/java/net/william278/huskclaims/hook/DatabaseImporter.java b/common/src/main/java/net/william278/huskclaims/hook/DatabaseImporter.java new file mode 100644 index 00000000..41b02daf --- /dev/null +++ b/common/src/main/java/net/william278/huskclaims/hook/DatabaseImporter.java @@ -0,0 +1,331 @@ +/* + * This file is part of HuskClaims, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * Copyright (c) contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.william278.huskclaims.hook; + +import net.kyori.adventure.text.Component; +import net.william278.huskclaims.HuskClaims; +import net.william278.huskclaims.claim.ClaimWorld; +import net.william278.huskclaims.database.Database; +import net.william278.huskclaims.database.MySqlDatabase; +import net.william278.huskclaims.database.SqLiteDatabase; +import net.william278.huskclaims.position.ServerWorld; +import net.william278.huskclaims.trust.UserGroup; +import net.william278.huskclaims.user.CommandUser; +import net.william278.huskclaims.user.SavedUser; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.logging.Level; + +/** + * Importer for migrating between SQLite and MySQL databases + */ +@PluginHook( + name = "DatabaseImporter", + register = PluginHook.Register.ON_ENABLE +) +public class DatabaseImporter extends Importer { + + // Constants for database operations + private static final String SQLITE_DB_NAME = "HuskClaimsData.db"; + private static final String BACKUP_FOLDER = "backups"; + + // Database instance references + private Database sourceDatabase; + private Database targetDatabase; + + // Data caches for migration + private List allUsers; + private Map> allUserGroups; + private Map allClaimWorlds; + + // Database type configuration + private String sourceType; + private String targetType; + + // Counters for migration statistics + private int userCount = 0; + private int groupCount = 0; + private int claimWorldCount = 0; + + /** + * Create a new database importer + * + * @param plugin The HuskClaims plugin instance + */ + public DatabaseImporter(@NotNull HuskClaims plugin) { + super( + List.of(ImportData.USERS, ImportData.CLAIMS), + plugin, + Map.of(), + Map.of() + ); + } + + @Override + @NotNull + public String getName() { + return "database"; + } + + /** + * Set source and target database types for migration + * + * @param source The source database type (mysql/sqlite) + * @param target The target database type (mysql/sqlite) + */ + public void setDatabaseTypes(@NotNull String source, @NotNull String target) { + this.sourceType = source.toLowerCase(Locale.ENGLISH); + this.targetType = target.toLowerCase(Locale.ENGLISH); + } + + @Override + protected void prepare() { + validateDatabaseTypes(); + createBackup(); + setupDatabases(); + resetCounters(); + } + + /** + * Validates that the database types are properly configured for migration + * + * @throws IllegalArgumentException if the configuration is invalid + */ + private void validateDatabaseTypes() { + if (sourceType == null || targetType == null) { + throw new IllegalArgumentException("Source and target database types must be specified"); + } + + if (!isValidDatabaseType(sourceType)) { + throw new IllegalArgumentException("Source database type must be 'mysql', 'mariadb', or 'sqlite'"); + } + + if (!isValidDatabaseType(targetType)) { + throw new IllegalArgumentException("Target database type must be 'mysql', 'mariadb', or 'sqlite'"); + } + + // Consider mariadb same as mysql for comparison purposes + String normalizedSource = normalizeDbType(sourceType); + String normalizedTarget = normalizeDbType(targetType); + + if (normalizedSource.equalsIgnoreCase(normalizedTarget)) { + throw new IllegalArgumentException("Source and target database types must be different"); + } + } + + /** + * Normalizes database type names (treats mariadb as mysql) + */ + private String normalizeDbType(String dbType) { + return dbType.equalsIgnoreCase("mariadb") ? "mysql" : dbType; + } + + /** + * Checks if the database type is valid + */ + private boolean isValidDatabaseType(String dbType) { + return dbType.equalsIgnoreCase("mysql") || + dbType.equalsIgnoreCase("mariadb") || + dbType.equalsIgnoreCase("sqlite"); + } + + /** + * Creates a backup of the source database if it's SQLite + */ + private void createBackup() { + try { + // Only backup SQLite databases + if (!sourceType.equalsIgnoreCase("sqlite")) { + getPlugin().log(Level.INFO, "MySQL database should be backed up manually before migration"); + return; + } + + // Create backups directory if it doesn't exist + Path backupsDir = getPlugin().getConfigDirectory().resolve(BACKUP_FOLDER); + Files.createDirectories(backupsDir); + + // Create timestamp for the backup filename + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")); + + // Backup SQLite database file + Path sourceFile = getPlugin().getConfigDirectory().resolve(SQLITE_DB_NAME); + Path targetFile = backupsDir.resolve("HuskClaimsData_" + timestamp + ".db"); + Files.copy(sourceFile, targetFile); + getPlugin().log(Level.INFO, "Created SQLite database backup at " + targetFile); + } catch (Throwable e) { + getPlugin().log(Level.WARNING, "Failed to create database backup: " + e.getMessage(), e); + } + } + + /** + * Sets up the source and target databases for migration + * + * @throws IllegalStateException if database initialization fails + */ + private void setupDatabases() { + // Get source database (current database) + sourceDatabase = getPlugin().getDatabase(); + + // Create target database based on target type + String normalizedTargetType = normalizeDbType(targetType); + if (normalizedTargetType.equalsIgnoreCase("mysql")) { + targetDatabase = new MySqlDatabase(getPlugin()); + } else { + targetDatabase = new SqLiteDatabase(getPlugin()); + } + + // Initialize target database + targetDatabase.initialize(); + if (!targetDatabase.hasLoaded()) { + throw new IllegalStateException("Failed to initialize target database"); + } + } + + /** + * Reset statistical counters for a new migration + */ + private void resetCounters() { + userCount = 0; + groupCount = 0; + claimWorldCount = 0; + } + + @Override + protected int importData(@NotNull ImportData importData, @NotNull CommandUser executor) { + return switch (importData) { + case USERS -> importUserData(executor); + case CLAIMS -> importClaimData(executor); + default -> 0; + }; + } + + /** + * Import user data including saved users and user groups + * + * @param executor The command user executing the import + * @return The number of users imported + */ + private int importUserData(CommandUser executor) { + try { + // Get and import all users (including inactive) + allUsers = sourceDatabase.getInactiveUsers(-1); + + // Import users + for (SavedUser user : allUsers) { + targetDatabase.createOrUpdateUser(user); + userCount++; + } + + // Import user groups - doing it here ensures we have all users first + allUserGroups = sourceDatabase.getAllUserGroups(); + for (Map.Entry> entry : allUserGroups.entrySet()) { + for (UserGroup group : entry.getValue()) { + targetDatabase.addUserGroup(group); + groupCount++; + } + } + + return userCount; + } catch (Throwable e) { + getPlugin().log(Level.SEVERE, "Error migrating user data: " + e.getMessage(), e); + executor.sendMessage(Component.text("Error migrating user data: " + e.getMessage())); + throw e; + } + } + + /** + * Import claim data including claim worlds and all associated claim information + * + * @param executor The command user executing the import + * @return The number of non-user entries imported (groups + claim worlds) + */ + private int importClaimData(CommandUser executor) { + try { + // Get and import claim worlds with all their data + allClaimWorlds = sourceDatabase.getAllClaimWorlds(); + + // Import claim worlds and their data + for (Map.Entry entry : allClaimWorlds.entrySet()) { + ServerWorld serverWorld = entry.getKey(); + ClaimWorld claimWorld = entry.getValue(); + + // Log more details + int claimCount = claimWorld.getClaims().size(); + getPlugin().log(Level.INFO, String.format( + "Migrating world %s with %d claims", + serverWorld.toString(), + claimCount + )); + + // Ensure the target database has the claim world record + targetDatabase.updateClaimWorld(claimWorld); + claimWorldCount++; + } + + return groupCount + claimWorldCount; + } catch (Throwable e) { + getPlugin().log(Level.SEVERE, "Error migrating claim data: " + e.getMessage(), e); + executor.sendMessage(Component.text("Error migrating claim data: " + e.getMessage())); + throw e; + } + } + + @Override + protected void finish() { + try { + // Close the target database + if (targetDatabase != null) { + targetDatabase.close(); + } + + // Log summary of migration + getPlugin().log(Level.INFO, String.format( + "Migration summary: %d users, %d user groups, %d claim worlds with all associated data (regions, trusts, flags)", + userCount, + groupCount, + claimWorldCount + )); + + // Clear cached data + clearCache(); + + // Let the user know they need to update their config.yml to use the new database + getPlugin().log(Level.INFO, "Database migration completed. Please update your config.yml to use the new database type."); + } catch (Throwable e) { + getPlugin().log(Level.SEVERE, "Error during migration cleanup: " + e.getMessage(), e); + } + } + + /** + * Clear all cached data after migration completes + */ + private void clearCache() { + allUsers = null; + allUserGroups = null; + allClaimWorlds = null; + sourceType = null; + targetType = null; + } +} \ No newline at end of file diff --git a/common/src/main/resources/locales/en-gb.yml b/common/src/main/resources/locales/en-gb.yml index d6476aca..85dc9800 100644 --- a/common/src/main/resources/locales/en-gb.yml +++ b/common/src/main/resources/locales/en-gb.yml @@ -36,9 +36,13 @@ locales: error_cannot_ban_owner: '[Error:](#ff3300) [You cannot ban the owner of the claim.](#ff7e5e)' error_cannot_ban_self: '[Error:](#ff3300) [You cannot ban yourself from a claim.](#ff7e5e)' error_ban_list_empty: '[Error:](#ff3300) [No players have been banned from this claim.](#ff7e5e)' + error_days_restricted: '[Error:](#ff3300) [Claim pruning is restricted to a minimum of 1 day.](#ff7e5e)' error_not_trapped: '[Error:](#ff3300) [You can build here—find a way out by yourself!](#ff7e5e)' error_no_permission_flag: '[Error:](#ff3300) [You do not have permission to use the %1% flag.](#ff7e5e)' error_no_flags: '[Error:](#ff3300) [You don''t have any flags available.](#ff7e5e)' + error_invalid_database_type: '[Error:](#ff3300) ["%1%" is not a valid database type. Use "mysql", "mariadb", or "sqlite".](#ff7e5e)' + error_same_database_type: '[Error:](#ff3300) [Source and target database types cannot be the same.](#ff7e5e)' + error_importer_not_found: '[Error:](#ff3300) [%1% importer could not be found.](#ff7e5e)' group_list: '[List of your groups:](#00fb9a) [%1%](gray)' group_list_item: '[%1%](white show_text=&7Group of %2% players:\n&8 run_command=/group %1%)' group_member_list: '[List of group members of %1%:](#00fb9a) %2%' @@ -88,7 +92,6 @@ locales: claim_private_disabled: '[Successfully made this claim traversable to the public.](#00fb9a)' claim_enter_is_private: '[You cannot enter %1%''s claim as it has been made private.](#ff7e5e)' no_inspecting_permission: '[You do not have permission to inspect claims.](#ff7e5e)' - no_claiming_permission: '[You do not have permission to create or edit claims.](#ff7e5e)' no_claiming_child_permission: '[You do not have permission to create or edit child claims.](#ff7e5e)' no_resizing_permission: '[You do not have permission to resize this claim.](#ff7e5e)' no_resizing_child_permission: '[You do not have permission to resize this child claim.](#ff7e5e)' diff --git a/common/src/main/resources/locales/it-it.yml b/common/src/main/resources/locales/it-it.yml index da8dcab1..64e5f390 100644 --- a/common/src/main/resources/locales/it-it.yml +++ b/common/src/main/resources/locales/it-it.yml @@ -36,9 +36,10 @@ locales: error_cannot_ban_owner: '[Errore:](#ff3300) [Non puoi bannare il proprietario del claim.](#ff7e5e)' error_cannot_ban_self: '[Errore:](#ff3300) [Non puoi bannare te stesso da un claim.](#ff7e5e)' error_ban_list_empty: '[Errore:](#ff3300) [Nessun giocatore è stato bannato da questo claim.](#ff7e5e)' - error_not_trapped: '[Errore:](#ff3300) [Puoi costruire qui—trova un modo per uscire da solo!](#ff7e5e)' + error_days_restricted: '[Errore:](#ff3300) [La pulizia dei claim è limitata a un minimo di 1 giorno.](#ff7e5e)' + error_not_trapped: '[Errore:](#ff3300) [Puoi costruire qui—trova una via d''uscita da solo!](#ff7e5e)' error_no_permission_flag: '[Error:](#ff3300) [You do not have permission to use the %1% flag.](#ff7e5e)' - error_no_flags: '[Error:](#ff3300) [You don''t have any flags available.](#ff7e5e)' + error_no_flags: '[Error:](#ff3300) [Non hai nessun flag disponibile.](#ff7e5e)' group_list: '[Lista dei tuoi gruppi:](#00fb9a) [%1%](gray)' group_list_item: '[%1%](white show_text=&7Gruppo di %2% giocatori:\n&8 run_command=/group %1%)' group_member_list: '[Lista dei membri del gruppo %1%:](#00fb9a) %2%' @@ -88,7 +89,6 @@ locales: claim_private_disabled: '[Claim reso accessibile al pubblico con successo.](#00fb9a)' claim_enter_is_private: '[Non puoi entrare nel claim di %1% poiché è stato reso privato.](#ff7e5e)' no_inspecting_permission: '[Non hai il permesso di ispezionare i claim.](#ff7e5e)' - no_claiming_permission: '[Non hai il permesso di creare o modificare claim.](#ff7e5e)' no_claiming_child_permission: '[Non hai il permesso di creare o modificare claim figlio.](#ff7e5e)' no_resizing_permission: '[Non hai il permesso di ridimensionare questo claim.](#ff7e5e)' no_resizing_child_permission: '[Non hai il permesso di ridimensionare questo claim figlio.](#ff7e5e)' @@ -234,5 +234,8 @@ locales: transferclaim_command_description: 'Trasferisci la proprietà del claim in cui ti trovi.' unclaimall_command_description: 'Elimina tutti i tuoi claim.' transferpet_command_description: 'Trasferisci la proprietà di un animale addomesticato.' - claimban_command_description: 'Banna un giocatore da un claim.' - trapped_command_description: 'Teletrasportati fuori da un claim in cui sei bloccato.' \ No newline at end of file + claimban_command_description: 'Bandisci un giocatore da una rivendicazione.' + trapped_command_description: 'Teletrasportati fuori da una rivendicazione in cui sei intrappolato.' + error_invalid_database_type: '[Error:](#ff3300) ["%1%" non è un tipo di database valido. Usa "mysql", "mariadb", o "sqlite".](#ff7e5e)' + error_same_database_type: '[Error:](#ff3300) [Il tipo di database di origine e di destinazione non possono essere gli stessi.](#ff7e5e)' + error_importer_not_found: '[Error:](#ff3300) [Non è stato possibile trovare l''importatore %1%.](#ff7e5e)' \ No newline at end of file diff --git a/common/src/main/resources/locales/ro-ro.yml b/common/src/main/resources/locales/ro-ro.yml index fca5c64f..c7cba145 100644 --- a/common/src/main/resources/locales/ro-ro.yml +++ b/common/src/main/resources/locales/ro-ro.yml @@ -35,10 +35,11 @@ locales: error_user_not_banned: '[Error:](#ff3300) [%1% is not banned from this claim.](#ff7e5e)' error_cannot_ban_owner: '[Error:](#ff3300) [You cannot ban the owner of the claim.](#ff7e5e)' error_cannot_ban_self: '[Error:](#ff3300) [You cannot ban yourself from a claim.](#ff7e5e)' - error_ban_list_empty: '[Error:](#ff3300) [No players have been banned from this claim.](#ff7e5e)' - error_not_trapped: '[Error:](#ff3300) [You can build here—find a way out by yourself!](#ff7e5e)' + error_ban_list_empty: '[Eroare:](#ff3300) [Nu sunt jucători interziși în această revendicare.](#ff7e5e)' + error_days_restricted: '[Eroare:](#ff3300) [Curățarea revendicărilor este restricționată la minimum 1 zi.](#ff7e5e)' + error_not_trapped: '[Eroare:](#ff3300) [Poți construi aici—găsește singur o cale de ieșire!](#ff7e5e)' error_no_permission_flag: '[Error:](#ff3300) [You do not have permission to use the %1% flag.](#ff7e5e)' - error_no_flags: '[Error:](#ff3300) [You don''t have any flags available.](#ff7e5e)' + error_no_flags: '[Eroare:](#ff3300) [Nu ai niciun flag disponibil.](#ff7e5e)' group_list: '[Lista grupurilor tale:](#00fb9a) [%1%](gray)' group_list_item: '[%1%](white show_text=&7Grup de %2% jucători:\n&8 run_command=/group %1%)' group_member_list: '[Lista membrilor grupului %1%:](#00fb9a) %2%' @@ -86,10 +87,9 @@ locales: user_banned_you: '[You have been banned from %1%''s claim.](#ff7e5e)' claim_private_enabled: '[Successfully made this claim private.](#00fb9a)' claim_private_disabled: '[Successfully made this claim traversable to the public.](#00fb9a)' - claim_enter_is_private: '[You cannot enter %1%''s claim as it has been made private.](#ff7e5e)' + claim_enter_is_private: '[Nu poți intra în revendicarea lui %1% deoarece a fost făcută privată.](#ff7e5e)' no_inspecting_permission: '[Nu ai permisiunea de a inspecta revendicări.](#ff7e5e)' - no_claiming_permission: '[Nu ai permisiunea de a crea sau edita revendicări.](#ff7e5e)' - no_claiming_child_permission: '[Nu ai permisiunea de a to crea sau edita revendicări fiu.](#ff7e5e)' + no_claiming_child_permission: '[Nu ai permisiunea de a crea sau edita revendicări secundare.](#ff7e5e)' no_resizing_permission: '[Nu ai permisiunea de a redimensiona această revendicare.](#ff7e5e)' no_resizing_child_permission: '[Nu ai permisiunea de a redimensiona această revendicare fiu.](#ff7e5e)' no_claim_privilege: '[Nu ai permisiunea de a face asta în această revendicare.](#ff7e5e)' @@ -234,5 +234,8 @@ locales: transferclaim_command_description: 'Transfer ownership of the claim you''re standing in.' unclaimall_command_description: 'Delete all of your claims.' transferpet_command_description: 'Transfer ownership of a tamed animal.' - claimban_command_description: 'Ban a player from a claim.' - trapped_command_description: 'Teleport out of a claim you''re trapped in.' \ No newline at end of file + claimban_command_description: 'Banează un jucător dintr-o revendicare.' + trapped_command_description: 'Teleportează-te afară dintr-o revendicare în care ești prins.' + error_invalid_database_type: '[Eroare:](#ff3300) ["%1%" nu este un tip de bază de date valid. Folosește "mysql", "mariadb", sau "sqlite".](#ff7e5e)' + error_same_database_type: '[Eroare:](#ff3300) [Tipurile de bază de date sursă și destinație nu pot fi identice.](#ff7e5e)' + error_importer_not_found: '[Eroare:](#ff3300) [Importatorul %1% nu a putut fi găsit.](#ff7e5e)' \ No newline at end of file diff --git a/common/src/main/resources/locales/ru-ru.yml b/common/src/main/resources/locales/ru-ru.yml index 0ee30170..4afb7b75 100644 --- a/common/src/main/resources/locales/ru-ru.yml +++ b/common/src/main/resources/locales/ru-ru.yml @@ -36,6 +36,7 @@ locales: error_cannot_ban_owner: '[Ошибка:](#ff3300) [Вы не можете заблокировать владельца привата.](#ff7e5e)' error_cannot_ban_self: '[Ошибка:](#ff3300) [Вы не можете заблокировать себя в привате.](#ff7e5e)' error_ban_list_empty: '[Ошибка:](#ff3300) [В этом привате нет заблокированных игроков.](#ff7e5e)' + error_days_restricted: '[Ошибка:](#ff3300) [Удаление неактивных приватов ограничено минимальным периодом в 1 день.](#ff7e5e)' error_not_trapped: '[Ошибка:](#ff3300) [Вы можете строить здесь — найдите выход самостоятельно!](#ff7e5e)' error_no_permission_flag: '[Ошибка:](#ff3300) [У вас нет доступа к флагу %1%.](#ff7e5e)' error_no_flags: '[Ошибка:](#ff3300) [У вас нет доступных флагов.](#ff7e5e)' @@ -88,7 +89,6 @@ locales: claim_private_disabled: '[Приват успешно открыт для посещения.](#00fb9a)' claim_enter_is_private: '[Вы не можете войти в приват игрока %1%, так как он закрыт для посещения.](#ff7e5e)' no_inspecting_permission: '[У вас нет доступа на просмотр приватов.](#ff7e5e)' - no_claiming_permission: '[У вас нет прав на создание или редактирование приватов.](#ff7e5e)' no_claiming_child_permission: '[У вас нет прав на создание или редактирование потомственных приватов.](#ff7e5e)' no_resizing_permission: '[У вас нет прав на масштабирование этого привата.](#ff7e5e)' no_resizing_child_permission: '[У вас нет прав на масштабирование этого потомственного привата.](#ff7e5e)' @@ -234,6 +234,9 @@ locales: transferclaim_command_description: 'Передать владение приватом.' unclaimall_command_description: 'Удалить все свои приваты.' transferpet_command_description: 'Передать владение питомцем.' - claimban_command_description: 'Заблокировать игрока в привате.' - trapped_command_description: 'Телепортироваться из привата.' - giftclaimblocks_command_description: 'Подарить блоки привата другому игроку.' \ No newline at end of file + claimban_command_description: 'Заблокировать игрока из владения.' + trapped_command_description: 'Телепортироваться из владения, в котором вы оказались в ловушке.' + giftclaimblocks_command_description: 'Подарить блоки привата другому игроку.' + error_invalid_database_type: '[Ошибка:](#ff3300) ["%1%" не является допустимым типом базы данных. Используйте "mysql", "mariadb" или "sqlite".](#ff7e5e)' + error_same_database_type: '[Ошибка:](#ff3300) [Исходный и целевой типы баз данных не могут быть одинаковыми.](#ff7e5e)' + error_importer_not_found: '[Ошибка:](#ff3300) [Импортер %1% не найден.](#ff7e5e)' \ No newline at end of file diff --git a/common/src/main/resources/locales/zh-cn.yml b/common/src/main/resources/locales/zh-cn.yml index d3257313..e064821e 100644 --- a/common/src/main/resources/locales/zh-cn.yml +++ b/common/src/main/resources/locales/zh-cn.yml @@ -35,11 +35,12 @@ locales: error_user_not_banned: '[错误:](#ff3300) [%1% 尚未被此领地封禁.](#ff7e5e)' error_cannot_ban_owner: '[错误:](#ff3300) [你不能封禁领地拥有者.](#ff7e5e)' error_cannot_ban_self: '[错误:](#ff3300) [你不能将自己从该领地中封禁.](#ff7e5e)' - error_ban_list_empty: '[错误:](#ff3300) [无玩家被该领地封禁.](#ff7e5e)' - error_not_trapped: '[错误:](#ff3300) [你可以在这里破坏方块—还是找找路自己出去吧!](#ff7e5e)' + error_ban_list_empty: '[错误:](#ff3300) [没有玩家被这个领地封禁。](#ff7e5e)' + error_days_restricted: '[错误:](#ff3300) [领地清理限制为最少1天。](#ff7e5e)' + error_not_trapped: '[错误:](#ff3300) [你可以在这里建造 - 自己找出路!](#ff7e5e)' error_no_permission_flag: '[Error:](#ff3300) [您没有使用 %1% 标志的权限.](#ff7e5e)' - error_no_flags: '[Error:](#ff3300) [您没有任何可用的标志.](#ff7e5e)' - group_list: '[创建的组列表:](#00fb9a) [%1%](gray)' + error_no_flags: '[错误:](#ff3300) [您没有可用的标志.](#ff7e5e)' + group_list: '[您的组列表:](#00fb9a) [%1%](gray)' group_list_item: '[%1%](white show_text=&7组 %2% 中的玩家:\n&8 run_command=/group %1%)' group_member_list: '[组 %1% 中的玩家:](#00fb9a) %2%' group_added_player: '[成功将玩家 %1% 移动至组 %2% 中.](#00fb9a)' @@ -86,10 +87,9 @@ locales: user_banned_you: '[你已被 %1% 的领地封禁.](#ff7e5e)' claim_private_enabled: '[成功将领地状态设置为私有.](#00fb9a)' claim_private_disabled: '[成功将领地状态设置为开放.](#00fb9a)' - claim_enter_is_private: '[你不能进入 %1% 的私有领地.](#ff7e5e)' - no_inspecting_permission: '[你没有权限查看领地.](#ff7e5e)' - no_claiming_permission: '[你没有权限创建或编辑领地.](#ff7e5e)' - no_claiming_child_permission: '[你没有权限创建或编辑子领地.](#ff7e5e)' + claim_enter_is_private: '[你不能进入 %1% 的领地,因为它是私人的。](#ff7e5e)' + no_inspecting_permission: '[你没有权限检查领地。](#ff7e5e)' + no_claiming_child_permission: '[你没有权限创建或编辑子领地。](#ff7e5e)' no_resizing_permission: '[你没有权限重设领地大小.](#ff7e5e)' no_resizing_child_permission: '[你没有权限重设子领地.](#ff7e5e)' no_claim_privilege: '[你没有权限在领地内这么做.](#ff7e5e)' @@ -234,5 +234,8 @@ locales: transferclaim_command_description: '转让所处领地.' unclaimall_command_description: '删除所有领地.' transferpet_command_description: '转让驯服动物.' - claimban_command_description: '将指定玩家从领地中封禁.' - trapped_command_description: '传送出无法脱身的领地.' + claimban_command_description: '将玩家禁止进入领地.' + trapped_command_description: '从被困的领地内传送出来.' + error_invalid_database_type: '[错误:](#ff3300) ["%1%" 不是有效的数据库类型。请使用 "mysql"、"mariadb" 或 "sqlite"。](#ff7e5e)' + error_same_database_type: '[错误:](#ff3300) [源数据库类型和目标数据库类型不能相同。](#ff7e5e)' + error_importer_not_found: '[错误:](#ff3300) [找不到 %1% 导入器。](#ff7e5e)' diff --git a/common/src/main/resources/locales/zh-tw.yml b/common/src/main/resources/locales/zh-tw.yml index c59c0fbc..8e1cc0b4 100644 --- a/common/src/main/resources/locales/zh-tw.yml +++ b/common/src/main/resources/locales/zh-tw.yml @@ -35,16 +35,17 @@ locales: error_user_not_banned: '[錯誤:](#ff3300) [尚未禁止 %1% 進入此領地。](#ff7e5e)' error_cannot_ban_owner: '[錯誤:](#ff3300) [您無法禁止領地的擁有者進入。](#ff7e5e)' error_cannot_ban_self: '[錯誤:](#ff3300) [您無法禁止自己進入領地。](#ff7e5e)' - error_ban_list_empty: '[錯誤:](#ff3300) [沒有玩家被禁止進入此領地。](#ff7e5e)' - error_not_trapped: '[Error:](#ff3300) [You can build here—find a way out by yourself!](#ff7e5e)' + error_ban_list_empty: '[錯誤:](#ff3300) [此領地中沒有被封禁的玩家.](#ff7e5e)' + error_days_restricted: '[錯誤:](#ff3300) [領地清理設置為最少1天.](#ff7e5e)' + error_not_trapped: '[錯誤:](#ff3300) [你可以在這裡建造—自己找出路吧!](#ff7e5e)' error_no_permission_flag: '[Error:](#ff3300) [You do not have permission to use the %1% flag.](#ff7e5e)' - error_no_flags: '[Error:](#ff3300) [You don''t have any flags available.](#ff7e5e)' - group_list: '[您的群組清單:](#00fb9a) [%1%](gray)' + error_no_flags: '[錯誤:](#ff3300) [您沒有可用的標誌.](#ff7e5e)' + group_list: '[您的組列表:](#00fb9a) [%1%](gray)' group_list_item: '[%1%](white show_text=&7%2% 位玩家的群組:\n&8 run_command=/group %1%)' group_member_list: '[%1% 的群組成員清單:](#00fb9a) %2%' group_added_player: '[已成功將 %1% 新增到您的 %2% 群組中。](#00fb9a)' group_removed_player: '[已成功將 %1% 從您的 %2% 群組中移除。](#00fb9a)' - group_created: '[已建立群組 %1%,成員如下:](#00fb9a) [%2%](#00fb9a show_text=�fb9a&點選以檢視群組 run_command=/group %1%)' + group_created: '[已建立群組 %1%,成員如下:](#00fb9a) [%2%](#00fb9a show_text=&fb9a&點選以檢視群組 run_command=/group %1%)' group_deleted: '[已成功刪除您的 %1% 群組。](#00fb9a)\n[成員將失去對已設定此群組信任級別的任何領地的權限。](gray)' world_not_claimable: '[此世界中已停用領地功能。](#ff7e5e)' region_outside_world_limits: '[您無法在世界限制外領地土地。](#ff7e5e)' @@ -86,10 +87,9 @@ locales: user_banned_you: '[您已被禁止進入 %1% 的領地。](#ff7e5e)' claim_private_enabled: '[Successfully made this claim private.](#00fb9a)' claim_private_disabled: '[Successfully made this claim traversable to the public.](#00fb9a)' - claim_enter_is_private: '[You cannot enter %1%''s claim as it has been made private.](#ff7e5e)' - no_inspecting_permission: '[您沒有權限檢查領地。](#ff7e5e)' - no_claiming_permission: '[您沒有權限建立或編輯領地。](#ff7e5e)' - no_claiming_child_permission: '[您沒有權限建立或編輯子領地。](#ff7e5e)' + claim_enter_is_private: '[你不能進入 %1% 的領地,因為它是私人的.](#ff7e5e)' + no_inspecting_permission: '[你沒有權限檢查領地.](#ff7e5e)' + no_claiming_child_permission: '[你沒有權限創建或編輯子領地.](#ff7e5e)' no_resizing_permission: '[您沒有權限調整此領地的大小。](#ff7e5e)' no_resizing_child_permission: '[您沒有權限調整此子領地的大小。](#ff7e5e)' no_claim_privilege: '[您沒有權限在此領地中執行該操作。](#ff7e5e)' @@ -101,8 +101,8 @@ locales: enabled_operation_group: '[已成功在此領地中啟用 %1%。](#00fb9a)' disabled_operation_group: '[已成功在此領地中停用 %1%。](#00fb9a)' trust_list_header: '[具有權限的受託人:](#00fb9a) %1%[:](#00fb9a)\n' - trust_list_claim: '[%1% 的領地](#00fb9a italic show_text=�fb9a&■ %2% 個方塊(%3%×%4%)\n&#faffde&♢ %5% 個子領地\n&#ff8cd3&☻ %6% 位受託人\n&7⌚ 建立於 %7%)' - trust_list_child_claim: '[%1% 的領地的子領地](#00fb9a italic show_text=�fb9a&■ %2% 個方塊(%3%×%4%)\n&#ff8cd3&☻ %5% 位受託人\n&#faffde&▼ 信任繼承:%6%\n&7⌚ 建立於 %7%)' + trust_list_claim: '[%1% 的領地](#00fb9a italic show_text=&fb9a&■ %2% 個方塊(%3%×%4%)\n&#faffde&♢ %5% 個子領地\n&#ff8cd3&☻ %6% 位受託人\n&7⌚ 建立於 %7%)' + trust_list_child_claim: '[%1% 的領地的子領地](#00fb9a italic show_text=&fb9a&■ %2% 個方塊(%3%×%4%)\n&#ff8cd3&☻ %5% 位受託人\n&#faffde&▼ 信任繼承:%6%\n&7⌚ 建立於 %7%)' trust_list_row: '[●](%1% show_text=&%1%&%2%\n&7%3%) %4%' trust_list_key: '\n[圖例:](gray) %1%' trust_list_key_level: '[[%1%]](%2% show_text=&%2%&● %1%\n&7%3% suggest_command=/%4% )' @@ -114,7 +114,7 @@ locales: ban_list_header: '[🔨 %1% 中被禁止的玩家:](#00fb9a)\n' ban_list_claim: '[%1% 的領地](#00fb9a italic)' ban_list_child_claim: '[%1% 的領地的子領地](#00fb9a italic)' - ban_list_item: '[%1%](white show_text=&7禁止者:&f%2%\n�fb9a&🔨 點選以解除禁止 suggest_command=/claimban unban %1%)' + ban_list_item: '[%1%](white show_text=&7禁止者:&f%2%\n&fb9a&🔨 點選以解除禁止 suggest_command=/claimban unban %1%)' days_since_last_login: '[上次上線時間為 %1% 天前。](gray)' today_last_login: '[今天早些時候上線。](gray)' claim_blocks_updated: '[已將 %1% 的領地方塊餘額設定為 %2%](#00fb9a)' @@ -127,7 +127,7 @@ locales: claim_block_balance_accrued: '[= %1% 個已累積的領地方塊總數](gray show_text=&7從上述項目中累積的領地方塊總數)' claim_block_balance_deducted: '[- %1% 個已扣除的領地方塊](#ff7e5e show_text=&7工作人員扣除的領地方塊)' claim_block_balance_spent: '[- %1% 個已花費的領地方塊](#ff7e5e show_text=&7花費在領地上的領地方塊) [(在 %2% 個領地上)](#ff7e5e italic show_text=&7點選以檢視領地清單 run_command=/claimslist %3%)\n' - claim_block_balance_available: '[■ %1% 個可花費的領地方塊](#00fb9a show_text=�fb9a&可花費的領地方塊數量。\n&7領地方塊餘額不能為負數。)' + claim_block_balance_available: '[■ %1% 個可花費的領地方塊](#00fb9a show_text=&fb9a&可花費的領地方塊數量。\n&7領地方塊餘額不能為負數。)' child_claims_inherit: '[您所在位置的子領地不受限制。](#00fb9a)\n[它現在將從其父領地繼承信任權限。](gray)' child_claims_do_not_inherit: '[您所在位置的子領地受到限制。](#00fb9a)\n[它不會從其父領地繼承信任權限。](gray)' child_claims_inherit_restricted: '受限' @@ -147,13 +147,13 @@ locales: claim_list_sort_options: '[(](white)%1%[)](white)' claim_list_sort_option_separator: '[/](white)' claim_list_sort_option: '[%1%](white show_text=&7點選以 %1% 排序 run_command=/%2% %3% %4% %5%)' - claim_list_sort_option_selected: '[%1%](#00fb9a underlined show_text=�fb9a&以 %1% 排序)' + claim_list_sort_option_selected: '[%1%](#00fb9a underlined show_text=&fb9a&以 %1% 排序)' claim_list_sort_option_label_size: '大小' claim_list_sort_option_label_world: '世界' claim_list_sort_option_label_trustees: '受託人' claim_list_sort_option_label_children: '子領地' - claim_list_sort_ascending: '[▲](#00fb9a show_text=�fb9a&以升序排序 run_command=/%1% %2% descending %3%)' - claim_list_sort_descending: '[▼](#00fb9a show_text=�fb9a&以降序排序 run_command=/%1% %2% ascending %3%)' + claim_list_sort_ascending: '[▲](#00fb9a show_text=&fb9a&以升序排序 run_command=/%1% %2% descending %3%)' + claim_list_sort_descending: '[▼](#00fb9a show_text=&fb9a&以降序排序 run_command=/%1% %2% ascending %3%)' claim_list_item_separator: ' ' claim_list_blocks: '[■ %1%(%2%×%3%)](#00fb9a show_text=&7已使用的領地方塊)' claim_list_children: '[♢ %1%](#faffde show_text=&7包含的子領地數量)' @@ -162,7 +162,7 @@ locales: position_nether: '[🔥 %1%(%2%, %3%)](#ff304f show_text=&7%4%\n&8🔥 在地獄%5%)' position_end: '[🗡 %1%(%2%, %3%)](#f3f59f show_text=&7%4%\n&8🗡 在終界%5%)' position_custom: '[🗡 %1% (%2%, %3%)](#96e640 show_text=&7%4%\n&8⛏ In %6%%5%)' - position_teleport_tooltip: '\n�fb9a&⏩ 點選以傳送' + position_teleport_tooltip: '\n&fb9a&⏩ 點選以傳送' position_world_tooltip: '世界和座標' position_server_world_tooltip: '伺服器/世界和座標' list_item_divider: ' [•](gray) ' @@ -205,7 +205,7 @@ locales: system_dump_ready: '[HuskClaims](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)' available_importers: '[HuskClaims](#00fb9a bold) [| 可用的匯入器:](#00fb9a) [%1%](white)' command_list_header: '[HuskClaims](#00fb9a bold) [| 指令清單:](#00fb9a)\n' - command_list_row: '[%1%](#00fb9a italic show_text=�fb9a&%1%\n&7%2% suggest_command=%1%) [%3%](gray show_text=&7%4% suggest_command=%1%)' + command_list_row: '[%1%](#00fb9a italic show_text=&fb9a&%1%\n&7%2% suggest_command=%1%) [%3%](gray show_text=&7%4% suggest_command=%1%)' audit_log_header: '[HuskClaims](#00fb9a bold) [| %1% 的審計記錄:](#00fb9a)\n' audit_log_row: '[%1%](gray) [%2%](#00fb9a) [%3%](#ff304f) [%4%](gray)' claim_flags_header: '[%1% 的領地旗標:](#00fb9a)\n' @@ -234,5 +234,8 @@ locales: transferclaim_command_description: '轉讓您所站立的領地的所有權。' unclaimall_command_description: '刪除您所有的領地。' transferpet_command_description: '轉讓馴服動物的所有權。' - claimban_command_description: '禁止玩家進入領地。' - trapped_command_description: 'Teleport out of a claim you''re trapped in.' \ No newline at end of file + claimban_command_description: '禁止玩家進入領地.' + trapped_command_description: '從被困的領地傳送出來.' + error_invalid_database_type: '[錯誤:](#ff3300) ["%1%" 不是有效的資料庫類型。請使用 "mysql"、"mariadb" 或 "sqlite"。](#ff7e5e)' + error_same_database_type: '[錯誤:](#ff3300) [來源資料庫類型和目標資料庫類型不能相同。](#ff7e5e)' + error_importer_not_found: '[錯誤:](#ff3300) [找不到 %1% 匯入器。](#ff7e5e)' \ No newline at end of file diff --git a/docs/Commands.md b/docs/Commands.md index fa414581..3aed61ae 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -427,6 +427,12 @@ This is a table of HuskClaims commands, how to use them, their required permissi huskclaims.command.huskclaims.import ❌ + + /huskclaims import database <source> <destination> + Import claim data between different database types (mysql/sqlite) + huskclaims.command.huskclaims.import + ❌ + /huskclaims teleport [coordinates] Teleport to a claim at a position. Requires the HuskHomes hook to use. diff --git a/docs/Database-Migration.md b/docs/Database-Migration.md new file mode 100644 index 00000000..fd7b5fad --- /dev/null +++ b/docs/Database-Migration.md @@ -0,0 +1,52 @@ +HuskClaims supports migrating data between different database types through the `/huskclaims import database` command. + +## Migrating Between Database Types + +You can seamlessly migrate all your claim data between MySQL, MariaDB, and SQLite databases with a simple command. + +### Prerequisites + +1. Ensure both database configurations are properly set in your `config.yml` file: + - For MySQL/MariaDB migration, make sure your database credentials are correctly configured + - For SQLite migration, the plugin will use the default file location + +### Migration Process + +To migrate data between database types, use the following command: + +``` +/huskclaims import database +``` + +Where: +- ``: Current database type (mysql/mariadb/sqlite) +- ``: Target database type (mysql/mariadb/sqlite) + +### Examples + +#### Migrating from MySQL to SQLite + +``` +/huskclaims import database mysql sqlite +``` + +#### Migrating from SQLite to MySQL + +``` +/huskclaims import database sqlite mysql +``` + +#### Migrating from SQLite to MariaDB + +``` +/huskclaims import database sqlite mariadb +``` + +### Troubleshooting + +If you encounter issues during migration: + +1. Check that both database configurations are correct in your `config.yml` +2. Ensure you have sufficient disk space for backups and new database files +3. Verify that your MySQL/MariaDB server is properly configured and accessible +4. Review the server logs for detailed error information \ No newline at end of file diff --git a/docs/Importers.md b/docs/Importers.md index 1342528c..f96e0f7e 100644 --- a/docs/Importers.md +++ b/docs/Importers.md @@ -3,6 +3,7 @@ HuskClaims supports importing data from other plugins through the `/huskclaims i | Name | Supported Import Data | Link | |-------------------------------------|-----------------------|------------------------------| | [GriefPrevention](#griefprevention) | Claims, Users | https://griefprevention.com/ | +| [Database Migration](Database-Migration.md) | Claims, Users, Groups | Internal Migration Tool | ## GriefPrevention HuskClaims supports importing Claims, Trust and User data from [GriefPrevention](https://griefprevention.com/) (`v16.18.1`+). diff --git a/fabric/src/main/java/net/william278/huskclaims/hook/FabricHookProvider.java b/fabric/src/main/java/net/william278/huskclaims/hook/FabricHookProvider.java index a7120b44..04b9123c 100644 --- a/fabric/src/main/java/net/william278/huskclaims/hook/FabricHookProvider.java +++ b/fabric/src/main/java/net/william278/huskclaims/hook/FabricHookProvider.java @@ -39,6 +39,9 @@ default List getAvailableHooks() { hooks.add(new FabricHuskHomesHook(getPlugin())); } + // Add database importer + hooks.add(new DatabaseImporter(getPlugin())); + return hooks; }