diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml new file mode 100644 index 0000000..0876b20 --- /dev/null +++ b/.github/workflows/preview-release.yml @@ -0,0 +1,53 @@ +name: Preview Release + +on: + push: + branches: + - dev # Trigger on push to dev branch + +jobs: + build: + runs-on: ubuntu-latest + + steps: + # Step 1: Checkout code + - name: Checkout Repository + uses: actions/checkout@v4 + + # Step 2: Setup Java (required for building the plugin) + - name: Set up Java + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + # Step 3: Build the plugin with Maven + - name: Build Plugin + run: mvn clean package -DskipTests + + # Step 4: Create a preview release + - name: Create Preview Release + uses: softprops/action-gh-release@v2 + with: + tag_name: preview-${{ github.run_number }} + name: "Preview Build #${{ github.run_number }}" + body: | + 🚀 Automatic Preview Build + Commit: ${{ github.sha }} + Branch: ${{ github.ref_name }} + prerelease: true + files: target/*.jar + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Step 5: Cleanup old preview releases (keep only 10 newest) + - name: Cleanup old Preview Releases + run: | + echo "Deleting old Preview Releases, keeping only the 10 newest..." + gh release list --limit 100 --repo $GITHUB_REPOSITORY \ + | grep preview \ + | sort -rk2 \ + | awk 'NR>10 {print $1}' \ + | xargs -I {} gh release delete {} --repo $GITHUB_REPOSITORY --yes + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-on-merge.yml b/.github/workflows/release-on-merge.yml new file mode 100644 index 0000000..8a1c2f5 --- /dev/null +++ b/.github/workflows/release-on-merge.yml @@ -0,0 +1,47 @@ +name: Release on Merge to Master + +on: + push: + branches: + - master # oder "main", je nach Repo + paths-ignore: + - '**.md' # ignoriert reine Dokumentationsänderungen + - '.github/**' # ignoriert Änderungen an Actions selbst + +jobs: + build-and-release: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Build with Maven + run: mvn clean package -DskipTests + + - name: Get version + id: get_version + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.get_version.outputs.version }} + name: "Release v${{ steps.get_version.outputs.version }}" + body: | + **v${{ steps.get_version.outputs.version }}** + - Commit: ${{ github.sha }} + - Branch: ${{ github.ref_name }} + - Version: v${{ steps.get_version.outputs.version }} + files: | + target/*.jar + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4788b4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,113 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +target/ + +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next + +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +.flattened-pom.xml + +# Common working directory +run/ diff --git a/LICENSE b/LICENSE index 2c87bac..b9cd78d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,18 +1,21 @@ MIT License -Copyright (c) 2025 deutschich +Copyright (c) 2025 D.L. -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..70a6ae3 --- /dev/null +++ b/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + com.user404_ + BalSync + 1.0 + jar + + BalSync + + + 21 + UTF-8 + + + + clean package + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.3 + + + package + + shade + + + + + + + + src/main/resources + true + + + + + + + papermc-repo + https://repo.papermc.io/repository/maven-public/ + + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + + jitpack.io + https://jitpack.io + + + + + + io.papermc.paper + paper-api + 1.21-R0.1-SNAPSHOT + provided + + + + com.github.MilkBowl + VaultAPI + 1.7 + provided + + + + mysql + mysql-connector-java + 8.0.33 + + + + com.zaxxer + HikariCP + 5.0.1 + + + diff --git a/src/main/java/com/user404_/balsync/BalSyncCommand.java b/src/main/java/com/user404_/balsync/BalSyncCommand.java new file mode 100644 index 0000000..eceea7d --- /dev/null +++ b/src/main/java/com/user404_/balsync/BalSyncCommand.java @@ -0,0 +1,69 @@ +package com.user404_.balsync; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class BalSyncCommand implements CommandExecutor { + private final BalSyncPlugin plugin; + private final BalanceManager balanceManager; + + public BalSyncCommand(BalSyncPlugin plugin, BalanceManager balanceManager) { + this.plugin = plugin; + this.balanceManager = balanceManager; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission("balsync.admin")) { + sender.sendMessage(plugin.getTranslationManager().formatMessage("no-permission")); + return true; + } + + if (args.length == 0) { + sender.sendMessage(plugin.getTranslationManager().formatMessage("usage")); + return true; + } + + switch (args[0].toLowerCase()) { + case "reload": + plugin.getConfigManager().reload(); + plugin.getTranslationManager().loadMessages(); + sender.sendMessage(plugin.getTranslationManager().formatMessage("config-reloaded")); + break; + + case "save": + // Save all balances - Methode ist jetzt in BalanceManager + balanceManager.saveAllBalances(); + sender.sendMessage(plugin.getTranslationManager().formatMessage("balance-saved")); + break; + + case "load": + if (sender instanceof Player) { + balanceManager.loadPlayerBalance((Player) sender); + } else { + sender.sendMessage(plugin.getTranslationManager().formatMessage("player-not-found")); + } + break; + + case "status": + sendStatusInfo(sender); + break; + + default: + sender.sendMessage(plugin.getTranslationManager().formatMessage("usage")); + break; + } + + return true; + } + + private void sendStatusInfo(CommandSender sender) { + sender.sendMessage("§6=== BalSync Status ==="); + sender.sendMessage("§7Auto-save interval: §e" + plugin.getConfigManager().getAutoSaveInterval() + "s"); + sender.sendMessage("§7Database polling: §e" + plugin.getConfigManager().getDbPollInterval() + "s"); + sender.sendMessage("§7Reset on join: §e" + plugin.getConfigManager().isResetOnJoin()); + sender.sendMessage("§7Offline monitoring: §e" + plugin.getConfigManager().monitorOfflineChanges()); + } +} \ No newline at end of file diff --git a/src/main/java/com/user404_/balsync/BalSyncPlugin.java b/src/main/java/com/user404_/balsync/BalSyncPlugin.java new file mode 100644 index 0000000..2301bcd --- /dev/null +++ b/src/main/java/com/user404_/balsync/BalSyncPlugin.java @@ -0,0 +1,123 @@ +package com.user404_.balsync; + +import org.bukkit.plugin.ServicePriority; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.plugin.RegisteredServiceProvider; +import net.milkbowl.vault.economy.Economy; +import java.util.logging.Logger; + +public class BalSyncPlugin extends JavaPlugin { + private static BalSyncPlugin instance; + private Economy economy; + private DatabaseManager databaseManager; + private BalanceManager balanceManager; + private TranslationManager translationManager; + private ConfigManager configManager; + private Logger logger; + + @Override + public void onEnable() { + instance = this; + logger = getLogger(); + + // Load configuration + configManager = new ConfigManager(this); + configManager.loadConfig(); + + // Load translations + translationManager = new TranslationManager(this); + translationManager.loadMessages(); + + // Setup Vault economy + if (!setupEconomy()) { + logger.severe("Vault economy not found! Disabling plugin..."); + getServer().getPluginManager().disablePlugin(this); + return; + } + + // Initialize database + databaseManager = new DatabaseManager(this); + if (!databaseManager.connect()) { + logger.severe("Failed to connect to database! Disabling plugin..."); + getServer().getPluginManager().disablePlugin(this); + return; + } + + // Setup database tables + databaseManager.setupTables(); + + // Initialize balance manager + balanceManager = new BalanceManager(this, economy, databaseManager); + + // Register event listeners + getServer().getPluginManager().registerEvents(new PlayerEventListener(this, balanceManager), this); + + // Register commands + getCommand("balsync").setExecutor(new BalSyncCommand(this, balanceManager)); + + // Start auto-save task if enabled + int interval = configManager.getAutoSaveInterval(); + if (interval > 0) { + getServer().getScheduler().runTaskTimerAsynchronously(this, + () -> balanceManager.saveAllBalances(), + interval * 20L, interval * 20L); + } + + logger.info("BalSync v" + getDescription().getVersion() + " enabled successfully!"); + } + + @Override + public void onDisable() { + // Save all balances on shutdown + if (balanceManager != null) { + balanceManager.saveAllBalances(); + balanceManager.shutdown(); // NEW: Cleanup polling tasks + } + + // Close database connection + if (databaseManager != null) { + databaseManager.disconnect(); + } + + logger.info("BalSync disabled."); + } + + private boolean setupEconomy() { + if (getServer().getPluginManager().getPlugin("Vault") == null) { + return false; + } + + RegisteredServiceProvider rsp = getServer().getServicesManager() + .getRegistration(Economy.class); + if (rsp == null) { + return false; + } + + economy = rsp.getProvider(); + return economy != null; + } + + public static BalSyncPlugin getInstance() { + return instance; + } + + public Economy getEconomy() { + return economy; + } + + public DatabaseManager getDatabaseManager() { + return databaseManager; + } + + public TranslationManager getTranslationManager() { + return translationManager; + } + + public ConfigManager getConfigManager() { + return configManager; + } + + public Logger getPluginLogger() { + return logger; + } +} \ No newline at end of file diff --git a/src/main/java/com/user404_/balsync/BalanceManager.java b/src/main/java/com/user404_/balsync/BalanceManager.java new file mode 100644 index 0000000..6849c98 --- /dev/null +++ b/src/main/java/com/user404_/balsync/BalanceManager.java @@ -0,0 +1,285 @@ +package com.user404_.balsync; + +import net.milkbowl.vault.economy.Economy; +import net.milkbowl.vault.economy.EconomyResponse; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitTask; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.logging.Level; + +public class BalanceManager { + private final BalSyncPlugin plugin; + private final Economy economy; + private final DatabaseManager databaseManager; + private BukkitTask dbPollingTask; + private final Map lastKnownBalances = new HashMap<>(); + private final Map lastKnownDbBalances = new HashMap<>(); + public void saveAllBalances() { + plugin.getPluginLogger().info("Saving all player balances to database..."); + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + int saved = 0; + for (OfflinePlayer player : Bukkit.getOfflinePlayers()) { + try { + if (economy.hasAccount(player)) { + double balance = economy.getBalance(player); + databaseManager.saveBalance(player.getUniqueId(), player.getName(), balance); + lastKnownBalances.put(player.getUniqueId(), balance); + lastKnownDbBalances.put(player.getUniqueId(), balance); + saved++; + } + } catch (SQLException e) { + plugin.getPluginLogger().log(Level.WARNING, + "Failed to save balance for: " + player.getName(), e); + } + } + plugin.getPluginLogger().info("Saved " + saved + " player balances to database."); + }); + } + public BalanceManager(BalSyncPlugin plugin, Economy economy, DatabaseManager databaseManager) { + this.plugin = plugin; + this.economy = economy; + this.databaseManager = databaseManager; + startDbPolling(); + startOfflineMonitoring(); + } + + // MODIFIED: Added reset functionality + public void loadPlayerBalance(Player player) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + double databaseBalance = databaseManager.getBalance(player.getUniqueId()); + + Bukkit.getScheduler().runTask(plugin, () -> { + // Ensure player has account + if (!economy.hasAccount(player)) { + economy.createPlayerAccount(player); + } + + // RESET TO ZERO if configured + double currentBalance = economy.getBalance(player); + if (plugin.getConfigManager().isResetOnJoin()) { + if (currentBalance > 0) { + economy.withdrawPlayer(player, currentBalance); + } else if (currentBalance < 0) { + economy.depositPlayer(player, Math.abs(currentBalance)); + } + plugin.getLogger().info("Reset balance to 0 for " + player.getName()); + currentBalance = 0; + } + + // Apply database balance (OVERWRITE) + double difference = databaseBalance - currentBalance; + if (difference > 0) { + economy.depositPlayer(player, difference); + } else if (difference < 0) { + economy.withdrawPlayer(player, Math.abs(difference)); + } + + // Update tracking maps + lastKnownBalances.put(player.getUniqueId(), databaseBalance); + lastKnownDbBalances.put(player.getUniqueId(), databaseBalance); + + plugin.getLogger().info("Balance loaded for " + player.getName() + + ": " + databaseBalance + " (from DB)"); + + // Send message to player + String message = plugin.getTranslationManager().getMessage("balance-loaded"); + if (message != null && !message.isEmpty()) { + player.sendMessage(plugin.getTranslationManager().formatMessage(message)); + } + }); + + } catch (SQLException e) { + plugin.getPluginLogger().log(Level.SEVERE, + "Failed to load balance for player: " + player.getName(), e); + } + }); + } + + // NEW: Poll database for external changes + private void startDbPolling() { + int interval = plugin.getConfigManager().getDbPollInterval(); + if (interval <= 0) return; + + dbPollingTask = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, () -> { + pollDatabaseForChanges(); + }, interval * 20L, interval * 20L); + + plugin.getLogger().info("Started database polling every " + interval + " seconds"); + } + + // NEW: Check database for balance changes and apply to online players + private void pollDatabaseForChanges() { + List onlineUUIDs = getOnlinePlayerUUIDs(); + + // Keine online Spieler → nichts abfragen + if (onlineUUIDs.isEmpty()) { + return; + } + + try (Connection conn = databaseManager.getConnection()) { + // Platzhalter für die IN-Klausel erstellen + StringBuilder placeholders = new StringBuilder(); + for (int i = 0; i < onlineUUIDs.size(); i++) { + placeholders.append("?"); + if (i < onlineUUIDs.size() - 1) { + placeholders.append(","); + } + } + + String sql = String.format( + "SELECT player_uuid, balance FROM %s WHERE player_uuid IN (%s)", + plugin.getConfigManager().getTableName(), + placeholders.toString() + ); + + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + // UUIDs als Parameter setzen + for (int i = 0; i < onlineUUIDs.size(); i++) { + stmt.setString(i + 1, onlineUUIDs.get(i).toString()); + } + + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + UUID playerUUID = UUID.fromString(rs.getString("player_uuid")); + double dbBalance = rs.getDouble("balance"); + + // Prüfen, ob sich die Datenbank-Balance geändert hat + Double lastDbBalance = lastKnownDbBalances.get(playerUUID); + if (lastDbBalance == null || Math.abs(dbBalance - lastDbBalance) > 0.001) { + // Datenbank hat sich geändert → auf Spieler anwenden + applyDbChangeToPlayer(playerUUID, dbBalance, lastDbBalance); + lastKnownDbBalances.put(playerUUID, dbBalance); + } + } + } + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "Error polling database for changes", e); + } + } + + // NEW: Apply database changes to online player + private void applyDbChangeToPlayer(UUID playerUUID, double newBalance, Double oldBalance) { + Player player = Bukkit.getPlayer(playerUUID); + if (player != null && player.isOnline()) { + Bukkit.getScheduler().runTask(plugin, () -> { + double currentBalance = economy.getBalance(player); + double difference = newBalance - currentBalance; + + if (Math.abs(difference) > 0.001) { + if (difference > 0) { + economy.depositPlayer(player, difference); + } else { + economy.withdrawPlayer(player, Math.abs(difference)); + } + + lastKnownBalances.put(playerUUID, newBalance); + + plugin.getLogger().info("Applied external DB change for " + + player.getName() + ": " + newBalance); + + // Notify player if configured + if (plugin.getConfigManager().notifyOnExternalChange()) { + String message = plugin.getTranslationManager().getMessage( + "balance-external-change"); + if (message != null && !message.isEmpty()) { + String formatted = message + .replace("{old}", String.format("%.2f", oldBalance != null ? oldBalance : currentBalance)) + .replace("{new}", String.format("%.2f", newBalance)) + .replace("&", "§"); + player.sendMessage(plugin.getTranslationManager().formatMessage("prefix") + formatted); + } + } + } + }); + } + } + + // NEW: Monitor offline player balance changes when auto-save-interval = 0 + private void startOfflineMonitoring() { + int autoSaveInterval = plugin.getConfigManager().getAutoSaveInterval(); + boolean monitorOffline = plugin.getConfigManager().monitorOfflineChanges(); + + if (autoSaveInterval == 0 && monitorOffline) { + // Check for balance changes every 30 seconds + Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, () -> { + monitorOfflineBalanceChanges(); + }, 600L, 600L); // 30 seconds (600 ticks) + + plugin.getLogger().info("Started offline balance change monitoring"); + } + } + + // NEW: Detect and save offline balance changes + private void monitorOfflineBalanceChanges() { + for (OfflinePlayer offlinePlayer : Bukkit.getOfflinePlayers()) { + if (economy.hasAccount(offlinePlayer)) { + double currentBalance = economy.getBalance(offlinePlayer); + UUID uuid = offlinePlayer.getUniqueId(); + + Double lastBalance = lastKnownBalances.get(uuid); + if (lastBalance != null && Math.abs(currentBalance - lastBalance) > 0.001) { + // Balance has changed - save to database + try { + databaseManager.saveBalance(uuid, offlinePlayer.getName(), currentBalance); + lastKnownBalances.put(uuid, currentBalance); + plugin.getLogger().info("Detected offline change for " + + offlinePlayer.getName() + ": " + currentBalance); + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, + "Failed to save offline change for " + offlinePlayer.getName(), e); + } + } else if (lastBalance == null) { + // First time seeing this player, store initial balance + lastKnownBalances.put(uuid, currentBalance); + } + } + } + } + + // NEW: Track balance when player quits + public void trackPlayerQuit(UUID playerUUID, double balance) { + lastKnownBalances.put(playerUUID, balance); + } + + // Helper method + private List getOnlinePlayerUUIDs() { + List uuids = new ArrayList<>(); + for (Player player : Bukkit.getOnlinePlayers()) { + uuids.add(player.getUniqueId()); + } + return uuids; + } + + // MODIFIED savePlayerBalance to update tracking + public void savePlayerBalance(OfflinePlayer player) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + double balance = economy.getBalance(player); + databaseManager.saveBalance(player.getUniqueId(), player.getName(), balance); + lastKnownBalances.put(player.getUniqueId(), balance); + lastKnownDbBalances.put(player.getUniqueId(), balance); + } catch (SQLException e) { + plugin.getPluginLogger().log(Level.SEVERE, + "Failed to save balance for player: " + player.getName(), e); + } + }); + } + + // Cleanup on disable + public void shutdown() { + if (dbPollingTask != null) { + dbPollingTask.cancel(); + } + lastKnownBalances.clear(); + lastKnownDbBalances.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/com/user404_/balsync/ConfigManager.java b/src/main/java/com/user404_/balsync/ConfigManager.java new file mode 100644 index 0000000..0e88c1d --- /dev/null +++ b/src/main/java/com/user404_/balsync/ConfigManager.java @@ -0,0 +1,139 @@ +package com.user404_.balsync; + +import org.bukkit.configuration.file.FileConfiguration; +import java.io.File; + +public class ConfigManager { + private final BalSyncPlugin plugin; + private FileConfiguration config; + + public ConfigManager(BalSyncPlugin plugin) { + this.plugin = plugin; + } + + public void loadConfig() { + // Create plugin folder if it doesn't exist + if (!plugin.getDataFolder().exists()) { + plugin.getDataFolder().mkdirs(); + } + + // Save default config from resources + plugin.saveDefaultConfig(); + + // Reload configuration + plugin.reloadConfig(); + config = plugin.getConfig(); + + // Set default values if not present + setDefaults(); + } + + private void setDefaults() { + config.addDefault("database.host", "localhost"); + config.addDefault("database.port", 3306); + config.addDefault("database.database", "minecraft"); + config.addDefault("database.username", "root"); + config.addDefault("database.password", "password"); + config.addDefault("database.use-ssl", false); + config.addDefault("database.connection-pool.maximum-pool-size", 10); + config.addDefault("database.connection-pool.minimum-idle", 5); + config.addDefault("database.connection-pool.connection-timeout", 30000); + config.addDefault("database.connection-pool.idle-timeout", 600000); + + config.addDefault("settings.auto-save-interval", 60); + config.addDefault("settings.save-on-quit", true); + config.addDefault("settings.starting-balance", 100.0); + config.addDefault("settings.locale", "en"); + + config.addDefault("tables.player_balances.table-name", "player_balances"); + config.addDefault("tables.player_balances.uuid-column", "player_uuid"); + config.addDefault("tables.player_balances.balance-column", "balance"); + config.addDefault("tables.player_balances.last-updated-column", "last_updated"); + + config.options().copyDefaults(true); + plugin.saveConfig(); + } + + public void reload() { + plugin.reloadConfig(); + config = plugin.getConfig(); + } + + // Database getters + public String getDatabaseHost() { + return config.getString("database.host", "localhost"); + } + + public int getDatabasePort() { + return config.getInt("database.port", 3306); + } + + public String getDatabaseName() { + return config.getString("database.database", "minecraft"); + } + + public String getDatabaseUsername() { + return config.getString("database.username", "root"); + } + + public String getDatabasePassword() { + return config.getString("database.password", "password"); + } + + public boolean useSSL() { + return config.getBoolean("database.use-ssl", false); + } + + public int getMaxPoolSize() { + return config.getInt("database.connection-pool.maximum-pool-size", 10); + } + + public int getMinIdle() { + return config.getInt("database.connection-pool.minimum-idle", 5); + } + + public int getConnectionTimeout() { + return config.getInt("database.connection-pool.connection-timeout", 30000); + } + + public int getIdleTimeout() { + return config.getInt("database.connection-pool.idle-timeout", 600000); + } + + // Settings getters + public int getAutoSaveInterval() { + return config.getInt("settings.auto-save-interval", 60); + } + + public boolean saveOnQuit() { + return config.getBoolean("settings.save-on-quit", true); + } + + public double getStartingBalance() { + return config.getDouble("settings.starting-balance", 100.0); + } + + public String getLocale() { + return config.getString("settings.locale", "en"); + } + + // Table getters + public String getTableName() { + return config.getString("tables.player_balances.table-name", "player_balances"); + } + public boolean isResetOnJoin() { + return config.getBoolean("settings.reset-on-join", false); + } + + public boolean monitorOfflineChanges() { + return config.getBoolean("settings.monitor-offline-changes", true); + } + + public int getDbPollInterval() { + return config.getInt("settings.db-poll-interval", 10); + } + + public boolean notifyOnExternalChange() { + return config.getBoolean("settings.notify-on-external-change", true); + } +} \ No newline at end of file diff --git a/src/main/java/com/user404_/balsync/DatabaseManager.java b/src/main/java/com/user404_/balsync/DatabaseManager.java new file mode 100644 index 0000000..c937421 --- /dev/null +++ b/src/main/java/com/user404_/balsync/DatabaseManager.java @@ -0,0 +1,168 @@ +package com.user404_.balsync; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import java.sql.*; +import java.util.UUID; +import java.util.logging.Level; + +public class DatabaseManager { + private final BalSyncPlugin plugin; + private HikariDataSource dataSource; + private final String tableName; + + public DatabaseManager(BalSyncPlugin plugin) { + this.plugin = plugin; + this.tableName = plugin.getConfigManager().getTableName(); + } + + public boolean connect() { + try { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl(String.format("jdbc:mysql://%s:%d/%s", + plugin.getConfigManager().getDatabaseHost(), + plugin.getConfigManager().getDatabasePort(), + plugin.getConfigManager().getDatabaseName())); + config.setUsername(plugin.getConfigManager().getDatabaseUsername()); + config.setPassword(plugin.getConfigManager().getDatabasePassword()); + config.addDataSourceProperty("useSSL", + plugin.getConfigManager().useSSL()); + + // Connection pool settings + config.setMaximumPoolSize(plugin.getConfigManager().getMaxPoolSize()); + config.setMinimumIdle(plugin.getConfigManager().getMinIdle()); + config.setConnectionTimeout(plugin.getConfigManager().getConnectionTimeout()); + config.setIdleTimeout(plugin.getConfigManager().getIdleTimeout()); + config.setLeakDetectionThreshold(30000); + + dataSource = new HikariDataSource(config); + + // Test connection + try (Connection conn = dataSource.getConnection()) { + plugin.getPluginLogger().info("Successfully connected to MySQL database!"); + return true; + } + } catch (SQLException e) { + plugin.getPluginLogger().log(Level.SEVERE, "Failed to connect to database!", e); + return false; + } + } + + public void disconnect() { + if (dataSource != null && !dataSource.isClosed()) { + dataSource.close(); + plugin.getPluginLogger().info("Database connection closed."); + } + } + + public void setupTables() { + // Verwende DECIMAL statt DOUBLE für genauere Währungswerte + double startingBalance = plugin.getConfigManager().getStartingBalance(); + + String createTableSQL = String.format( + "CREATE TABLE IF NOT EXISTS `%s` (" + + "`id` INT AUTO_INCREMENT PRIMARY KEY, " + + "`player_uuid` CHAR(36) UNIQUE NOT NULL, " + + "`player_name` VARCHAR(16), " + + "`balance` DECIMAL(15, 2) NOT NULL DEFAULT %.2f, " + // DECIMAL für bessere Präzision + "`last_updated` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " + + "INDEX `idx_uuid` (`player_uuid`)" + + ") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB", + tableName, startingBalance + ); + + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.execute(createTableSQL); + plugin.getPluginLogger().info("Database tables checked/created successfully!"); + + // Optional: Protokolliere die erstellte Tabelle + logTableInfo(conn); + } catch (SQLException e) { + plugin.getPluginLogger().log(Level.SEVERE, "Failed to create database tables!", e); + + // Fallback-SQL ohne DEFAULT-Wert + tryFallbackTableCreation(); + } + } + + private void logTableInfo(Connection conn) throws SQLException { + String checkSQL = String.format("DESCRIBE `%s`", tableName); + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(checkSQL)) { + plugin.getPluginLogger().info("Table structure for '" + tableName + "':"); + while (rs.next()) { + plugin.getPluginLogger().info(String.format( + "Column: %s, Type: %s, Default: %s", + rs.getString("Field"), + rs.getString("Type"), + rs.getString("Default") + )); + } + } + } + + private void tryFallbackTableCreation() { + plugin.getPluginLogger().warning("Trying fallback table creation..."); + + // Fallback: Tabelle ohne DEFAULT-Wert, dann Standardwert über die Anwendung + String fallbackSQL = String.format( + "CREATE TABLE IF NOT EXISTS `%s` (" + + "`id` INT AUTO_INCREMENT PRIMARY KEY, " + + "`player_uuid` CHAR(36) UNIQUE NOT NULL, " + + "`player_name` VARCHAR(16), " + + "`balance` DECIMAL(15, 2) NOT NULL, " + // Kein DEFAULT hier + "`last_updated` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " + + "INDEX `idx_uuid` (`player_uuid`)" + + ") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB", + tableName + ); + + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.execute(fallbackSQL); + plugin.getPluginLogger().info("Fallback table created successfully!"); + } catch (SQLException ex) { + plugin.getPluginLogger().log(Level.SEVERE, "Fallback creation also failed!", ex); + } + } + + public double getBalance(UUID playerUUID) throws SQLException { + String sql = String.format("SELECT balance FROM %s WHERE player_uuid = ?", tableName); + + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, playerUUID.toString()); + + ResultSet rs = stmt.executeQuery(); + if (rs.next()) { + return rs.getDouble("balance"); + } + } + return plugin.getConfigManager().getStartingBalance(); + } + + public void saveBalance(UUID playerUUID, String playerName, double balance) throws SQLException { + String sql = String.format( + "INSERT INTO %s (player_uuid, player_name, balance) VALUES (?, ?, ?) " + + "ON DUPLICATE KEY UPDATE player_name = VALUES(player_name), balance = VALUES(balance)", + tableName + ); + + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, playerUUID.toString()); + stmt.setString(2, playerName); + stmt.setDouble(3, balance); + stmt.executeUpdate(); + } + } + + public Connection getConnection() throws SQLException { + return dataSource.getConnection(); + } + + public boolean isConnected() { + return dataSource != null && !dataSource.isClosed(); + } +} \ No newline at end of file diff --git a/src/main/java/com/user404_/balsync/PlayerEventListener.java b/src/main/java/com/user404_/balsync/PlayerEventListener.java new file mode 100644 index 0000000..f2ea01a --- /dev/null +++ b/src/main/java/com/user404_/balsync/PlayerEventListener.java @@ -0,0 +1,46 @@ +package com.user404_.balsync; + +import net.milkbowl.vault.economy.Economy; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +public class PlayerEventListener implements Listener { + private final BalSyncPlugin plugin; + private final BalanceManager balanceManager; + private final Economy economy; + + public PlayerEventListener(BalSyncPlugin plugin, BalanceManager balanceManager) { + this.plugin = plugin; + this.balanceManager = balanceManager; + this.economy = plugin.getEconomy(); // Economy vom Plugin holen + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + // 2 Sekunden (40 Ticks) Verzögerung + plugin.getServer().getScheduler().runTaskLater(plugin, () -> { + if (event.getPlayer().isOnline()) { + balanceManager.loadPlayerBalance(event.getPlayer()); + } + }, 40L); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + + if (plugin.getConfigManager().saveOnQuit()) { + balanceManager.savePlayerBalance(player); + } + + // Track balance on quit for offline monitoring + if (plugin.getConfigManager().monitorOfflineChanges() && + plugin.getConfigManager().getAutoSaveInterval() == 0) { + double balance = economy.getBalance(player); + balanceManager.trackPlayerQuit(player.getUniqueId(), balance); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/user404_/balsync/TranslationManager.java b/src/main/java/com/user404_/balsync/TranslationManager.java new file mode 100644 index 0000000..bb1283e --- /dev/null +++ b/src/main/java/com/user404_/balsync/TranslationManager.java @@ -0,0 +1,93 @@ +package com.user404_.balsync; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.configuration.file.FileConfiguration; +import java.io.File; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; + +public class TranslationManager { + private final BalSyncPlugin plugin; + private final Map messages; + private String locale; + + public TranslationManager(BalSyncPlugin plugin) { + this.plugin = plugin; + this.messages = new HashMap<>(); + } + + public void loadMessages() { + messages.clear(); + locale = plugin.getConfigManager().getLocale(); + + // First, load default messages from JAR + try (Reader reader = new InputStreamReader( + plugin.getResource("messages_en.yml"), StandardCharsets.UTF_8)) { + YamlConfiguration defaultConfig = YamlConfiguration.loadConfiguration(reader); + loadConfigIntoMap(defaultConfig); + } catch (Exception e) { + plugin.getPluginLogger().warning("Failed to load default messages!"); + } + + // Load locale-specific file if different from English + if (!locale.equalsIgnoreCase("en")) { + String fileName = "messages_" + locale.toLowerCase() + ".yml"; + File localeFile = new File(plugin.getDataFolder(), fileName); + + // Copy from resources if doesn't exist + if (!localeFile.exists()) { + plugin.saveResource(fileName, false); + } + + // Load the file + if (localeFile.exists()) { + YamlConfiguration localeConfig = YamlConfiguration.loadConfiguration(localeFile); + loadConfigIntoMap(localeConfig); + } + } + + // Finally, load user overrides from data folder + File userFile = new File(plugin.getDataFolder(), "messages.yml"); + if (userFile.exists()) { + YamlConfiguration userConfig = YamlConfiguration.loadConfiguration(userFile); + loadConfigIntoMap(userConfig); + } + + plugin.getPluginLogger().info("Loaded messages for locale: " + locale); + } + + private void loadConfigIntoMap(YamlConfiguration config) { + for (String key : config.getKeys(true)) { + if (config.isString(key)) { + messages.put(key, config.getString(key)); + } + } + } + + public String getMessage(String key) { + return messages.getOrDefault(key, key); + } + + public String formatMessage(String key, Object... args) { + String message = getMessage(key); + String prefix = getMessage("prefix"); + + if (args.length > 0) { + try { + message = MessageFormat.format(message, args); + } catch (Exception e) { + plugin.getPluginLogger().warning("Failed to format message: " + key); + } + } + + return prefix + message.replace('&', '§'); + } + + public String getLocale() { + return locale; + } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..a9be692 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,32 @@ +# Database Configuration (existing)... + +# Plugin Settings +settings: + # Auto-save interval in seconds (0 to disable) + auto-save-interval: 60 + # Whether to save on player quit + save-on-quit: true + # Starting balance for new players + starting-balance: 100.0 + # Locale for messages (en, de, etc.) + locale: "en" + + # Reset to zero before loading from database on join + reset-on-join: false + + # Monitor offline balance changes when auto-save-interval = 0 + monitor-offline-changes: true + + # Poll database for changes (interval in seconds, 0 = disabled) + db-poll-interval: 10 + + # Notification when balance is changed externally + notify-on-external-change: true + +# Database Table Configuration +tables: + player_balances: + table-name: "player_balances" + uuid-column: "player_uuid" + balance-column: "balance" + last-updated-column: "last_updated" \ No newline at end of file diff --git a/src/main/resources/messages_at.yml b/src/main/resources/messages_at.yml new file mode 100644 index 0000000..c65238b --- /dev/null +++ b/src/main/resources/messages_at.yml @@ -0,0 +1,15 @@ +# This is an Easter egg language. If you found this, you're probably digging through the source code. +# Have fun with the Austrian/Wienerisch translation! :) + +prefix: "§8[§dGödSynk§8] §7" +no-permission: "§cHeast, du host ka Berechtigung für den Befehl!" +config-reloaded: "§aKonfig is wieda gscheit neig'laden!" +balance-loaded: "§aDei Gödstandl is jetzt mit da Datenbank a glei." +balance-saved: "§aOlle Gödstandln woan in da Datenbank gspeichert, passt so!" +database-connected: "§aJo fix, Verbindung zur Datenbank steht!" +database-error: "§cOida… do is a Fehler in da Datenbank. Schau amoi in die Konsole." +player-not-found: "§cDen Spieler find i net, host di vertippt?" +usage: "§cSo geht's: /balsync [reload|save|load]" +balance-external-change: "§eDei Gödstandl woan von außen gändat: §6{old} §e→ §6{new}" +balance-reset: "§aDei Gödstandl woan auf §e${0}§a zruckgsetzt, bevors aus da Datenbank kema san." +offline-change-detected: "§7A Änderung währendst ned do woarst is gmerkt und gspeichert wordn." diff --git a/src/main/resources/messages_de.yml b/src/main/resources/messages_de.yml new file mode 100644 index 0000000..6f1fd0e --- /dev/null +++ b/src/main/resources/messages_de.yml @@ -0,0 +1,12 @@ +prefix: "§8[§6BalSync§8] §7" +no-permission: "§cDu hast keine Berechtigung diesen Befehl zu verwenden!" +config-reloaded: "§aKonfiguration erfolgreich neu geladen!" +balance-loaded: "§aDein Kontostand wurde mit der Datenbank synchronisiert." +balance-saved: "§aAlle Spielerkontostände wurden in der Datenbank gespeichert." +database-connected: "§aErfolgreich mit der Datenbank verbunden!" +database-error: "§cDatenbankfehler aufgetreten. Überprüfe die Konsole oder die Logs für Details." +player-not-found: "§cSpieler nicht gefunden!" +usage: "§cVerwendung: /balsync [reload|save|load]" +balance-external-change: "§eDein Kontostand wurde extern (vielleicht von einem anderen Server) aktualisiert: §6{old} §e→ §6{new}" +balance-reset: "§aDein Kontostand wurde auf §e${0}§a zurückgesetzt, bevor er aus der Datenbank geladen wurde." +offline-change-detected: "§7Offline-Kontostandänderung erkannt und gespeichert." \ No newline at end of file diff --git a/src/main/resources/messages_en.yml b/src/main/resources/messages_en.yml new file mode 100644 index 0000000..7ff0865 --- /dev/null +++ b/src/main/resources/messages_en.yml @@ -0,0 +1,12 @@ +prefix: "§8[§6BalSync§8] §7" +no-permission: "§cYou don't have permission to use this command!" +config-reloaded: "§aConfiguration reloaded successfully!" +balance-loaded: "§aYour balance has been synchronized with the database." +balance-saved: "§aAll player balances have been saved to the database." +database-connected: "§aSuccessfully connected to the database!" +database-error: "§cDatabase error occurred. Check console for details." +player-not-found: "§cPlayer not found!" +usage: "§cUsage: /balsync [reload|save|load]" +balance-external-change: "§eYour balance was updated externally: §6{old} §e→ §6{new}" +balance-reset: "§aYour balance was reset to §e${0}§a before loading from database." +offline-change-detected: "§7Offline balance change detected and saved." \ No newline at end of file diff --git a/src/main/resources/messages_es.yml b/src/main/resources/messages_es.yml new file mode 100644 index 0000000..cfc6f76 --- /dev/null +++ b/src/main/resources/messages_es.yml @@ -0,0 +1,12 @@ +prefix: "§8[§6BalSync§8] §7" +no-permission: "§c¡No tienes permiso para usar este comando!" +config-reloaded: "§a¡Configuración recargada correctamente!" +balance-loaded: "§aTu saldo ha sido sincronizado con la base de datos." +balance-saved: "§aTodos los saldos de los jugadores han sido guardados en la base de datos." +database-connected: "§a¡Conexión exitosa a la base de datos!" +database-error: "§cOcurrió un error de base de datos. Revisa la consola para más detalles." +player-not-found: "§c¡Jugador no encontrado!" +usage: "§cUso: /balsync [reload|save|load]" +balance-external-change: "§eTu saldo fue actualizado externamente: §6{old} §e→ §6{new}" +balance-reset: "§aTu saldo fue restablecido a §e${0}§a antes de cargarse desde la base de datos." +offline-change-detected: "§7Cambio de saldo offline detectado y guardado." diff --git a/src/main/resources/messages_fr.yml b/src/main/resources/messages_fr.yml new file mode 100644 index 0000000..403d0d5 --- /dev/null +++ b/src/main/resources/messages_fr.yml @@ -0,0 +1,12 @@ +prefix: "§8[§6BalSync§8] §7" +no-permission: "§cVous n'avez pas la permission d'utiliser cette commande !" +config-reloaded: "§aConfiguration rechargée avec succès !" +balance-loaded: "§aVotre solde a été synchronisé avec la base de données." +balance-saved: "§aTous les soldes des joueurs ont été enregistrés dans la base de données." +database-connected: "§aConnexion réussie à la base de données !" +database-error: "§cUne erreur de base de données est survenue. Vérifiez la console pour plus de détails." +player-not-found: "§cJoueur introuvable !" +usage: "§cUtilisation : /balsync [reload|save|load]" +balance-external-change: "§eVotre solde a été mis à jour extérieurement : §6{old} §e→ §6{new}" +balance-reset: "§aVotre solde a été réinitialisé à §e${0}§a avant le chargement depuis la base de données." +offline-change-detected: "§7Modification du solde hors ligne détectée et enregistrée." diff --git a/src/main/resources/messages_pl.yml b/src/main/resources/messages_pl.yml new file mode 100644 index 0000000..403d0d5 --- /dev/null +++ b/src/main/resources/messages_pl.yml @@ -0,0 +1,12 @@ +prefix: "§8[§6BalSync§8] §7" +no-permission: "§cVous n'avez pas la permission d'utiliser cette commande !" +config-reloaded: "§aConfiguration rechargée avec succès !" +balance-loaded: "§aVotre solde a été synchronisé avec la base de données." +balance-saved: "§aTous les soldes des joueurs ont été enregistrés dans la base de données." +database-connected: "§aConnexion réussie à la base de données !" +database-error: "§cUne erreur de base de données est survenue. Vérifiez la console pour plus de détails." +player-not-found: "§cJoueur introuvable !" +usage: "§cUtilisation : /balsync [reload|save|load]" +balance-external-change: "§eVotre solde a été mis à jour extérieurement : §6{old} §e→ §6{new}" +balance-reset: "§aVotre solde a été réinitialisé à §e${0}§a avant le chargement depuis la base de données." +offline-change-detected: "§7Modification du solde hors ligne détectée et enregistrée." diff --git a/src/main/resources/messages_pt-br.yml b/src/main/resources/messages_pt-br.yml new file mode 100644 index 0000000..c028cf4 --- /dev/null +++ b/src/main/resources/messages_pt-br.yml @@ -0,0 +1,12 @@ +prefix: "§8[§6BalSync§8] §7" +no-permission: "§cVocê não tem permissão para usar este comando!" +config-reloaded: "§aConfiguração recarregada com sucesso!" +balance-loaded: "§aSeu saldo foi sincronizado com o banco de dados." +balance-saved: "§aTodos os saldos dos jogadores foram salvos no banco de dados." +database-connected: "§aConectado com sucesso ao banco de dados!" +database-error: "§cOcorreu um erro no banco de dados. Verifique o console para mais detalhes." +player-not-found: "§cJogador não encontrado!" +usage: "§cUso: /balsync [reload|save|load]" +balance-external-change: "§eSeu saldo foi atualizado externamente: §6{old} §e→ §6{new}" +balance-reset: "§aSeu saldo foi redefinido para §e${0}§a antes de carregar do banco de dados." +offline-change-detected: "§7Alteração de saldo offline detectada e salva." diff --git a/src/main/resources/messages_ru.yml b/src/main/resources/messages_ru.yml new file mode 100644 index 0000000..2001fac --- /dev/null +++ b/src/main/resources/messages_ru.yml @@ -0,0 +1,12 @@ +prefix: "§8[§6BalSync§8] §7" +no-permission: "§cУ вас нет прав для использования этой команды!" +config-reloaded: "§aКонфигурация успешно перезагружена!" +balance-loaded: "§aВаш баланс был синхронизирован с базой данных." +balance-saved: "§aВсе балансы игроков были сохранены в базе данных." +database-connected: "§aУспешно подключено к базе данных!" +database-error: "§cПроизошла ошибка базы данных. Проверьте консоль для деталей." +player-not-found: "§cИгрок не найден!" +usage: "§cИспользование: /balsync [reload|save|load]" +balance-external-change: "§eВаш баланс был обновлён извне: §6{old} §e→ §6{new}" +balance-reset: "§aВаш баланс был сброшен до §e${0}§a перед загрузкой из базы данных." +offline-change-detected: "§7Обнаружено и сохранено изменение баланса оффлайн." diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..5c3704e --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,20 @@ +name: BalSync +version: 1.0 +main: com.user404_.balsync.BalSyncPlugin +api-version: '1.20' +description: Synchronizes player balances with MySQL database +author: user404_ +website: https://deutschich.github.io/BalSync + +depend: [Vault] + +commands: + balsync: + description: Main command for BalSync + usage: / [reload|save|load] + permission: balsync.admin + +permissions: + balsync.admin: + description: Allows access to all BalSync commands + default: op \ No newline at end of file