Merge pull request 'dev' (#1) from dev into main

Reviewed-on: #1
This commit is contained in:
2025-12-03 16:34:41 +01:00
22 changed files with 1400 additions and 13 deletions

53
.github/workflows/preview-release.yml vendored Normal file
View File

@@ -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 }}

47
.github/workflows/release-on-merge.yml vendored Normal file
View File

@@ -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 }}

113
.gitignore vendored Normal file
View File

@@ -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/

29
LICENSE
View File

@@ -1,18 +1,21 @@
MIT License 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 Permission is hereby granted, free of charge, to any person obtaining a copy
associated documentation files (the "Software"), to deal in the Software without restriction, including of this software and associated documentation files (the "Software"), to deal
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell in the Software without restriction, including without limitation the rights
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
following conditions: 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 The above copyright notice and this permission notice shall be included in all
portions of the Software. copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
USE OR OTHER DEALINGS IN THE SOFTWARE. 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.

97
pom.xml Normal file
View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.user404_</groupId>
<artifactId>BalSync</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>BalSync</name>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<defaultGoal>clean package</defaultGoal>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>papermc-repo</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<!-- PaperMC Repository -->
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<!-- VaultAPI Repository (JitPack) -->
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.21-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!-- VaultAPI -->
<dependency>
<groupId>com.github.MilkBowl</groupId>
<artifactId>VaultAPI</artifactId>
<version>1.7</version>
<scope>provided</scope>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- HikariCP for Connection Pooling -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>
</dependencies>
</project>

View File

@@ -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());
}
}

View File

@@ -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<Economy> 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;
}
}

View File

@@ -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<UUID, Double> lastKnownBalances = new HashMap<>();
private final Map<UUID, Double> 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<UUID> 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<UUID> getOnlinePlayerUUIDs() {
List<UUID> 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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<String, String> 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;
}
}

View File

@@ -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"

View File

@@ -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."

View File

@@ -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."

View File

@@ -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."

View File

@@ -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."

View File

@@ -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."

View File

@@ -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."

View File

@@ -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."

View File

@@ -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Обнаружено и сохранено изменение баланса оффлайн."

View File

@@ -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: /<command> [reload|save|load]
permission: balsync.admin
permissions:
balsync.admin:
description: Allows access to all BalSync commands
default: op