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