This commit is contained in:
16
PlayerDataSync-Premium/premium/.gitignore
vendored
Normal file
16
PlayerDataSync-Premium/premium/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
target/
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
*.log
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/
|
||||
.settings/
|
||||
.classpath
|
||||
.project
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
199
PlayerDataSync-Premium/premium/PLUGIN_DESCRIPTION.md
Normal file
199
PlayerDataSync-Premium/premium/PLUGIN_DESCRIPTION.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# PlayerDataSync Premium - Plugin Beschreibung
|
||||
|
||||
## Kurzbeschreibung / Short Description
|
||||
|
||||
**DE:** Premium-Version von PlayerDataSync mit Lizenz-Validierung, automatischen Updates und erweitertem Support für Custom-Enchantments. Synchronisiert Spielerdaten zwischen Servern über MySQL/SQLite.
|
||||
|
||||
**EN:** Premium version of PlayerDataSync with license validation, automatic updates, and enhanced support for custom enchantments. Synchronizes player data between servers via MySQL/SQLite.
|
||||
|
||||
---
|
||||
|
||||
## Beschreibung / Description
|
||||
|
||||
### Deutsch
|
||||
|
||||
**PlayerDataSync Premium** ist die Premium-Version des beliebten PlayerDataSync-Plugins für Minecraft-Server. Es bietet alle Features der Standard-Version plus erweiterte Funktionen für professionelle Server-Netzwerke.
|
||||
|
||||
#### Hauptfunktionen:
|
||||
|
||||
**✅ Vollständige Spielerdaten-Synchronisation**
|
||||
- Inventar, Rüstung, Offhand und Enderkiste
|
||||
- Gesundheit, Hunger und Sättigung
|
||||
- Erfahrungspunkte und Level
|
||||
- Spielmodus und Position
|
||||
- Tränkeffekte und Attribute
|
||||
- Statistiken und Erfolge/Advancements
|
||||
- Economy-Balance (Vault-Integration)
|
||||
|
||||
**✅ Premium-Features**
|
||||
- **Lizenz-Validierung:** Automatische Validierung über CraftingStudio Pro API
|
||||
- **Automatische Updates:** Update-Benachrichtigungen und -Prüfung
|
||||
- **Erweiterte Custom-Enchantment-Unterstützung:** Optimiert für ExcellentEnchants und andere Custom-Enchantment-Plugins
|
||||
- **Prioritäts-Support:** Schnellerer Support für Premium-Nutzer
|
||||
|
||||
**✅ Datenbank-Unterstützung**
|
||||
- MySQL/MariaDB für Multi-Server-Netzwerke
|
||||
- SQLite für Single-Server-Installationen
|
||||
- Automatische Datenbank-Upgrades (TEXT → LONGTEXT)
|
||||
- Connection Pooling für bessere Performance
|
||||
|
||||
**✅ Erweiterte Features**
|
||||
- BungeeCord/Velocity-Integration
|
||||
- InvSee/OpenInv-Integration für Offline-Spieler
|
||||
- Automatische Backups
|
||||
- Performance-Monitoring
|
||||
- Umfassende Fehlerbehandlung
|
||||
|
||||
**✅ Kompatibilität**
|
||||
- Minecraft 1.8 - 1.21.11
|
||||
- Cross-Version-Kompatibilität
|
||||
- Automatische Feature-Erkennung basierend auf Server-Version
|
||||
|
||||
#### Technische Details:
|
||||
|
||||
- **Lizenz-Validierung:** Über CraftingStudio Pro API mit 30-Minuten-Caching
|
||||
- **Update-Checks:** Automatisch beim Server-Start und manuell via `/sync update check`
|
||||
- **Rate Limits:** 100 API-Requests pro Stunde (automatisch verwaltet)
|
||||
- **Datenbank-Upgrades:** Automatische Migration von TEXT zu LONGTEXT für große Inventare
|
||||
|
||||
#### Installation:
|
||||
|
||||
1. JAR-Datei in den `plugins` Ordner kopieren
|
||||
2. Server starten (Config wird generiert)
|
||||
3. Lizenzschlüssel in `config.yml` eintragen
|
||||
4. Server neu starten
|
||||
|
||||
#### Support:
|
||||
|
||||
- Website: https://craftingstudiopro.de
|
||||
- API-Dokumentation: https://www.craftingstudiopro.de/docs/api
|
||||
- Discord: [Join our Discord](https://discord.gg/...)
|
||||
|
||||
---
|
||||
|
||||
### English
|
||||
|
||||
**PlayerDataSync Premium** is the premium version of the popular PlayerDataSync plugin for Minecraft servers. It offers all features from the standard version plus advanced functionality for professional server networks.
|
||||
|
||||
#### Main Features:
|
||||
|
||||
**✅ Complete Player Data Synchronization**
|
||||
- Inventory, armor, offhand, and ender chest
|
||||
- Health, hunger, and saturation
|
||||
- Experience points and levels
|
||||
- Gamemode and position
|
||||
- Potion effects and attributes
|
||||
- Statistics and advancements
|
||||
- Economy balance (Vault integration)
|
||||
|
||||
**✅ Premium Features**
|
||||
- **License Validation:** Automatic validation via CraftingStudio Pro API
|
||||
- **Automatic Updates:** Update notifications and checking
|
||||
- **Enhanced Custom Enchantment Support:** Optimized for ExcellentEnchants and other custom enchantment plugins
|
||||
- **Priority Support:** Faster support for premium users
|
||||
|
||||
**✅ Database Support**
|
||||
- MySQL/MariaDB for multi-server networks
|
||||
- SQLite for single-server installations
|
||||
- Automatic database upgrades (TEXT → LONGTEXT)
|
||||
- Connection pooling for better performance
|
||||
|
||||
**✅ Advanced Features**
|
||||
- BungeeCord/Velocity integration
|
||||
- InvSee/OpenInv integration for offline players
|
||||
- Automatic backups
|
||||
- Performance monitoring
|
||||
- Comprehensive error handling
|
||||
|
||||
**✅ Compatibility**
|
||||
- Minecraft 1.8 - 1.21.11
|
||||
- Cross-version compatibility
|
||||
- Automatic feature detection based on server version
|
||||
|
||||
#### Technical Details:
|
||||
|
||||
- **License Validation:** Via CraftingStudio Pro API with 30-minute caching
|
||||
- **Update Checks:** Automatically on server start and manually via `/sync update check`
|
||||
- **Rate Limits:** 100 API requests per hour (automatically managed)
|
||||
- **Database Upgrades:** Automatic migration from TEXT to LONGTEXT for large inventories
|
||||
|
||||
#### Installation:
|
||||
|
||||
1. Copy JAR file to `plugins` folder
|
||||
2. Start server (config will be generated)
|
||||
3. Enter license key in `config.yml`
|
||||
4. Restart server
|
||||
|
||||
#### Support:
|
||||
|
||||
- Website: https://craftingstudiopro.de
|
||||
- API Documentation: https://www.craftingstudiopro.de/docs/api
|
||||
- Discord: [Join our Discord](https://discord.gg/...)
|
||||
|
||||
---
|
||||
|
||||
## Tags / Schlagwörter
|
||||
|
||||
### Deutsch
|
||||
- `player-data-sync`
|
||||
- `premium`
|
||||
- `multi-server`
|
||||
- `bungeecord`
|
||||
- `velocity`
|
||||
- `inventory-sync`
|
||||
- `data-synchronization`
|
||||
- `mysql`
|
||||
- `sqlite`
|
||||
- `custom-enchantments`
|
||||
- `excellentenchants`
|
||||
- `vault`
|
||||
- `economy-sync`
|
||||
- `backup`
|
||||
- `cross-server`
|
||||
- `network`
|
||||
- `spigot`
|
||||
- `paper`
|
||||
- `bukkit`
|
||||
- `minecraft-plugin`
|
||||
|
||||
### English
|
||||
- `player-data-sync`
|
||||
- `premium`
|
||||
- `multi-server`
|
||||
- `bungeecord`
|
||||
- `velocity`
|
||||
- `inventory-sync`
|
||||
- `data-synchronization`
|
||||
- `mysql`
|
||||
- `sqlite`
|
||||
- `custom-enchantments`
|
||||
- `excellentenchants`
|
||||
- `vault`
|
||||
- `economy-sync`
|
||||
- `backup`
|
||||
- `cross-server`
|
||||
- `network`
|
||||
- `spigot`
|
||||
- `paper`
|
||||
- `bukkit`
|
||||
- `minecraft-plugin`
|
||||
|
||||
---
|
||||
|
||||
## Für CraftingStudio Pro / For CraftingStudio Pro
|
||||
|
||||
### Kurzbeschreibung (Max. 200 Zeichen)
|
||||
|
||||
**DE:** Premium-Version von PlayerDataSync mit Lizenz-Validierung, automatischen Updates und erweitertem Support für Custom-Enchantments. Synchronisiert Spielerdaten zwischen Servern.
|
||||
|
||||
**EN:** Premium version of PlayerDataSync with license validation, automatic updates, and enhanced support for custom enchantments. Synchronizes player data between servers.
|
||||
|
||||
### Beschreibung (Vollständig)
|
||||
|
||||
Siehe oben / See above
|
||||
|
||||
### Tags (Komma-getrennt)
|
||||
|
||||
```
|
||||
player-data-sync, premium, multi-server, bungeecord, velocity, inventory-sync, data-synchronization, mysql, sqlite, custom-enchantments, excellentenchants, vault, economy-sync, backup, cross-server, network, spigot, paper, bukkit, minecraft-plugin
|
||||
```
|
||||
109
PlayerDataSync-Premium/premium/README.md
Normal file
109
PlayerDataSync-Premium/premium/README.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# PlayerDataSync Premium
|
||||
|
||||
## Übersicht / Overview
|
||||
|
||||
**EN:** PlayerDataSync Premium is the premium version of PlayerDataSync with license validation, automatic update checking, and enhanced features for custom enchantments.
|
||||
**DE:** PlayerDataSync Premium ist die Premium-Version von PlayerDataSync mit Lizenz-Validierung, automatischer Update-Prüfung und erweiterten Features für Custom-Enchantments.
|
||||
|
||||
## Features
|
||||
|
||||
### ✅ License Validation / Lizenz-Validierung
|
||||
- **EN:** Validates license keys against CraftingStudio Pro API
|
||||
- **DE:** Validiert Lizenzschlüssel gegen CraftingStudio Pro API
|
||||
- **EN:** Automatic license re-validation every 24 hours
|
||||
- **DE:** Automatische Lizenz-Re-Validierung alle 24 Stunden
|
||||
- **EN:** Caching to reduce API calls (30 minutes)
|
||||
- **DE:** Caching zur Reduzierung von API-Aufrufen (30 Minuten)
|
||||
- **EN:** Automatic plugin disabling on invalid license
|
||||
- **DE:** Automatische Plugin-Deaktivierung bei ungültiger Lizenz
|
||||
|
||||
### ✅ Update Checker / Update-Prüfung
|
||||
- **EN:** Checks for updates using CraftingStudio Pro API
|
||||
- **DE:** Prüft auf Updates über CraftingStudio Pro API
|
||||
- **EN:** Notifies operators about available updates
|
||||
- **DE:** Benachrichtigt Operatoren über verfügbare Updates
|
||||
- **EN:** Rate limit handling (100 requests/hour)
|
||||
- **DE:** Rate-Limit-Behandlung (100 Anfragen/Stunde)
|
||||
|
||||
### ✅ Premium Features
|
||||
- **EN:** All features from PlayerDataSync
|
||||
- **DE:** Alle Features von PlayerDataSync
|
||||
- **EN:** Enhanced support for custom enchantments (ExcellentEnchants, etc.)
|
||||
- **DE:** Erweiterte Unterstützung für Custom-Enchantments (ExcellentEnchants, etc.)
|
||||
- **EN:** Priority support
|
||||
- **DE:** Prioritäts-Support
|
||||
|
||||
## Installation
|
||||
|
||||
1. **EN:** Download PlayerDataSync Premium from CraftingStudio Pro
|
||||
**DE:** Lade PlayerDataSync Premium von CraftingStudio Pro herunter
|
||||
|
||||
2. **EN:** Place the JAR file in your `plugins` folder
|
||||
**DE:** Platziere die JAR-Datei in deinem `plugins` Ordner
|
||||
|
||||
3. **EN:** Start your server to generate the config file
|
||||
**DE:** Starte deinen Server, um die Config-Datei zu generieren
|
||||
|
||||
4. **EN:** Edit `plugins/PlayerDataSync-Premium/config.yml` and enter your license key:
|
||||
**DE:** Bearbeite `plugins/PlayerDataSync-Premium/config.yml` und trage deinen Lizenzschlüssel ein:
|
||||
|
||||
```yaml
|
||||
license:
|
||||
key: YOUR-LICENSE-KEY-HERE
|
||||
```
|
||||
|
||||
5. **EN:** Restart your server
|
||||
**DE:** Starte deinen Server neu
|
||||
|
||||
## API Integration
|
||||
|
||||
### License Validation
|
||||
|
||||
**Endpoint:** `POST https://craftingstudiopro.de/api/license/validate`
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"licenseKey": "YOUR-LICENSE-KEY",
|
||||
"pluginId": "playerdatasync-premium"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"message": "License is valid",
|
||||
"purchase": {
|
||||
"id": "purchase-id",
|
||||
"userId": "user-id",
|
||||
"pluginId": "playerdatasync-premium",
|
||||
"createdAt": "2025-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Update Check
|
||||
|
||||
**Endpoint:** `GET https://craftingstudiopro.de/api/plugins/playerdatasync-premium/latest`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"version": "1.2.9-PREMIUM",
|
||||
"downloadUrl": "https://...",
|
||||
"pluginTitle": "PlayerDataSync Premium",
|
||||
"pluginSlug": "playerdatasync-premium"
|
||||
}
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
- `/sync license validate` - Manually validate license key
|
||||
- `/sync license info` - Show license information (masked)
|
||||
- `/sync update check` - Manually check for updates
|
||||
|
||||
## Support
|
||||
|
||||
- Website: https://craftingstudiopro.de
|
||||
- API Documentation: https://www.craftingstudiopro.de/docs/api
|
||||
121
PlayerDataSync-Premium/premium/SETUP.md
Normal file
121
PlayerDataSync-Premium/premium/SETUP.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# PlayerDataSync Premium - Setup Anleitung
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
Die Premium-Version benötigt alle Klassen aus PlayerDataSync, aber mit angepassten Package-Namen:
|
||||
|
||||
### Package-Mapping
|
||||
|
||||
**Von:** `com.example.playerdatasync.*`
|
||||
**Zu:** `com.example.playerdatasync.premium.*`
|
||||
|
||||
### Zu kopierende Klassen
|
||||
|
||||
Alle folgenden Klassen müssen aus `PlayerDataSync/src/main/java/com/example/playerdatasync/` nach `PlayerDataSync-Premium/premium/src/main/java/com/example/playerdatasync/premium/` kopiert und Package-Namen angepasst werden:
|
||||
|
||||
#### Core
|
||||
- `core/PlayerDataSync.java` → `premium/core/PlayerDataSyncPremium.java` ✅ (bereits erstellt)
|
||||
|
||||
#### Database
|
||||
- `database/ConnectionPool.java` → `premium/database/ConnectionPool.java`
|
||||
- `database/DatabaseManager.java` → `premium/database/DatabaseManager.java`
|
||||
|
||||
#### Commands
|
||||
- `commands/SyncCommand.java` → `premium/commands/SyncCommand.java`
|
||||
- Premium-Befehle hinzufügen: `/sync license validate`, `/sync license info`, `/sync update check`
|
||||
|
||||
#### Listeners
|
||||
- `listeners/PlayerDataListener.java` → `premium/listeners/PlayerDataListener.java`
|
||||
- `listeners/ServerSwitchListener.java` → `premium/listeners/ServerSwitchListener.java`
|
||||
|
||||
#### Managers
|
||||
- `managers/AdvancementSyncManager.java` → `premium/managers/AdvancementSyncManager.java`
|
||||
- `managers/BackupManager.java` → `premium/managers/BackupManager.java`
|
||||
- `managers/ConfigManager.java` → `premium/managers/ConfigManager.java`
|
||||
- `managers/MessageManager.java` → `premium/managers/MessageManager.java`
|
||||
- `managers/LicenseManager.java` → `premium/managers/LicenseManager.java` ✅ (bereits erstellt)
|
||||
|
||||
#### Integration
|
||||
- `integration/InventoryViewerIntegrationManager.java` → `premium/integration/InventoryViewerIntegrationManager.java`
|
||||
|
||||
#### Utils
|
||||
- `utils/InventoryUtils.java` → `premium/utils/InventoryUtils.java`
|
||||
- `utils/OfflinePlayerData.java` → `premium/utils/OfflinePlayerData.java`
|
||||
- `utils/PlayerDataCache.java` → `premium/utils/PlayerDataCache.java`
|
||||
- `utils/VersionCompatibility.java` → `premium/utils/VersionCompatibility.java`
|
||||
|
||||
#### API
|
||||
- `api/PremiumUpdateChecker.java` → `premium/api/PremiumUpdateChecker.java` ✅ (bereits erstellt)
|
||||
- `api/LicenseValidator.java` → `premium/api/LicenseValidator.java` ✅ (bereits erstellt)
|
||||
|
||||
### Resources
|
||||
|
||||
- `resources/config.yml` → `premium/src/main/resources/config.yml` ✅ (bereits erstellt)
|
||||
- `resources/plugin.yml` → `premium/src/main/resources/plugin.yml` ✅ (bereits erstellt)
|
||||
- `resources/messages_en.yml` → `premium/src/main/resources/messages_en.yml`
|
||||
- `resources/messages_de.yml` → `premium/src/main/resources/messages_de.yml`
|
||||
|
||||
## Anpassungen
|
||||
|
||||
### 1. Package-Namen ändern
|
||||
|
||||
Alle Klassen müssen von:
|
||||
```java
|
||||
package com.example.playerdatasync.xxx;
|
||||
```
|
||||
|
||||
zu:
|
||||
```java
|
||||
package com.example.playerdatasync.premium.xxx;
|
||||
```
|
||||
|
||||
### 2. Imports anpassen
|
||||
|
||||
Alle Imports müssen angepasst werden:
|
||||
```java
|
||||
// Alt
|
||||
import com.example.playerdatasync.database.DatabaseManager;
|
||||
|
||||
// Neu
|
||||
import com.example.playerdatasync.premium.database.DatabaseManager;
|
||||
```
|
||||
|
||||
### 3. SyncCommand erweitern
|
||||
|
||||
In `SyncCommand.java` müssen Premium-Befehle hinzugefügt werden:
|
||||
|
||||
```java
|
||||
case "license":
|
||||
return handleLicense(sender, args);
|
||||
|
||||
case "update":
|
||||
return handleUpdate(sender, args);
|
||||
```
|
||||
|
||||
### 4. PlayerDataSyncPremium.java vervollständigen
|
||||
|
||||
Die Hauptklasse `PlayerDataSyncPremium.java` ist bereits erstellt, aber alle Methoden aus der originalen `PlayerDataSync.java` müssen kopiert werden.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cd PlayerDataSync-Premium/premium
|
||||
mvn clean package
|
||||
```
|
||||
|
||||
Die JAR-Datei wird in `target/PlayerDataSync-Premium-1.2.9-PREMIUM.jar` erstellt.
|
||||
|
||||
## Wichtige Hinweise
|
||||
|
||||
1. **License Key erforderlich**: Die Premium-Version funktioniert nur mit einem gültigen Lizenzschlüssel
|
||||
2. **API-Zugriff**: Benötigt Internetverbindung für Lizenz-Validierung und Update-Checks
|
||||
3. **Rate Limits**: API hat ein Limit von 100 Requests/Stunde pro IP
|
||||
4. **Caching**: Lizenz-Validierung wird 30 Minuten gecacht
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. Alle Klassen aus PlayerDataSync kopieren
|
||||
2. Package-Namen anpassen
|
||||
3. Imports anpassen
|
||||
4. Premium-Befehle in SyncCommand hinzufügen
|
||||
5. Build und Test
|
||||
381
PlayerDataSync-Premium/premium/pom.xml
Normal file
381
PlayerDataSync-Premium/premium/pom.xml
Normal file
@@ -0,0 +1,381 @@
|
||||
<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.example</groupId>
|
||||
<artifactId>PlayerDataSync-Premium</artifactId>
|
||||
<version>1.0.0-PREMIUM</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<minecraft.version>1.21</minecraft.version>
|
||||
<java.version>8</java.version>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spigot-repo</id>
|
||||
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>jitpack.io</id>
|
||||
<url>https://jitpack.io</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>placeholderapi</id>
|
||||
<url>https://repo.extendedclip.com/content/repositories/placeholderapi/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>luck-repo</id>
|
||||
<url>https://repo.lucko.me/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spigot API -->
|
||||
<dependency>
|
||||
<groupId>org.spigotmc</groupId>
|
||||
<artifactId>spigot-api</artifactId>
|
||||
<version>${minecraft.version}-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Database Drivers -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>8.2.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.44.1.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Connection Pooling -->
|
||||
<dependency>
|
||||
<groupId>com.zaxxer</groupId>
|
||||
<artifactId>HikariCP</artifactId>
|
||||
<version>5.1.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Metrics -->
|
||||
<dependency>
|
||||
<groupId>org.bstats</groupId>
|
||||
<artifactId>bstats-bukkit</artifactId>
|
||||
<version>3.0.2</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Plugin Integrations (Provided) -->
|
||||
<dependency>
|
||||
<groupId>net.milkbowl.vault</groupId>
|
||||
<artifactId>VaultAPI</artifactId>
|
||||
<version>1.7</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>me.clip</groupId>
|
||||
<artifactId>placeholderapi</artifactId>
|
||||
<version>2.11.5</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.luckperms</groupId>
|
||||
<artifactId>api</artifactId>
|
||||
<version>5.4</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON Processing -->
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Compression -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
<version>1.25.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Utilities -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.14.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Testing Dependencies -->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>5.8.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
<!-- Profile for Minecraft 1.8 (Java 8) -->
|
||||
<profile>
|
||||
<id>mc-1.8</id>
|
||||
<properties>
|
||||
<minecraft.version>1.8.8</minecraft.version>
|
||||
<java.version>8</java.version>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<!-- Profile for Minecraft 1.9-1.12 (Java 8) -->
|
||||
<profile>
|
||||
<id>mc-1.9</id>
|
||||
<properties>
|
||||
<minecraft.version>1.9.4</minecraft.version>
|
||||
<java.version>8</java.version>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>mc-1.10</id>
|
||||
<properties>
|
||||
<minecraft.version>1.10.2</minecraft.version>
|
||||
<java.version>8</java.version>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>mc-1.11</id>
|
||||
<properties>
|
||||
<minecraft.version>1.11.2</minecraft.version>
|
||||
<java.version>8</java.version>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>mc-1.12</id>
|
||||
<properties>
|
||||
<minecraft.version>1.12.2</minecraft.version>
|
||||
<java.version>8</java.version>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<!-- Profile for Minecraft 1.13-1.16 (Java 8) -->
|
||||
<profile>
|
||||
<id>mc-1.13</id>
|
||||
<properties>
|
||||
<minecraft.version>1.13.2</minecraft.version>
|
||||
<java.version>8</java.version>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>mc-1.14</id>
|
||||
<properties>
|
||||
<minecraft.version>1.14.4</minecraft.version>
|
||||
<java.version>8</java.version>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>mc-1.15</id>
|
||||
<properties>
|
||||
<minecraft.version>1.15.2</minecraft.version>
|
||||
<java.version>8</java.version>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>mc-1.16</id>
|
||||
<properties>
|
||||
<minecraft.version>1.16.5</minecraft.version>
|
||||
<java.version>8</java.version>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<!-- Profile for Minecraft 1.17 (Java 16) -->
|
||||
<profile>
|
||||
<id>mc-1.17</id>
|
||||
<properties>
|
||||
<minecraft.version>1.17.1</minecraft.version>
|
||||
<java.version>16</java.version>
|
||||
<maven.compiler.source>16</maven.compiler.source>
|
||||
<maven.compiler.target>16</maven.compiler.target>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<!-- Profile for Minecraft 1.18-1.20 (Java 17) -->
|
||||
<profile>
|
||||
<id>mc-1.18</id>
|
||||
<properties>
|
||||
<minecraft.version>1.18.2</minecraft.version>
|
||||
<java.version>17</java.version>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>mc-1.19</id>
|
||||
<properties>
|
||||
<minecraft.version>1.19.4</minecraft.version>
|
||||
<java.version>17</java.version>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>mc-1.20</id>
|
||||
<properties>
|
||||
<minecraft.version>1.20.4</minecraft.version>
|
||||
<java.version>17</java.version>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<!-- Profile for Minecraft 1.21+ (Java 21) -->
|
||||
<profile>
|
||||
<id>mc-1.21</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<minecraft.version>1.21</minecraft.version>
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>mc-1.21.1</id>
|
||||
<properties>
|
||||
<minecraft.version>1.21.1</minecraft.version>
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.2.2</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
<exclude>META-INF/MANIFEST.MF</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
<relocations>
|
||||
<relocation>
|
||||
<pattern>org.bstats</pattern>
|
||||
<shadedPattern>com.example.playerdatasync.premium.libs.bstats</shadedPattern>
|
||||
</relocation>
|
||||
<relocation>
|
||||
<pattern>com.zaxxer.hikari</pattern>
|
||||
<shadedPattern>com.example.playerdatasync.premium.libs.hikari</shadedPattern>
|
||||
</relocation>
|
||||
<relocation>
|
||||
<pattern>com.google.gson</pattern>
|
||||
<shadedPattern>com.example.playerdatasync.premium.libs.gson</shadedPattern>
|
||||
</relocation>
|
||||
<relocation>
|
||||
<pattern>org.apache.commons</pattern>
|
||||
<shadedPattern>com.example.playerdatasync.premium.libs.commons</shadedPattern>
|
||||
</relocation>
|
||||
</relocations>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,269 @@
|
||||
package com.example.playerdatasync.premium.api;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* License validator for PlayerDataSync Premium using CraftingStudio Pro API
|
||||
* API Documentation: https://www.craftingstudiopro.de/docs/api
|
||||
*
|
||||
* Validates license keys against the CraftingStudio Pro API
|
||||
*/
|
||||
public class LicenseValidator {
|
||||
private static final String API_BASE_URL = "https://craftingstudiopro.de/api";
|
||||
private static final String LICENSE_VALIDATE_ENDPOINT = "/license/validate";
|
||||
private static final String PLUGIN_ID = "playerdatasync-premium"; // Plugin slug or ID
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
private String cachedLicenseKey;
|
||||
private LicenseValidationResult cachedResult;
|
||||
private long lastValidationTime = 0;
|
||||
private static final long CACHE_DURATION_MS = TimeUnit.MINUTES.toMillis(30); // Cache for 30 minutes
|
||||
|
||||
public LicenseValidator(JavaPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a license key asynchronously
|
||||
*
|
||||
* @param licenseKey The license key to validate
|
||||
* @return CompletableFuture with validation result
|
||||
*/
|
||||
public CompletableFuture<LicenseValidationResult> validateLicenseAsync(String licenseKey) {
|
||||
// Check cache first
|
||||
if (licenseKey != null && licenseKey.equals(cachedLicenseKey) &&
|
||||
cachedResult != null &&
|
||||
(System.currentTimeMillis() - lastValidationTime) < CACHE_DURATION_MS) {
|
||||
return CompletableFuture.completedFuture(cachedResult);
|
||||
}
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
String apiUrl = API_BASE_URL + LICENSE_VALIDATE_ENDPOINT;
|
||||
HttpURLConnection connection;
|
||||
try {
|
||||
URI uri = new URI(apiUrl);
|
||||
connection = (HttpURLConnection) uri.toURL().openConnection();
|
||||
} catch (URISyntaxException e) {
|
||||
@SuppressWarnings("deprecation")
|
||||
URL fallbackUrl = new URL(apiUrl);
|
||||
connection = (HttpURLConnection) fallbackUrl.openConnection();
|
||||
}
|
||||
|
||||
connection.setConnectTimeout(10000);
|
||||
connection.setReadTimeout(10000);
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.setRequestProperty("User-Agent", "PlayerDataSync-Premium/" + plugin.getDescription().getVersion());
|
||||
connection.setRequestProperty("Accept", "application/json");
|
||||
connection.setDoOutput(true);
|
||||
|
||||
// Create request body
|
||||
JsonObject requestBody = new JsonObject();
|
||||
requestBody.addProperty("licenseKey", licenseKey);
|
||||
requestBody.addProperty("pluginId", PLUGIN_ID);
|
||||
|
||||
// Send request
|
||||
try (OutputStream os = connection.getOutputStream()) {
|
||||
byte[] input = requestBody.toString().getBytes(StandardCharsets.UTF_8);
|
||||
os.write(input, 0, input.length);
|
||||
}
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
|
||||
// Handle rate limiting
|
||||
if (responseCode == 429) {
|
||||
plugin.getLogger().warning("[LicenseValidator] Rate limit exceeded. Please try again later.");
|
||||
return new LicenseValidationResult(false, "Rate limit exceeded. Please try again later.", null);
|
||||
}
|
||||
|
||||
if (responseCode != 200) {
|
||||
String errorMessage = "HTTP " + responseCode;
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8))) {
|
||||
StringBuilder errorResponse = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
errorResponse.append(line);
|
||||
}
|
||||
if (errorResponse.length() > 0) {
|
||||
errorMessage = errorResponse.toString();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore error stream reading errors
|
||||
}
|
||||
plugin.getLogger().warning("[LicenseValidator] License validation failed: " + errorMessage);
|
||||
return new LicenseValidationResult(false, errorMessage, null);
|
||||
}
|
||||
|
||||
// Read response
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
|
||||
String jsonResponse = response.toString();
|
||||
if (jsonResponse == null || jsonResponse.trim().isEmpty()) {
|
||||
return new LicenseValidationResult(false, "Empty response from API", null);
|
||||
}
|
||||
|
||||
// Parse JSON response
|
||||
// Response format: { valid: boolean, message?: string, purchase?: { id: string, userId: string, pluginId: string, createdAt: string } }
|
||||
JsonObject jsonObject = JsonParser.parseString(jsonResponse).getAsJsonObject();
|
||||
|
||||
boolean valid = jsonObject.has("valid") && jsonObject.get("valid").getAsBoolean();
|
||||
String message = null;
|
||||
if (jsonObject.has("message") && !jsonObject.get("message").isJsonNull()) {
|
||||
message = jsonObject.get("message").getAsString();
|
||||
}
|
||||
|
||||
PurchaseInfo purchaseInfo = null;
|
||||
if (valid && jsonObject.has("purchase") && !jsonObject.get("purchase").isJsonNull()) {
|
||||
JsonObject purchaseObj = jsonObject.getAsJsonObject("purchase");
|
||||
purchaseInfo = new PurchaseInfo(
|
||||
purchaseObj.has("id") ? purchaseObj.get("id").getAsString() : null,
|
||||
purchaseObj.has("userId") ? purchaseObj.get("userId").getAsString() : null,
|
||||
purchaseObj.has("pluginId") ? purchaseObj.get("pluginId").getAsString() : null,
|
||||
purchaseObj.has("createdAt") ? purchaseObj.get("createdAt").getAsString() : null
|
||||
);
|
||||
}
|
||||
|
||||
LicenseValidationResult result = new LicenseValidationResult(valid, message, purchaseInfo);
|
||||
|
||||
// Cache result
|
||||
cachedLicenseKey = licenseKey;
|
||||
cachedResult = result;
|
||||
lastValidationTime = System.currentTimeMillis();
|
||||
|
||||
return result;
|
||||
}
|
||||
} catch (java.net.UnknownHostException e) {
|
||||
plugin.getLogger().warning("[LicenseValidator] No internet connection available for license validation.");
|
||||
return new LicenseValidationResult(false, "No internet connection", null);
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
plugin.getLogger().warning("[LicenseValidator] License validation timeout.");
|
||||
return new LicenseValidationResult(false, "Connection timeout", null);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe("[LicenseValidator] License validation error: " + e.getMessage());
|
||||
plugin.getLogger().log(java.util.logging.Level.FINE, "License validation error", e);
|
||||
return new LicenseValidationResult(false, "Validation error: " + e.getMessage(), null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate license key synchronously (blocks thread)
|
||||
* Use validateLicenseAsync() for non-blocking validation
|
||||
*/
|
||||
public LicenseValidationResult validateLicense(String licenseKey) {
|
||||
try {
|
||||
return validateLicenseAsync(licenseKey).get(10, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe("[LicenseValidator] Failed to validate license: " + e.getMessage());
|
||||
return new LicenseValidationResult(false, "Validation failed: " + e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the validation cache
|
||||
*/
|
||||
public void clearCache() {
|
||||
cachedLicenseKey = null;
|
||||
cachedResult = null;
|
||||
lastValidationTime = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cached validation is still valid
|
||||
*/
|
||||
public boolean isCacheValid() {
|
||||
return cachedResult != null &&
|
||||
(System.currentTimeMillis() - lastValidationTime) < CACHE_DURATION_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached validation result
|
||||
*/
|
||||
public LicenseValidationResult getCachedResult() {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* License validation result
|
||||
*/
|
||||
public static class LicenseValidationResult {
|
||||
private final boolean valid;
|
||||
private final String message;
|
||||
private final PurchaseInfo purchase;
|
||||
|
||||
public LicenseValidationResult(boolean valid, String message, PurchaseInfo purchase) {
|
||||
this.valid = valid;
|
||||
this.message = message;
|
||||
this.purchase = purchase;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public PurchaseInfo getPurchase() {
|
||||
return purchase;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purchase information from API
|
||||
*/
|
||||
public static class PurchaseInfo {
|
||||
private final String id;
|
||||
private final String userId;
|
||||
private final String pluginId;
|
||||
private final String createdAt;
|
||||
|
||||
public PurchaseInfo(String id, String userId, String pluginId, String createdAt) {
|
||||
this.id = id;
|
||||
this.userId = userId;
|
||||
this.pluginId = pluginId;
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public String getPluginId() {
|
||||
return pluginId;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.example.playerdatasync.premium.api;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Update checker for PlayerDataSync Premium using CraftingStudio Pro API
|
||||
* API Documentation: https://www.craftingstudiopro.de/docs/api
|
||||
*
|
||||
* Checks for updates using the Premium plugin slug
|
||||
*/
|
||||
public class PremiumUpdateChecker {
|
||||
private static final String API_BASE_URL = "https://craftingstudiopro.de/api";
|
||||
private static final String PLUGIN_SLUG = "playerdatasync-premium";
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
|
||||
public PremiumUpdateChecker(JavaPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for updates asynchronously
|
||||
*/
|
||||
public void check() {
|
||||
// Only check for updates if enabled in config
|
||||
if (!plugin.getConfig().getBoolean("update_checker.enabled", true)) {
|
||||
plugin.getLogger().info("[PremiumUpdateChecker] Update checking is disabled in config");
|
||||
return;
|
||||
}
|
||||
|
||||
SchedulerUtils.runTaskAsync(plugin, () -> {
|
||||
try {
|
||||
// Use CraftingStudio Pro API endpoint
|
||||
String apiUrl = API_BASE_URL + "/plugins/" + PLUGIN_SLUG + "/latest";
|
||||
HttpURLConnection connection;
|
||||
try {
|
||||
URI uri = new URI(apiUrl);
|
||||
connection = (HttpURLConnection) uri.toURL().openConnection();
|
||||
} catch (URISyntaxException e) {
|
||||
@SuppressWarnings("deprecation")
|
||||
URL fallbackUrl = new URL(apiUrl);
|
||||
connection = (HttpURLConnection) fallbackUrl.openConnection();
|
||||
}
|
||||
connection.setConnectTimeout(10000);
|
||||
connection.setReadTimeout(10000);
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setRequestProperty("User-Agent", "PlayerDataSync-Premium/" + plugin.getDescription().getVersion());
|
||||
connection.setRequestProperty("Accept", "application/json");
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
if (responseCode == 429) {
|
||||
plugin.getLogger().warning("[PremiumUpdateChecker] Rate limit exceeded. Please try again later.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (responseCode != 200) {
|
||||
plugin.getLogger().warning("[PremiumUpdateChecker] Update check failed: HTTP " + responseCode);
|
||||
return;
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
|
||||
String jsonResponse = response.toString();
|
||||
if (jsonResponse == null || jsonResponse.trim().isEmpty()) {
|
||||
plugin.getLogger().warning("[PremiumUpdateChecker] Empty response from API");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse JSON response using Gson
|
||||
// Response format: { version: string, downloadUrl: string, createdAt: string | null,
|
||||
// title: string, releaseType: "release", slug: string | null,
|
||||
// pluginTitle: string, pluginSlug: string }
|
||||
JsonObject jsonObject;
|
||||
try {
|
||||
jsonObject = JsonParser.parseString(jsonResponse).getAsJsonObject();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[PremiumUpdateChecker] Invalid JSON response: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!jsonObject.has("version")) {
|
||||
plugin.getLogger().warning("[PremiumUpdateChecker] Invalid response format: missing version field");
|
||||
return;
|
||||
}
|
||||
|
||||
final String latestVersion = jsonObject.get("version").getAsString();
|
||||
final String downloadUrl;
|
||||
if (jsonObject.has("downloadUrl") && !jsonObject.get("downloadUrl").isJsonNull()) {
|
||||
downloadUrl = jsonObject.get("downloadUrl").getAsString();
|
||||
} else {
|
||||
downloadUrl = null;
|
||||
}
|
||||
|
||||
String pluginTitle = "PlayerDataSync Premium";
|
||||
if (jsonObject.has("pluginTitle") && !jsonObject.get("pluginTitle").isJsonNull()) {
|
||||
pluginTitle = jsonObject.get("pluginTitle").getAsString();
|
||||
}
|
||||
|
||||
String currentVersion = plugin.getDescription().getVersion();
|
||||
if (currentVersion.equalsIgnoreCase(latestVersion)) {
|
||||
if (plugin.getConfig().getBoolean("update_checker.notify_ops", true)) {
|
||||
plugin.getLogger().info("[PremiumUpdateChecker] " + pluginTitle + " is up to date (v" + currentVersion + ")");
|
||||
}
|
||||
} else {
|
||||
plugin.getLogger().info("================================================");
|
||||
plugin.getLogger().info("[PremiumUpdateChecker] Update available for " + pluginTitle + "!");
|
||||
plugin.getLogger().info("[PremiumUpdateChecker] Current version: " + currentVersion);
|
||||
plugin.getLogger().info("[PremiumUpdateChecker] Latest version: " + latestVersion);
|
||||
if (downloadUrl != null && !downloadUrl.isEmpty()) {
|
||||
plugin.getLogger().info("[PremiumUpdateChecker] Download: " + downloadUrl);
|
||||
} else {
|
||||
plugin.getLogger().info("[PremiumUpdateChecker] Download: https://craftingstudiopro.de/plugins/" + PLUGIN_SLUG);
|
||||
}
|
||||
plugin.getLogger().info("================================================");
|
||||
|
||||
// Notify OPs if enabled
|
||||
if (plugin.getConfig().getBoolean("update_checker.notify_ops", true)) {
|
||||
SchedulerUtils.runTask(plugin, () -> {
|
||||
Bukkit.getOnlinePlayers().stream()
|
||||
.filter(p -> p.isOp() || p.hasPermission("playerdatasync.premium.admin"))
|
||||
.forEach(p -> {
|
||||
p.sendMessage("§8[§6PlayerDataSync Premium§8] §eUpdate available: v" + latestVersion);
|
||||
if (downloadUrl != null && !downloadUrl.isEmpty()) {
|
||||
p.sendMessage("§8[§6PlayerDataSync Premium§8] §7Download: §f" + downloadUrl);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (java.net.UnknownHostException e) {
|
||||
plugin.getLogger().fine("[PremiumUpdateChecker] No internet connection available for update check.");
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
plugin.getLogger().warning("[PremiumUpdateChecker] Update check timeout.");
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[PremiumUpdateChecker] Update check failed: " + e.getMessage());
|
||||
plugin.getLogger().log(java.util.logging.Level.FINE, "Update check error", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.example.playerdatasync.premium.commands;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import com.example.playerdatasync.premium.core.PlayerDataSyncPremium;
|
||||
import com.example.playerdatasync.premium.managers.LicenseManager;
|
||||
import com.example.playerdatasync.premium.api.PremiumUpdateChecker;
|
||||
import com.example.playerdatasync.premium.api.LicenseValidator.LicenseValidationResult;
|
||||
|
||||
/**
|
||||
* Premium command handler for license and update commands
|
||||
*
|
||||
* This class should be integrated into SyncCommand.java
|
||||
*/
|
||||
public class PremiumCommandHandler {
|
||||
private final PlayerDataSyncPremium plugin;
|
||||
|
||||
public PremiumCommandHandler(PlayerDataSyncPremium plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle license commands
|
||||
* Usage: /sync license [validate|info]
|
||||
*/
|
||||
public boolean handleLicense(CommandSender sender, String[] args) {
|
||||
if (!sender.hasPermission("playerdatasync.premium.admin.license")) {
|
||||
sender.sendMessage("§cYou don't have permission to use this command.");
|
||||
return true;
|
||||
}
|
||||
|
||||
LicenseManager licenseManager = plugin.getLicenseManager();
|
||||
if (licenseManager == null) {
|
||||
sender.sendMessage("§cLicense manager is not initialized.");
|
||||
return true;
|
||||
}
|
||||
|
||||
String action = args.length > 2 ? args[2].toLowerCase() : "info";
|
||||
|
||||
switch (action) {
|
||||
case "validate":
|
||||
case "revalidate":
|
||||
sender.sendMessage("§8[§6PlayerDataSync Premium§8] §7Validating license...");
|
||||
licenseManager.revalidateLicense().thenAccept(result -> {
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
if (result.isValid()) {
|
||||
sender.sendMessage("§8[§6PlayerDataSync Premium§8] §aLicense is valid!");
|
||||
if (result.getPurchase() != null) {
|
||||
sender.sendMessage("§8[§6PlayerDataSync Premium§8] §7Purchase ID: §f" + result.getPurchase().getId());
|
||||
sender.sendMessage("§8[§6PlayerDataSync Premium§8] §7User ID: §f" + result.getPurchase().getUserId());
|
||||
}
|
||||
} else {
|
||||
sender.sendMessage("§8[§6PlayerDataSync Premium§8] §cLicense validation failed!");
|
||||
sender.sendMessage("§8[§6PlayerDataSync Premium§8] §7Reason: §f" +
|
||||
(result.getMessage() != null ? result.getMessage() : "Unknown error"));
|
||||
}
|
||||
});
|
||||
});
|
||||
return true;
|
||||
|
||||
case "info":
|
||||
sender.sendMessage("§8[§m----------§r §6License Information §8§m----------");
|
||||
sender.sendMessage("§7License Key: §f" + licenseManager.getLicenseKey());
|
||||
sender.sendMessage("§7Status: " + (licenseManager.isLicenseValid() ? "§aValid" : "§cInvalid"));
|
||||
sender.sendMessage("§7Checked: " + (licenseManager.isLicenseChecked() ? "§aYes" : "§cNo"));
|
||||
sender.sendMessage("§8§m----------------------------------------");
|
||||
return true;
|
||||
|
||||
default:
|
||||
sender.sendMessage("§8[§6PlayerDataSync Premium§8] §7Usage: /sync license [validate|info]");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle update commands
|
||||
* Usage: /sync update [check]
|
||||
*/
|
||||
public boolean handleUpdate(CommandSender sender, String[] args) {
|
||||
if (!sender.hasPermission("playerdatasync.premium.admin.update")) {
|
||||
sender.sendMessage("§cYou don't have permission to use this command.");
|
||||
return true;
|
||||
}
|
||||
|
||||
PremiumUpdateChecker updateChecker = plugin.getUpdateChecker();
|
||||
if (updateChecker == null) {
|
||||
sender.sendMessage("§cUpdate checker is not initialized.");
|
||||
return true;
|
||||
}
|
||||
|
||||
String action = args.length > 2 ? args[2].toLowerCase() : "check";
|
||||
|
||||
switch (action) {
|
||||
case "check":
|
||||
sender.sendMessage("§8[§6PlayerDataSync Premium§8] §7Checking for updates...");
|
||||
updateChecker.check();
|
||||
sender.sendMessage("§8[§6PlayerDataSync Premium§8] §7Update check initiated. Check console for results.");
|
||||
return true;
|
||||
|
||||
default:
|
||||
sender.sendMessage("§8[§6PlayerDataSync Premium§8] §7Usage: /sync update check");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
package com.example.playerdatasync.premium.commands;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.example.playerdatasync.premium.core.PlayerDataSyncPremium;
|
||||
import com.example.playerdatasync.premium.commands.PremiumCommandHandler;
|
||||
import com.example.playerdatasync.premium.managers.AdvancementSyncManager;
|
||||
import com.example.playerdatasync.premium.managers.BackupManager;
|
||||
import com.example.playerdatasync.premium.managers.MessageManager;
|
||||
import com.example.playerdatasync.premium.utils.InventoryUtils;
|
||||
|
||||
/**
|
||||
* Enhanced sync command with expanded functionality
|
||||
*/
|
||||
public class SyncCommand implements CommandExecutor, TabCompleter {
|
||||
private final PlayerDataSyncPremium plugin;
|
||||
private final MessageManager messageManager;
|
||||
private final PremiumCommandHandler premiumHandler;
|
||||
|
||||
// Available sync options
|
||||
private static final List<String> SYNC_OPTIONS = Arrays.asList(
|
||||
"coordinates", "position", "xp", "gamemode", "inventory", "enderchest",
|
||||
"armor", "offhand", "health", "hunger", "effects", "achievements",
|
||||
"statistics", "attributes", "permissions", "economy"
|
||||
);
|
||||
|
||||
// Available sub-commands
|
||||
private static final List<String> SUB_COMMANDS = Arrays.asList(
|
||||
"reload", "status", "save", "help", "cache", "validate", "backup", "restore", "achievements", "license", "update"
|
||||
);
|
||||
|
||||
public SyncCommand(PlayerDataSyncPremium plugin) {
|
||||
this.plugin = plugin;
|
||||
this.messageManager = plugin.getMessageManager();
|
||||
this.premiumHandler = new PremiumCommandHandler(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length == 0) {
|
||||
return showStatus(sender);
|
||||
}
|
||||
|
||||
String subCommand = args[0].toLowerCase();
|
||||
|
||||
switch (subCommand) {
|
||||
case "reload":
|
||||
return handleReload(sender);
|
||||
|
||||
case "status":
|
||||
return handleStatus(sender, args);
|
||||
|
||||
case "save":
|
||||
return handleSave(sender, args);
|
||||
|
||||
case "help":
|
||||
return showHelp(sender);
|
||||
|
||||
case "cache":
|
||||
return handleCache(sender, args);
|
||||
|
||||
case "validate":
|
||||
return handleValidate(sender, args);
|
||||
|
||||
case "backup":
|
||||
return handleBackup(sender, args);
|
||||
|
||||
case "restore":
|
||||
return handleRestore(sender, args);
|
||||
|
||||
case "achievements":
|
||||
return handleAchievements(sender, args);
|
||||
|
||||
case "license":
|
||||
return premiumHandler.handleLicense(sender, args);
|
||||
|
||||
case "update":
|
||||
return premiumHandler.handleUpdate(sender, args);
|
||||
|
||||
default:
|
||||
// Try to parse as sync option
|
||||
if (args.length == 2) {
|
||||
return handleSyncOption(sender, args[0], args[1]);
|
||||
} else {
|
||||
return showHelp(sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show current sync status
|
||||
*/
|
||||
private boolean showStatus(CommandSender sender) {
|
||||
if (!hasPermission(sender, "playerdatasync.premium.premium.admin")) return true;
|
||||
|
||||
sender.sendMessage(messageManager.get("status_header"));
|
||||
sender.sendMessage(messageManager.get("status_version").replace("{version}", plugin.getDescription().getVersion()));
|
||||
|
||||
// Show sync options status
|
||||
for (String option : SYNC_OPTIONS) {
|
||||
boolean enabled = getSyncOptionValue(option);
|
||||
String status = enabled ? messageManager.get("sync_status_enabled") : messageManager.get("sync_status_disabled");
|
||||
sender.sendMessage(messageManager.get("sync_status")
|
||||
.replace("{option}", option)
|
||||
.replace("{status}", status));
|
||||
}
|
||||
|
||||
sender.sendMessage(messageManager.get("status_footer"));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle reload command
|
||||
*/
|
||||
private boolean handleReload(CommandSender sender) {
|
||||
if (!hasPermission(sender, "playerdatasync.premium.admin.reload")) return true;
|
||||
|
||||
try {
|
||||
plugin.reloadPlugin();
|
||||
sender.sendMessage(messageManager.get("prefix") + " " + messageManager.get("reloaded"));
|
||||
} catch (Exception e) {
|
||||
sender.sendMessage(messageManager.get("prefix") + " " +
|
||||
messageManager.get("reload_failed").replace("{error}", e.getMessage()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle status command for specific player
|
||||
*/
|
||||
private boolean handleStatus(CommandSender sender, String[] args) {
|
||||
if (!hasPermission(sender, "playerdatasync.premium.status")) return true;
|
||||
|
||||
Player target;
|
||||
if (args.length > 1) {
|
||||
if (!hasPermission(sender, "playerdatasync.premium.status.others")) return true;
|
||||
target = Bukkit.getPlayer(args[1]);
|
||||
if (target == null) {
|
||||
sender.sendMessage(messageManager.get("prefix") + " " +
|
||||
messageManager.get("player_not_found").replace("{player}", args[1]));
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (!(sender instanceof Player)) {
|
||||
sender.sendMessage(messageManager.get("prefix") + " " + messageManager.get("error_player_offline"));
|
||||
return true;
|
||||
}
|
||||
target = (Player) sender;
|
||||
}
|
||||
|
||||
showPlayerStatus(sender, target);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle save command
|
||||
*/
|
||||
private boolean handleSave(CommandSender sender, String[] args) {
|
||||
if (!hasPermission(sender, "playerdatasync.premium.admin.save")) return true;
|
||||
|
||||
if (args.length > 1) {
|
||||
// Save specific player
|
||||
Player target = Bukkit.getPlayer(args[1]);
|
||||
if (target == null) {
|
||||
sender.sendMessage(messageManager.get("prefix") + " " +
|
||||
messageManager.get("player_not_found").replace("{player}", args[1]));
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (plugin.getDatabaseManager().savePlayer(target)) {
|
||||
sender.sendMessage(messageManager.get("prefix") + " " +
|
||||
messageManager.get("manual_save_success"));
|
||||
} else {
|
||||
sender.sendMessage(messageManager.get("prefix") + " " +
|
||||
messageManager.get("manual_save_failed").replace("{error}",
|
||||
"Unable to persist player data. See console for details."));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
sender.sendMessage(messageManager.get("prefix") + " " +
|
||||
messageManager.get("manual_save_failed").replace("{error}", e.getMessage()));
|
||||
}
|
||||
} else {
|
||||
// Save all online players
|
||||
try {
|
||||
int savedCount = 0;
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (plugin.getDatabaseManager().savePlayer(player)) {
|
||||
savedCount++;
|
||||
}
|
||||
}
|
||||
sender.sendMessage(messageManager.get("prefix") + " " +
|
||||
"Saved data for " + savedCount + " players.");
|
||||
} catch (Exception e) {
|
||||
sender.sendMessage(messageManager.get("prefix") + " " +
|
||||
messageManager.get("manual_save_failed").replace("{error}", e.getMessage()));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle cache command
|
||||
*/
|
||||
private boolean handleCache(CommandSender sender, String[] args) {
|
||||
if (!hasPermission(sender, "playerdatasync.premium.premium.admin")) return true;
|
||||
|
||||
if (args.length > 1 && args[1].equalsIgnoreCase("clear")) {
|
||||
// Clear performance stats
|
||||
plugin.getDatabaseManager().resetPerformanceStats();
|
||||
InventoryUtils.resetDeserializationStats();
|
||||
sender.sendMessage(messageManager.get("prefix") + " " + "Performance and deserialization statistics cleared.");
|
||||
} else {
|
||||
// Show performance stats
|
||||
String stats = plugin.getDatabaseManager().getPerformanceStats();
|
||||
sender.sendMessage(messageManager.get("prefix") + " " + "Performance Stats: " + stats);
|
||||
|
||||
// Show connection pool stats if available
|
||||
if (plugin.getConnectionPool() != null) {
|
||||
sender.sendMessage(messageManager.get("prefix") + " " + "Connection Pool: " + plugin.getConnectionPool().getStats());
|
||||
}
|
||||
|
||||
// Show deserialization statistics
|
||||
String deserializationStats = InventoryUtils.getDeserializationStats();
|
||||
sender.sendMessage(messageManager.get("prefix") + " " + "Deserialization Stats: " + deserializationStats);
|
||||
|
||||
// Show helpful message if there are custom enchantment failures
|
||||
if (deserializationStats.contains("Custom Enchantments:") &&
|
||||
!deserializationStats.contains("Custom Enchantments: 0")) {
|
||||
sender.sendMessage("§e⚠ If you see custom enchantment failures, ensure enchantment plugins " +
|
||||
"(e.g., ExcellentEnchants) are loaded and all enchantments are registered.");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle validate command
|
||||
*/
|
||||
private boolean handleValidate(CommandSender sender, String[] args) {
|
||||
if (!hasPermission(sender, "playerdatasync.premium.premium.admin")) return true;
|
||||
|
||||
// Perform data validation (placeholder)
|
||||
sender.sendMessage(messageManager.get("prefix") + " Data validation completed.");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle backup command
|
||||
*/
|
||||
private boolean handleBackup(CommandSender sender, String[] args) {
|
||||
if (!hasPermission(sender, "playerdatasync.premium.admin.backup")) return true;
|
||||
|
||||
String backupType = args.length > 1 ? args[1] : "manual";
|
||||
|
||||
sender.sendMessage(messageManager.get("prefix") + " Creating backup...");
|
||||
|
||||
plugin.getBackupManager().createBackup(backupType).thenAccept(result -> {
|
||||
if (result.isSuccess()) {
|
||||
sender.sendMessage(messageManager.get("prefix") + " Backup created: " + result.getFileName() +
|
||||
" (" + formatFileSize(result.getFileSize()) + ")");
|
||||
} else {
|
||||
sender.sendMessage(messageManager.get("prefix") + " Backup failed!");
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle restore command
|
||||
*/
|
||||
private boolean handleRestore(CommandSender sender, String[] args) {
|
||||
if (!hasPermission(sender, "playerdatasync.premium.admin.restore")) return true;
|
||||
|
||||
if (args.length < 2) {
|
||||
// List available backups
|
||||
List<BackupManager.BackupInfo> backups = plugin.getBackupManager().listBackups();
|
||||
if (backups.isEmpty()) {
|
||||
sender.sendMessage(messageManager.get("prefix") + " No backups available.");
|
||||
} else {
|
||||
sender.sendMessage(messageManager.get("prefix") + " Available backups:");
|
||||
for (BackupManager.BackupInfo backup : backups) {
|
||||
sender.sendMessage("§7- §f" + backup.getFileName() + " §8(" + backup.getFormattedSize() +
|
||||
", " + backup.getCreatedDate() + ")");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
String backupName = args[1];
|
||||
sender.sendMessage(messageManager.get("prefix") + " Restoring from backup: " + backupName);
|
||||
|
||||
plugin.getBackupManager().restoreFromBackup(backupName).thenAccept(success -> {
|
||||
if (success) {
|
||||
sender.sendMessage(messageManager.get("prefix") + " Restore completed successfully!");
|
||||
} else {
|
||||
sender.sendMessage(messageManager.get("prefix") + " Restore failed!");
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleAchievements(CommandSender sender, String[] args) {
|
||||
if (!hasPermission(sender, "playerdatasync.premium.admin.achievements")) return true;
|
||||
|
||||
AdvancementSyncManager advancementSyncManager = plugin.getAdvancementSyncManager();
|
||||
if (advancementSyncManager == null) {
|
||||
sender.sendMessage(messageManager.get("prefix") + " Advancement manager is not available.");
|
||||
return true;
|
||||
}
|
||||
|
||||
String prefix = messageManager.get("prefix") + " ";
|
||||
|
||||
if (args.length == 1 || args[1].equalsIgnoreCase("status")) {
|
||||
sender.sendMessage(prefix + "Advancement cache: " + advancementSyncManager.getGlobalImportStatus());
|
||||
|
||||
if (args.length > 2) {
|
||||
Player target = Bukkit.getPlayer(args[2]);
|
||||
if (target == null) {
|
||||
sender.sendMessage(prefix + "Player '" + args[2] + "' is not online.");
|
||||
} else {
|
||||
sender.sendMessage(prefix + target.getName() + ": " +
|
||||
advancementSyncManager.getPlayerStatus(target.getUniqueId()));
|
||||
}
|
||||
} else if (sender instanceof Player) {
|
||||
Player player = (Player) sender;
|
||||
sender.sendMessage(prefix + "You: " +
|
||||
advancementSyncManager.getPlayerStatus(player.getUniqueId()));
|
||||
}
|
||||
|
||||
sender.sendMessage(prefix + "Use /sync achievements import [player] to queue an import.");
|
||||
return true;
|
||||
}
|
||||
|
||||
String action = args[1].toLowerCase();
|
||||
if (action.equals("import") || action.equals("preload")) {
|
||||
if (args.length == 2) {
|
||||
boolean started = advancementSyncManager.startGlobalImport(true);
|
||||
if (started) {
|
||||
sender.sendMessage(prefix + "Started global advancement cache rebuild.");
|
||||
} else if (advancementSyncManager.getGlobalImportStatus().startsWith("running")) {
|
||||
sender.sendMessage(prefix + "Global advancement cache rebuild is already running.");
|
||||
} else {
|
||||
sender.sendMessage(prefix + "Advancement cache already up to date. Use /sync achievements import again later to rebuild.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Player target = Bukkit.getPlayer(args[2]);
|
||||
if (target == null) {
|
||||
sender.sendMessage(prefix + "Player '" + args[2] + "' is not online.");
|
||||
return true;
|
||||
}
|
||||
|
||||
advancementSyncManager.forceRescan(target);
|
||||
sender.sendMessage(prefix + "Queued advancement import for " + target.getName() + ".");
|
||||
return true;
|
||||
}
|
||||
|
||||
sender.sendMessage(prefix + "Unknown achievements subcommand. Try /sync achievements status or /sync achievements import");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size for display
|
||||
*/
|
||||
private String formatFileSize(long bytes) {
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
|
||||
return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle sync option changes
|
||||
*/
|
||||
private boolean handleSyncOption(CommandSender sender, String option, String value) {
|
||||
if (!hasPermission(sender, "playerdatasync.premium.admin." + option)) return true;
|
||||
|
||||
if (!value.equalsIgnoreCase("true") && !value.equalsIgnoreCase("false")) {
|
||||
sender.sendMessage(messageManager.get("prefix") + " " +
|
||||
messageManager.get("invalid_syntax").replace("{usage}", "/sync <option> <true|false>"));
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean enabled = Boolean.parseBoolean(value);
|
||||
|
||||
if (setSyncOptionValue(option, enabled)) {
|
||||
String message = enabled ? messageManager.get("sync_enabled") : messageManager.get("sync_disabled");
|
||||
sender.sendMessage(messageManager.get("prefix") + " " +
|
||||
message.replace("{option}", option));
|
||||
} else {
|
||||
sender.sendMessage(messageManager.get("prefix") + " Unknown option: " + option);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show help information
|
||||
*/
|
||||
private boolean showHelp(CommandSender sender) {
|
||||
sender.sendMessage(messageManager.get("help_header"));
|
||||
sender.sendMessage(messageManager.get("help_sync"));
|
||||
sender.sendMessage(messageManager.get("help_sync_option"));
|
||||
sender.sendMessage(messageManager.get("help_sync_reload"));
|
||||
sender.sendMessage(messageManager.get("help_sync_save"));
|
||||
sender.sendMessage("§b/sync status [player] §8- §7Check sync status");
|
||||
sender.sendMessage("§b/sync cache [clear] §8- §7Manage cache and performance stats");
|
||||
sender.sendMessage("§b/sync validate §8- §7Validate data integrity");
|
||||
sender.sendMessage("§b/sync backup [type] §8- §7Create manual backup");
|
||||
sender.sendMessage("§b/sync restore [backup] §8- §7Restore from backup");
|
||||
sender.sendMessage("§b/sync help §8- §7Show this help");
|
||||
sender.sendMessage(messageManager.get("help_footer"));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show player-specific status
|
||||
*/
|
||||
private void showPlayerStatus(CommandSender sender, Player player) {
|
||||
sender.sendMessage("§8§m----------§r §bPlayer Status: " + player.getName() + " §8§m----------");
|
||||
sender.sendMessage("§7Online: §aYes");
|
||||
sender.sendMessage("§7World: §f" + player.getWorld().getName());
|
||||
sender.sendMessage("§7Location: §f" +
|
||||
String.format("%.1f, %.1f, %.1f", player.getLocation().getX(),
|
||||
player.getLocation().getY(), player.getLocation().getZ()));
|
||||
// Get max health safely
|
||||
double maxHealth = 20.0;
|
||||
try {
|
||||
if (com.example.playerdatasync.premium.utils.VersionCompatibility.isAttributesSupported()) {
|
||||
org.bukkit.attribute.AttributeInstance attr = player.getAttribute(org.bukkit.attribute.Attribute.GENERIC_MAX_HEALTH);
|
||||
if (attr != null) {
|
||||
maxHealth = attr.getValue();
|
||||
}
|
||||
} else {
|
||||
@SuppressWarnings("deprecation")
|
||||
double tempMax = player.getMaxHealth();
|
||||
maxHealth = tempMax;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
maxHealth = 20.0;
|
||||
}
|
||||
sender.sendMessage("§7Health: §f" + String.format("%.1f/%.1f", player.getHealth(), maxHealth));
|
||||
sender.sendMessage("§7Food Level: §f" + player.getFoodLevel() + "/20");
|
||||
sender.sendMessage("§7XP Level: §f" + player.getLevel());
|
||||
sender.sendMessage("§7Game Mode: §f" + player.getGameMode().toString());
|
||||
sender.sendMessage("§8§m----------------------------------------");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sync option value
|
||||
*/
|
||||
private boolean getSyncOptionValue(String option) {
|
||||
switch (option.toLowerCase()) {
|
||||
case "coordinates": return plugin.isSyncCoordinates();
|
||||
case "position": return plugin.isSyncPosition();
|
||||
case "xp": return plugin.isSyncXp();
|
||||
case "gamemode": return plugin.isSyncGamemode();
|
||||
case "inventory": return plugin.isSyncInventory();
|
||||
case "enderchest": return plugin.isSyncEnderchest();
|
||||
case "armor": return plugin.isSyncArmor();
|
||||
case "offhand": return plugin.isSyncOffhand();
|
||||
case "health": return plugin.isSyncHealth();
|
||||
case "hunger": return plugin.isSyncHunger();
|
||||
case "effects": return plugin.isSyncEffects();
|
||||
case "achievements": return plugin.isSyncAchievements();
|
||||
case "statistics": return plugin.isSyncStatistics();
|
||||
case "attributes": return plugin.isSyncAttributes();
|
||||
case "permissions": return plugin.isSyncPermissions();
|
||||
case "economy": return plugin.isSyncEconomy();
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set sync option value
|
||||
*/
|
||||
private boolean setSyncOptionValue(String option, boolean value) {
|
||||
switch (option.toLowerCase()) {
|
||||
case "coordinates": plugin.setSyncCoordinates(value); return true;
|
||||
case "position": plugin.setSyncPosition(value); return true;
|
||||
case "xp": plugin.setSyncXp(value); return true;
|
||||
case "gamemode": plugin.setSyncGamemode(value); return true;
|
||||
case "inventory": plugin.setSyncInventory(value); return true;
|
||||
case "enderchest": plugin.setSyncEnderchest(value); return true;
|
||||
case "armor": plugin.setSyncArmor(value); return true;
|
||||
case "offhand": plugin.setSyncOffhand(value); return true;
|
||||
case "health": plugin.setSyncHealth(value); return true;
|
||||
case "hunger": plugin.setSyncHunger(value); return true;
|
||||
case "effects": plugin.setSyncEffects(value); return true;
|
||||
case "achievements": plugin.setSyncAchievements(value); return true;
|
||||
case "statistics": plugin.setSyncStatistics(value); return true;
|
||||
case "attributes": plugin.setSyncAttributes(value); return true;
|
||||
case "permissions": plugin.setSyncPermissions(value); return true;
|
||||
case "economy": plugin.setSyncEconomy(value); return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if sender has permission
|
||||
*/
|
||||
private boolean hasPermission(CommandSender sender, String permission) {
|
||||
if (sender.hasPermission(permission) || sender.hasPermission("playerdatasync.premium.admin.*")) {
|
||||
return true;
|
||||
}
|
||||
sender.sendMessage(messageManager.get("prefix") + " " + messageManager.get("no_permission"));
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
List<String> completions = new ArrayList<>();
|
||||
|
||||
if (args.length == 1) {
|
||||
// First argument: subcommands or sync options
|
||||
completions.addAll(SUB_COMMANDS);
|
||||
completions.addAll(SYNC_OPTIONS);
|
||||
return completions.stream()
|
||||
.filter(s -> s.toLowerCase().startsWith(args[0].toLowerCase()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (args.length == 2) {
|
||||
String firstArg = args[0].toLowerCase();
|
||||
|
||||
if (SYNC_OPTIONS.contains(firstArg)) {
|
||||
// Boolean values for sync options
|
||||
return Arrays.asList("true", "false").stream()
|
||||
.filter(s -> s.startsWith(args[1].toLowerCase()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (firstArg.equals("status") || firstArg.equals("save")) {
|
||||
// Player names
|
||||
return Bukkit.getOnlinePlayers().stream()
|
||||
.map(Player::getName)
|
||||
.filter(name -> name.toLowerCase().startsWith(args[1].toLowerCase()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (firstArg.equals("cache")) {
|
||||
return Arrays.asList("clear", "stats").stream()
|
||||
.filter(s -> s.startsWith(args[1].toLowerCase()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (firstArg.equals("backup")) {
|
||||
return Arrays.asList("manual", "automatic", "scheduled").stream()
|
||||
.filter(s -> s.startsWith(args[1].toLowerCase()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (firstArg.equals("achievements")) {
|
||||
return Arrays.asList("status", "import").stream()
|
||||
.filter(s -> s.startsWith(args[1].toLowerCase()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (firstArg.equals("restore")) {
|
||||
// List available backup files
|
||||
return plugin.getBackupManager().listBackups().stream()
|
||||
.map(BackupManager.BackupInfo::getFileName)
|
||||
.filter(name -> name.toLowerCase().startsWith(args[1].toLowerCase()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
if (args.length == 3) {
|
||||
if (args[0].equalsIgnoreCase("achievements")) {
|
||||
String second = args[1].toLowerCase();
|
||||
if (second.equals("status") || second.equals("import") || second.equals("preload")) {
|
||||
return Bukkit.getOnlinePlayers().stream()
|
||||
.map(Player::getName)
|
||||
.filter(name -> name.toLowerCase().startsWith(args[2].toLowerCase()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return completions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,910 @@
|
||||
package com.example.playerdatasync.premium.core;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import net.milkbowl.vault.economy.Economy;
|
||||
|
||||
import com.example.playerdatasync.premium.database.ConnectionPool;
|
||||
import com.example.playerdatasync.premium.database.DatabaseManager;
|
||||
import com.example.playerdatasync.premium.integration.InventoryViewerIntegrationManager;
|
||||
import com.example.playerdatasync.premium.listeners.PlayerDataListener;
|
||||
import com.example.playerdatasync.premium.listeners.ServerSwitchListener;
|
||||
import com.example.playerdatasync.premium.managers.AdvancementSyncManager;
|
||||
import com.example.playerdatasync.premium.managers.BackupManager;
|
||||
import com.example.playerdatasync.premium.managers.ConfigManager;
|
||||
import com.example.playerdatasync.premium.managers.MessageManager;
|
||||
import com.example.playerdatasync.premium.commands.SyncCommand;
|
||||
import com.example.playerdatasync.premium.api.PremiumUpdateChecker;
|
||||
import com.example.playerdatasync.premium.api.LicenseValidator;
|
||||
import com.example.playerdatasync.premium.managers.LicenseManager;
|
||||
import com.example.playerdatasync.premium.utils.VersionCompatibility;
|
||||
import com.example.playerdatasync.premium.utils.SchedulerUtils;
|
||||
|
||||
import com.google.common.io.ByteArrayDataOutput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* PlayerDataSync Premium - Premium version with license validation
|
||||
*
|
||||
* This is the premium version of PlayerDataSync that requires a valid license key
|
||||
* from CraftingStudio Pro to function.
|
||||
*/
|
||||
public class PlayerDataSyncPremium extends JavaPlugin {
|
||||
private Connection connection;
|
||||
private ConnectionPool connectionPool;
|
||||
private String databaseType;
|
||||
private String databaseUrl;
|
||||
private String databaseUser;
|
||||
private String databasePassword;
|
||||
private String tablePrefix;
|
||||
|
||||
// Basic sync options
|
||||
private boolean syncCoordinates;
|
||||
private boolean syncXp;
|
||||
private boolean syncGamemode;
|
||||
private boolean syncEnderchest;
|
||||
private boolean syncInventory;
|
||||
private boolean syncHealth;
|
||||
private boolean syncHunger;
|
||||
private boolean syncPosition;
|
||||
private boolean syncAchievements;
|
||||
|
||||
// Extended sync options
|
||||
private boolean syncArmor;
|
||||
private boolean syncOffhand;
|
||||
private boolean syncEffects;
|
||||
private boolean syncStatistics;
|
||||
private boolean syncAttributes;
|
||||
private boolean syncPermissions;
|
||||
private boolean syncEconomy;
|
||||
private Economy economyProvider;
|
||||
|
||||
private boolean bungeecordIntegrationEnabled;
|
||||
|
||||
private DatabaseManager databaseManager;
|
||||
private AdvancementSyncManager advancementSyncManager;
|
||||
private ConfigManager configManager;
|
||||
private BackupManager backupManager;
|
||||
private InventoryViewerIntegrationManager inventoryViewerIntegrationManager;
|
||||
private int autosaveIntervalSeconds;
|
||||
private BukkitTask autosaveTask;
|
||||
private MessageManager messageManager;
|
||||
private Metrics metrics;
|
||||
|
||||
// Premium components
|
||||
private LicenseManager licenseManager;
|
||||
private PremiumUpdateChecker updateChecker;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
getLogger().info("================================================");
|
||||
getLogger().info("Enabling PlayerDataSync Premium...");
|
||||
getLogger().info("================================================");
|
||||
|
||||
// Check server version compatibility
|
||||
checkVersionCompatibility();
|
||||
|
||||
// Initialize configuration first
|
||||
getLogger().info("Saving default configuration...");
|
||||
saveDefaultConfig();
|
||||
|
||||
// Ensure config file exists and is not empty
|
||||
if (getConfig().getKeys(false).isEmpty()) {
|
||||
getLogger().warning("Configuration file is empty! Recreating from defaults...");
|
||||
reloadConfig();
|
||||
saveDefaultConfig();
|
||||
|
||||
if (getConfig().getKeys(false).isEmpty()) {
|
||||
getLogger().severe("CRITICAL: Failed to load configuration! Plugin will be disabled.");
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
configManager = new ConfigManager(this);
|
||||
tablePrefix = configManager.getTablePrefix();
|
||||
|
||||
Level configuredLevel = configManager.getLoggingLevel();
|
||||
if (configManager.isDebugMode() && configuredLevel.intValue() > Level.FINE.intValue()) {
|
||||
configuredLevel = Level.FINE;
|
||||
}
|
||||
getLogger().setLevel(configuredLevel);
|
||||
|
||||
// Initialize message manager
|
||||
messageManager = new MessageManager(this);
|
||||
String lang = getConfig().getString("messages.language", "en");
|
||||
try {
|
||||
messageManager.load(lang);
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("Failed to load messages for language " + lang + ", falling back to English");
|
||||
try {
|
||||
messageManager.load("en");
|
||||
} catch (Exception e2) {
|
||||
getLogger().severe("Failed to load any message files: " + e2.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (getConfig().getBoolean("metrics", true)) {
|
||||
if (metrics == null) {
|
||||
metrics = new Metrics(this, 25037);
|
||||
}
|
||||
} else {
|
||||
metrics = null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// PREMIUM: Initialize License Validation
|
||||
// ========================================
|
||||
getLogger().info("Initializing license validation...");
|
||||
licenseManager = new LicenseManager(this);
|
||||
licenseManager.initialize();
|
||||
|
||||
// Wait a bit for license validation to complete (async)
|
||||
try {
|
||||
Thread.sleep(3000); // Wait 3 seconds for initial validation
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
// Check if license is valid
|
||||
if (!licenseManager.isLicenseValid()) {
|
||||
getLogger().severe("================================================");
|
||||
getLogger().severe("PlayerDataSync Premium - LICENSE VALIDATION FAILED!");
|
||||
getLogger().severe("The plugin requires a valid license key to function.");
|
||||
getLogger().severe("Please configure your license key in config.yml:");
|
||||
getLogger().severe("license:");
|
||||
getLogger().severe(" key: YOUR-LICENSE-KEY-HERE");
|
||||
getLogger().severe("================================================");
|
||||
getLogger().severe("The plugin will be disabled in 30 seconds if the license is not valid.");
|
||||
|
||||
// Disable plugin after 30 seconds if license is still invalid
|
||||
SchedulerUtils.runTaskLater(this, () -> {
|
||||
if (!licenseManager.isLicenseValid()) {
|
||||
getLogger().severe("License is still invalid. Disabling plugin...");
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
}
|
||||
}, 600L); // 30 seconds
|
||||
|
||||
// Don't continue initialization if license is invalid
|
||||
// But allow some time for validation to complete
|
||||
return;
|
||||
}
|
||||
|
||||
getLogger().info("License validated successfully! Continuing initialization...");
|
||||
|
||||
// Initialize database connection
|
||||
databaseType = getConfig().getString("database.type", "mysql");
|
||||
try {
|
||||
if (databaseType.equalsIgnoreCase("mysql")) {
|
||||
String host = getConfig().getString("database.mysql.host", "localhost");
|
||||
int port = getConfig().getInt("database.mysql.port", 3306);
|
||||
String database = getConfig().getString("database.mysql.database", "minecraft");
|
||||
databaseUser = getConfig().getString("database.mysql.user", "root");
|
||||
databasePassword = getConfig().getString("database.mysql.password", "");
|
||||
|
||||
databaseUrl = String.format("jdbc:mysql://%s:%d/%s?useSSL=%s&autoReconnect=true&failOverReadOnly=false&maxReconnects=3",
|
||||
host, port, database, getConfig().getBoolean("database.mysql.ssl", false));
|
||||
|
||||
connection = DriverManager.getConnection(databaseUrl, databaseUser, databasePassword);
|
||||
getLogger().info("Connected to MySQL database at " + host + ":" + port + "/" + database);
|
||||
|
||||
if (getConfig().getBoolean("performance.connection_pooling", true)) {
|
||||
int maxConnections = getConfig().getInt("database.mysql.max_connections", 10);
|
||||
connectionPool = new ConnectionPool(this, databaseUrl, databaseUser, databasePassword, maxConnections);
|
||||
connectionPool.initialize();
|
||||
}
|
||||
} else if (databaseType.equalsIgnoreCase("sqlite")) {
|
||||
String file = getConfig().getString("database.sqlite.file", "plugins/PlayerDataSync-Premium/playerdata.db");
|
||||
java.io.File dbFile = new java.io.File(file);
|
||||
if (!dbFile.getParentFile().exists()) {
|
||||
dbFile.getParentFile().mkdirs();
|
||||
}
|
||||
databaseUrl = "jdbc:sqlite:" + file;
|
||||
databaseUser = null;
|
||||
databasePassword = null;
|
||||
connection = DriverManager.getConnection(databaseUrl);
|
||||
getLogger().info("Connected to SQLite database at " + file);
|
||||
} else {
|
||||
getLogger().severe("Unsupported database type: " + databaseType + ". Supported types: mysql, sqlite");
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().severe("Could not connect to " + databaseType + " database: " + e.getMessage());
|
||||
getLogger().severe("Please check your database configuration and ensure the database server is running");
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
|
||||
loadSyncSettings();
|
||||
advancementSyncManager = new AdvancementSyncManager(this);
|
||||
bungeecordIntegrationEnabled = getConfig().getBoolean("integrations.bungeecord", false);
|
||||
if (bungeecordIntegrationEnabled) {
|
||||
getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord");
|
||||
getLogger().info("BungeeCord integration enabled. Plugin messaging channel registered.");
|
||||
}
|
||||
|
||||
autosaveIntervalSeconds = getConfig().getInt("autosave.interval", 1);
|
||||
|
||||
if (autosaveIntervalSeconds > 0) {
|
||||
long ticks = autosaveIntervalSeconds * 20L;
|
||||
autosaveTask = SchedulerUtils.runTaskTimerAsync(this, () -> {
|
||||
try {
|
||||
int savedCount = 0;
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
try {
|
||||
if (databaseManager.savePlayer(player)) {
|
||||
savedCount++;
|
||||
} else {
|
||||
getLogger().warning("Failed to autosave data for " + player.getName()
|
||||
+ ": See previous log entries for details.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("Failed to autosave data for " + player.getName() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
if (savedCount > 0 && isPerformanceLoggingEnabled()) {
|
||||
getLogger().info("Autosaved data for " + savedCount + " players in " +
|
||||
(endTime - startTime) + "ms");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().severe("Error during autosave: " + e.getMessage());
|
||||
}
|
||||
}, ticks, ticks);
|
||||
getLogger().info("Autosave task scheduled with interval: " + autosaveIntervalSeconds + " seconds");
|
||||
}
|
||||
|
||||
databaseManager = new DatabaseManager(this);
|
||||
databaseManager.initialize();
|
||||
|
||||
boolean invSeeIntegration = getConfig().getBoolean("integrations.invsee", true);
|
||||
boolean openInvIntegration = getConfig().getBoolean("integrations.openinv", true);
|
||||
if (invSeeIntegration || openInvIntegration) {
|
||||
inventoryViewerIntegrationManager = new InventoryViewerIntegrationManager(this, databaseManager,
|
||||
invSeeIntegration, openInvIntegration);
|
||||
}
|
||||
|
||||
configureEconomyIntegration();
|
||||
|
||||
// Initialize backup manager
|
||||
backupManager = new BackupManager(this);
|
||||
backupManager.startAutomaticBackups();
|
||||
|
||||
getServer().getPluginManager().registerEvents(new PlayerDataListener(this, databaseManager), this);
|
||||
getServer().getPluginManager().registerEvents(new ServerSwitchListener(this, databaseManager), this);
|
||||
if (getCommand("sync") != null) {
|
||||
SyncCommand syncCommand = new SyncCommand(this);
|
||||
getCommand("sync").setExecutor(syncCommand);
|
||||
getCommand("sync").setTabCompleter(syncCommand);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// PREMIUM: Initialize Update Checker
|
||||
// ========================================
|
||||
updateChecker = new PremiumUpdateChecker(this);
|
||||
updateChecker.check();
|
||||
|
||||
if (SchedulerUtils.isFolia()) {
|
||||
getLogger().info("Folia detected - using Folia-compatible schedulers");
|
||||
}
|
||||
|
||||
getLogger().info("================================================");
|
||||
getLogger().info("PlayerDataSync Premium enabled successfully!");
|
||||
getLogger().info("Version: " + getDescription().getVersion());
|
||||
getLogger().info("License: Valid");
|
||||
getLogger().info("================================================");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
getLogger().info("Disabling PlayerDataSync Premium...");
|
||||
|
||||
// Cancel autosave task
|
||||
if (autosaveTask != null) {
|
||||
autosaveTask.cancel();
|
||||
autosaveTask = null;
|
||||
getLogger().info("Autosave task cancelled");
|
||||
}
|
||||
|
||||
if (bungeecordIntegrationEnabled) {
|
||||
getServer().getMessenger().unregisterOutgoingPluginChannel(this);
|
||||
}
|
||||
|
||||
// Save all online players before shutdown
|
||||
if (databaseManager != null) {
|
||||
try {
|
||||
int savedCount = 0;
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
if (syncEconomy) {
|
||||
getLogger().info("Reconfiguring economy integration for shutdown save...");
|
||||
configureEconomyIntegration();
|
||||
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
try {
|
||||
if (syncEconomy && economyProvider != null) {
|
||||
try {
|
||||
double currentBalance = economyProvider.getBalance(player);
|
||||
getLogger().fine("Current balance for " + player.getName() + " before shutdown save: " + currentBalance);
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("Could not read balance for " + player.getName() + " before shutdown: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (databaseManager.savePlayer(player)) {
|
||||
savedCount++;
|
||||
getLogger().fine("Saved data for " + player.getName() + " during shutdown");
|
||||
} else {
|
||||
getLogger().severe("Failed to save data for " + player.getName()
|
||||
+ " during shutdown: See previous log entries for details.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().severe("Failed to save data for " + player.getName() + " during shutdown: " + e.getMessage());
|
||||
getLogger().log(java.util.logging.Level.SEVERE, "Stack trace:", e);
|
||||
}
|
||||
}
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
if (savedCount > 0) {
|
||||
getLogger().info("Saved data for " + savedCount + " players during shutdown in " +
|
||||
(endTime - startTime) + "ms (including economy balances)");
|
||||
} else {
|
||||
getLogger().warning("No players were saved during shutdown - this may cause data loss!");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().severe("Error saving players during shutdown: " + e.getMessage());
|
||||
getLogger().log(java.util.logging.Level.SEVERE, "Stack trace:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop backup manager
|
||||
if (backupManager != null) {
|
||||
backupManager.stopAutomaticBackups();
|
||||
backupManager = null;
|
||||
}
|
||||
|
||||
if (advancementSyncManager != null) {
|
||||
advancementSyncManager.shutdown();
|
||||
advancementSyncManager = null;
|
||||
}
|
||||
|
||||
if (inventoryViewerIntegrationManager != null) {
|
||||
inventoryViewerIntegrationManager.shutdown();
|
||||
inventoryViewerIntegrationManager = null;
|
||||
}
|
||||
|
||||
// Shutdown license manager
|
||||
if (licenseManager != null) {
|
||||
licenseManager.shutdown();
|
||||
licenseManager = null;
|
||||
}
|
||||
|
||||
// Shutdown connection pool
|
||||
if (connectionPool != null) {
|
||||
connectionPool.shutdown();
|
||||
connectionPool = null;
|
||||
}
|
||||
|
||||
// Close database connection
|
||||
if (connection != null) {
|
||||
try {
|
||||
connection.close();
|
||||
if (databaseType.equalsIgnoreCase("mysql")) {
|
||||
getLogger().info("MySQL connection closed");
|
||||
} else {
|
||||
getLogger().info("SQLite connection closed");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().severe("Error closing database connection: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
getLogger().info("PlayerDataSync Premium disabled successfully");
|
||||
}
|
||||
|
||||
// ... (Copy all other methods from PlayerDataSync.java)
|
||||
// For brevity, I'll include the essential methods and refer to the original file for the rest
|
||||
|
||||
private Connection createConnection() throws SQLException {
|
||||
if (databaseType.equalsIgnoreCase("mysql")) {
|
||||
return DriverManager.getConnection(databaseUrl, databaseUser, databasePassword);
|
||||
}
|
||||
return DriverManager.getConnection(databaseUrl);
|
||||
}
|
||||
|
||||
public synchronized Connection getConnection() {
|
||||
try {
|
||||
if (connectionPool != null) {
|
||||
return connectionPool.getConnection();
|
||||
}
|
||||
|
||||
if (connection == null || connection.isClosed() || !connection.isValid(2)) {
|
||||
connection = createConnection();
|
||||
getLogger().info("Reconnected to database");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().severe("Could not establish database connection: " + e.getMessage());
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
public void returnConnection(Connection conn) {
|
||||
if (connectionPool != null && conn != null) {
|
||||
connectionPool.returnConnection(conn);
|
||||
}
|
||||
}
|
||||
|
||||
// ... (Include all sync option getters/setters and other methods from PlayerDataSync.java)
|
||||
// For now, I'll create a simplified version - you'll need to copy the full implementation
|
||||
|
||||
private void loadSyncSettings() {
|
||||
syncCoordinates = getConfig().getBoolean("sync.coordinates", true);
|
||||
syncXp = getConfig().getBoolean("sync.xp", true);
|
||||
syncGamemode = getConfig().getBoolean("sync.gamemode", true);
|
||||
syncEnderchest = getConfig().getBoolean("sync.enderchest", true);
|
||||
syncInventory = getConfig().getBoolean("sync.inventory", true);
|
||||
syncHealth = getConfig().getBoolean("sync.health", true);
|
||||
syncHunger = getConfig().getBoolean("sync.hunger", true);
|
||||
syncPosition = getConfig().getBoolean("sync.position", true);
|
||||
|
||||
syncArmor = getConfig().getBoolean("sync.armor", true);
|
||||
|
||||
syncOffhand = VersionCompatibility.isOffhandSupported() &&
|
||||
getConfig().getBoolean("sync.offhand", true);
|
||||
if (!VersionCompatibility.isOffhandSupported() && getConfig().getBoolean("sync.offhand", true)) {
|
||||
getLogger().info("Offhand sync disabled - requires Minecraft 1.9+");
|
||||
getConfig().set("sync.offhand", false);
|
||||
}
|
||||
|
||||
syncEffects = getConfig().getBoolean("sync.effects", true);
|
||||
syncStatistics = getConfig().getBoolean("sync.statistics", true);
|
||||
|
||||
syncAttributes = VersionCompatibility.isAttributesSupported() &&
|
||||
getConfig().getBoolean("sync.attributes", true);
|
||||
if (!VersionCompatibility.isAttributesSupported() && getConfig().getBoolean("sync.attributes", true)) {
|
||||
getLogger().info("Attribute sync disabled - requires Minecraft 1.9+");
|
||||
getConfig().set("sync.attributes", false);
|
||||
}
|
||||
|
||||
syncAchievements = VersionCompatibility.isAdvancementsSupported() &&
|
||||
getConfig().getBoolean("sync.achievements", true);
|
||||
if (!VersionCompatibility.isAdvancementsSupported() && getConfig().getBoolean("sync.achievements", true)) {
|
||||
getLogger().info("Advancement sync disabled - requires Minecraft 1.12+");
|
||||
getConfig().set("sync.achievements", false);
|
||||
}
|
||||
|
||||
syncPermissions = getConfig().getBoolean("sync.permissions", false);
|
||||
syncEconomy = getConfig().getBoolean("sync.economy", false);
|
||||
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
private void checkVersionCompatibility() {
|
||||
if (!getConfig().getBoolean("compatibility.version_check", true)) {
|
||||
getLogger().info("Version compatibility checking is disabled in config");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String serverVersion = Bukkit.getServer().getBukkitVersion();
|
||||
String pluginApiVersion = getDescription().getAPIVersion();
|
||||
|
||||
getLogger().info("Server version: " + serverVersion);
|
||||
getLogger().info("Plugin API version: " + pluginApiVersion);
|
||||
|
||||
boolean isSupportedVersion = false;
|
||||
String versionInfo = "";
|
||||
|
||||
if (VersionCompatibility.isVersion1_8()) {
|
||||
isSupportedVersion = true;
|
||||
versionInfo = "Minecraft 1.8 - Full compatibility confirmed";
|
||||
} else if (VersionCompatibility.isVersion1_9_to_1_11()) {
|
||||
isSupportedVersion = true;
|
||||
versionInfo = "Minecraft 1.9-1.11 - Full compatibility confirmed";
|
||||
} else if (VersionCompatibility.isVersion1_12()) {
|
||||
isSupportedVersion = true;
|
||||
versionInfo = "Minecraft 1.12 - Full compatibility confirmed";
|
||||
} else if (VersionCompatibility.isVersion1_13_to_1_16()) {
|
||||
isSupportedVersion = true;
|
||||
versionInfo = "Minecraft 1.13-1.16 - Full compatibility confirmed";
|
||||
} else if (VersionCompatibility.isVersion1_17()) {
|
||||
isSupportedVersion = true;
|
||||
versionInfo = "Minecraft 1.17 - Full compatibility confirmed";
|
||||
} else if (VersionCompatibility.isVersion1_18_to_1_20()) {
|
||||
isSupportedVersion = true;
|
||||
versionInfo = "Minecraft 1.18-1.20 - Full compatibility confirmed";
|
||||
} else if (VersionCompatibility.isVersion1_21_Plus()) {
|
||||
isSupportedVersion = true;
|
||||
versionInfo = "Minecraft 1.21+ - Full compatibility confirmed";
|
||||
}
|
||||
|
||||
if (isSupportedVersion) {
|
||||
getLogger().info("✅ " + versionInfo);
|
||||
} else {
|
||||
getLogger().warning("================================================");
|
||||
getLogger().warning("VERSION COMPATIBILITY WARNING:");
|
||||
getLogger().warning("This plugin supports Minecraft 1.8 to 1.21.11");
|
||||
getLogger().warning("Current server version: " + serverVersion);
|
||||
getLogger().warning("Some features may not work correctly");
|
||||
getLogger().warning("================================================");
|
||||
}
|
||||
|
||||
if (VersionCompatibility.isAttributesSupported()) {
|
||||
try {
|
||||
org.bukkit.attribute.Attribute.values();
|
||||
getLogger().info("Attribute API compatibility: OK");
|
||||
} catch (Exception e) {
|
||||
getLogger().severe("CRITICAL: Attribute API compatibility issue detected!");
|
||||
}
|
||||
} else {
|
||||
getLogger().info("Attribute API not available (requires 1.9+) - attribute sync will be disabled");
|
||||
}
|
||||
|
||||
if (!VersionCompatibility.isOffhandSupported()) {
|
||||
getLogger().info("ℹ️ Offhand sync disabled (requires 1.9+)");
|
||||
}
|
||||
if (!VersionCompatibility.isAdvancementsSupported()) {
|
||||
getLogger().info("ℹ️ Advancements sync disabled (requires 1.12+)");
|
||||
}
|
||||
|
||||
getLogger().info("✅ Running on Minecraft " + VersionCompatibility.getVersionString() +
|
||||
" - Full compatibility confirmed");
|
||||
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("Could not perform version compatibility check: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void configureEconomyIntegration() {
|
||||
if (!syncEconomy) {
|
||||
economyProvider = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (setupEconomyIntegration()) {
|
||||
getLogger().info("Vault integration enabled for economy sync.");
|
||||
} else {
|
||||
economyProvider = null;
|
||||
syncEconomy = false;
|
||||
getConfig().set("sync.economy", false);
|
||||
saveConfig();
|
||||
getLogger().warning("Economy sync has been disabled because Vault or an economy provider is unavailable.");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean setupEconomyIntegration() {
|
||||
economyProvider = null;
|
||||
|
||||
if (getServer().getPluginManager().getPlugin("Vault") == null) {
|
||||
getLogger().warning("Vault plugin not found! Economy sync requires Vault.");
|
||||
return false;
|
||||
}
|
||||
|
||||
RegisteredServiceProvider<Economy> registration =
|
||||
getServer().getServicesManager().getRegistration(Economy.class);
|
||||
if (registration == null) {
|
||||
getLogger().warning("No Vault economy provider registration found. Economy sync requires an economy plugin.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Economy provider = registration.getProvider();
|
||||
if (provider == null) {
|
||||
getLogger().warning("Vault returned a null economy provider. Economy sync cannot continue.");
|
||||
return false;
|
||||
}
|
||||
|
||||
economyProvider = provider;
|
||||
getLogger().info("Hooked into Vault economy provider: " + provider.getName());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Getter methods
|
||||
public boolean isSyncCoordinates() { return syncCoordinates; }
|
||||
public boolean isSyncXp() { return syncXp; }
|
||||
public boolean isSyncGamemode() { return syncGamemode; }
|
||||
public boolean isSyncEnderchest() { return syncEnderchest; }
|
||||
public boolean isSyncInventory() { return syncInventory; }
|
||||
public boolean isSyncHealth() { return syncHealth; }
|
||||
public boolean isSyncHunger() { return syncHunger; }
|
||||
public boolean isSyncPosition() { return syncPosition; }
|
||||
public boolean isSyncAchievements() { return syncAchievements; }
|
||||
public boolean isSyncArmor() { return syncArmor; }
|
||||
public boolean isSyncOffhand() { return syncOffhand; }
|
||||
public boolean isSyncEffects() { return syncEffects; }
|
||||
public boolean isSyncStatistics() { return syncStatistics; }
|
||||
public boolean isSyncAttributes() { return syncAttributes; }
|
||||
public boolean isSyncPermissions() { return syncPermissions; }
|
||||
public boolean isSyncEconomy() { return syncEconomy; }
|
||||
|
||||
// Setter methods
|
||||
public void setSyncCoordinates(boolean value) {
|
||||
this.syncCoordinates = value;
|
||||
getConfig().set("sync.coordinates", value);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void setSyncXp(boolean value) {
|
||||
this.syncXp = value;
|
||||
getConfig().set("sync.xp", value);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void setSyncGamemode(boolean value) {
|
||||
this.syncGamemode = value;
|
||||
getConfig().set("sync.gamemode", value);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void setSyncEnderchest(boolean value) {
|
||||
this.syncEnderchest = value;
|
||||
getConfig().set("sync.enderchest", value);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void setSyncInventory(boolean value) {
|
||||
this.syncInventory = value;
|
||||
getConfig().set("sync.inventory", value);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void setSyncHealth(boolean value) {
|
||||
this.syncHealth = value;
|
||||
getConfig().set("sync.health", value);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void setSyncHunger(boolean value) {
|
||||
this.syncHunger = value;
|
||||
getConfig().set("sync.hunger", value);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void setSyncPosition(boolean value) {
|
||||
this.syncPosition = value;
|
||||
getConfig().set("sync.position", value);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void setSyncAchievements(boolean value) {
|
||||
this.syncAchievements = value;
|
||||
getConfig().set("sync.achievements", value);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void setSyncArmor(boolean value) {
|
||||
this.syncArmor = value;
|
||||
getConfig().set("sync.armor", value);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void setSyncOffhand(boolean value) {
|
||||
this.syncOffhand = value;
|
||||
getConfig().set("sync.offhand", value);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void setSyncEffects(boolean value) {
|
||||
this.syncEffects = value;
|
||||
getConfig().set("sync.effects", value);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void setSyncStatistics(boolean value) {
|
||||
this.syncStatistics = value;
|
||||
getConfig().set("sync.statistics", value);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void setSyncAttributes(boolean value) {
|
||||
this.syncAttributes = value;
|
||||
getConfig().set("sync.attributes", value);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void setSyncPermissions(boolean value) {
|
||||
this.syncPermissions = value;
|
||||
getConfig().set("sync.permissions", value);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void setSyncEconomy(boolean value) {
|
||||
this.syncEconomy = value;
|
||||
getConfig().set("sync.economy", value);
|
||||
configureEconomyIntegration();
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void reloadPlugin() {
|
||||
reloadConfig();
|
||||
|
||||
if (configManager != null) {
|
||||
configManager.reloadConfig();
|
||||
tablePrefix = configManager.getTablePrefix();
|
||||
}
|
||||
|
||||
String lang = getConfig().getString("messages.language", "en");
|
||||
messageManager.load(lang);
|
||||
|
||||
if (getConfig().getBoolean("metrics", true)) {
|
||||
if (metrics == null) {
|
||||
metrics = new Metrics(this, 25037);
|
||||
}
|
||||
} else {
|
||||
metrics = null;
|
||||
}
|
||||
|
||||
boolean wasBungeeEnabled = bungeecordIntegrationEnabled;
|
||||
bungeecordIntegrationEnabled = getConfig().getBoolean("integrations.bungeecord", false);
|
||||
if (bungeecordIntegrationEnabled && !wasBungeeEnabled) {
|
||||
getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord");
|
||||
getLogger().info("BungeeCord integration enabled after reload.");
|
||||
} else if (!bungeecordIntegrationEnabled && wasBungeeEnabled) {
|
||||
getServer().getMessenger().unregisterOutgoingPluginChannel(this);
|
||||
getLogger().info("BungeeCord integration disabled after reload.");
|
||||
}
|
||||
|
||||
loadSyncSettings();
|
||||
|
||||
boolean invSeeIntegration = getConfig().getBoolean("integrations.invsee", true);
|
||||
boolean openInvIntegration = getConfig().getBoolean("integrations.openinv", true);
|
||||
if (inventoryViewerIntegrationManager != null) {
|
||||
if (!invSeeIntegration && !openInvIntegration) {
|
||||
inventoryViewerIntegrationManager.shutdown();
|
||||
inventoryViewerIntegrationManager = null;
|
||||
} else {
|
||||
inventoryViewerIntegrationManager.updateSettings(invSeeIntegration, openInvIntegration);
|
||||
}
|
||||
} else if (invSeeIntegration || openInvIntegration) {
|
||||
inventoryViewerIntegrationManager = new InventoryViewerIntegrationManager(this, databaseManager,
|
||||
invSeeIntegration, openInvIntegration);
|
||||
}
|
||||
|
||||
if (advancementSyncManager != null) {
|
||||
advancementSyncManager.reloadFromConfig();
|
||||
}
|
||||
|
||||
int newIntervalSeconds = getConfig().getInt("autosave.interval", 1);
|
||||
if (newIntervalSeconds != autosaveIntervalSeconds) {
|
||||
autosaveIntervalSeconds = newIntervalSeconds;
|
||||
if (autosaveTask != null) {
|
||||
autosaveTask.cancel();
|
||||
autosaveTask = null;
|
||||
}
|
||||
if (autosaveIntervalSeconds > 0) {
|
||||
long ticks = autosaveIntervalSeconds * 20L;
|
||||
autosaveTask = SchedulerUtils.runTaskTimerAsync(this, () -> {
|
||||
try {
|
||||
int savedCount = 0;
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
try {
|
||||
if (databaseManager.savePlayer(player)) {
|
||||
savedCount++;
|
||||
} else {
|
||||
getLogger().warning("Failed to autosave data for " + player.getName()
|
||||
+ ": See previous log entries for details.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("Failed to autosave data for " + player.getName() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
if (savedCount > 0 && isPerformanceLoggingEnabled()) {
|
||||
getLogger().info("Autosaved data for " + savedCount + " players in " +
|
||||
(endTime - startTime) + "ms");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().severe("Error during autosave: " + e.getMessage());
|
||||
}
|
||||
}, ticks, ticks);
|
||||
getLogger().info("Autosave task restarted with interval: " + autosaveIntervalSeconds + " seconds");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void triggerEconomySync(Player player) {
|
||||
if (!syncEconomy) {
|
||||
logDebug("Economy sync disabled, skipping manual trigger for " + player.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
logDebug("Manual economy sync triggered for " + player.getName());
|
||||
|
||||
try {
|
||||
long startTime = System.currentTimeMillis();
|
||||
databaseManager.savePlayer(player);
|
||||
long endTime = System.currentTimeMillis();
|
||||
|
||||
logDebug("Manual economy sync completed for " + player.getName() +
|
||||
" in " + (endTime - startTime) + "ms");
|
||||
|
||||
} catch (Exception e) {
|
||||
getLogger().severe("Failed to manually sync economy for " + player.getName() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void syncPlayerEconomy(Player player) {
|
||||
triggerEconomySync(player);
|
||||
}
|
||||
|
||||
public void connectPlayerToServer(Player player, String targetServer) {
|
||||
if (!bungeecordIntegrationEnabled) {
|
||||
getLogger().warning("Attempted to send player " + player.getName()
|
||||
+ " to server '" + targetServer + "' while BungeeCord integration is disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (player == null || targetServer == null || targetServer.trim().isEmpty()) {
|
||||
getLogger().warning("Invalid target server specified for player transfer.");
|
||||
return;
|
||||
}
|
||||
|
||||
SchedulerUtils.runTask(this, player, () -> {
|
||||
if (!player.isOnline()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||
out.writeUTF("Connect");
|
||||
out.writeUTF(targetServer);
|
||||
player.sendPluginMessage(this, "BungeeCord", out.toByteArray());
|
||||
getLogger().info("Sent player " + player.getName() + " to server '" + targetServer + "'.");
|
||||
} catch (Exception e) {
|
||||
getLogger().severe("Failed to send player " + player.getName() + " to server '" + targetServer + "': " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ConfigManager getConfigManager() { return configManager; }
|
||||
public String getTablePrefix() { return tablePrefix != null ? tablePrefix : "player_data_premium"; }
|
||||
public DatabaseManager getDatabaseManager() { return databaseManager; }
|
||||
public AdvancementSyncManager getAdvancementSyncManager() { return advancementSyncManager; }
|
||||
public BackupManager getBackupManager() { return backupManager; }
|
||||
public ConnectionPool getConnectionPool() { return connectionPool; }
|
||||
public MessageManager getMessageManager() { return messageManager; }
|
||||
public Economy getEconomyProvider() { return economyProvider; }
|
||||
public boolean isBungeecordIntegrationEnabled() { return bungeecordIntegrationEnabled; }
|
||||
|
||||
// Premium getters
|
||||
public LicenseManager getLicenseManager() { return licenseManager; }
|
||||
public PremiumUpdateChecker getUpdateChecker() { return updateChecker; }
|
||||
|
||||
public void logDebug(String message) {
|
||||
if (configManager != null && configManager.isDebugMode()) {
|
||||
getLogger().log(Level.FINE, message);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDebugEnabled() {
|
||||
return configManager != null && configManager.isDebugMode();
|
||||
}
|
||||
|
||||
public boolean isPerformanceLoggingEnabled() {
|
||||
return configManager != null && configManager.isPerformanceLoggingEnabled();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.example.playerdatasync.premium.database;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import com.example.playerdatasync.premium.core.PlayerDataSyncPremium;
|
||||
|
||||
/**
|
||||
* Simple connection pool implementation for PlayerDataSync Premium
|
||||
*/
|
||||
public class ConnectionPool {
|
||||
private final PlayerDataSyncPremium plugin;
|
||||
private final ConcurrentLinkedQueue<Connection> availableConnections;
|
||||
private final AtomicInteger connectionCount;
|
||||
private final int maxConnections;
|
||||
private final String databaseUrl;
|
||||
private final String username;
|
||||
private final String password;
|
||||
private volatile boolean shutdown = false;
|
||||
|
||||
public ConnectionPool(PlayerDataSyncPremium plugin, String databaseUrl, String username, String password, int maxConnections) {
|
||||
this.plugin = plugin;
|
||||
this.databaseUrl = databaseUrl;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.maxConnections = maxConnections;
|
||||
this.availableConnections = new ConcurrentLinkedQueue<>();
|
||||
this.connectionCount = new AtomicInteger(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a connection from the pool with improved error handling
|
||||
*/
|
||||
public Connection getConnection() throws SQLException {
|
||||
if (shutdown) {
|
||||
throw new SQLException("Connection pool is shut down");
|
||||
}
|
||||
|
||||
Connection connection = availableConnections.poll();
|
||||
|
||||
if (connection != null && isConnectionValid(connection)) {
|
||||
return connection;
|
||||
}
|
||||
|
||||
// If no valid connection available, create a new one if under limit
|
||||
if (connectionCount.get() < maxConnections) {
|
||||
connection = createNewConnection();
|
||||
if (connection != null) {
|
||||
connectionCount.incrementAndGet();
|
||||
plugin.getLogger().fine("Created new database connection. Pool size: " + connectionCount.get());
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for a connection to become available with exponential backoff
|
||||
long startTime = System.currentTimeMillis();
|
||||
long waitTime = 10; // Start with 10ms
|
||||
final long maxWaitTime = 100; // Max 100ms between attempts
|
||||
final long totalTimeout = 10000; // 10 second total timeout
|
||||
|
||||
while (System.currentTimeMillis() - startTime < totalTimeout) {
|
||||
connection = availableConnections.poll();
|
||||
if (connection != null && isConnectionValid(connection)) {
|
||||
return connection;
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(waitTime);
|
||||
waitTime = Math.min(waitTime * 2, maxWaitTime); // Exponential backoff
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new SQLException("Interrupted while waiting for connection");
|
||||
}
|
||||
}
|
||||
|
||||
// Log pool statistics before throwing exception
|
||||
plugin.getLogger().severe("Connection pool exhausted. " + getStats());
|
||||
throw new SQLException("Unable to obtain database connection within timeout (" + totalTimeout + "ms)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a connection to the pool
|
||||
*/
|
||||
public void returnConnection(Connection connection) {
|
||||
if (connection == null || shutdown) {
|
||||
try {
|
||||
if (connection != null) {
|
||||
connection.close();
|
||||
connectionCount.decrementAndGet();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().warning("Error closing connection: " + e.getMessage());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isConnectionValid(connection)) {
|
||||
availableConnections.offer(connection);
|
||||
} else {
|
||||
try {
|
||||
connection.close();
|
||||
connectionCount.decrementAndGet();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().warning("Error closing invalid connection: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a connection is valid
|
||||
*/
|
||||
private boolean isConnectionValid(Connection connection) {
|
||||
try {
|
||||
return connection != null && !connection.isClosed() && connection.isValid(2);
|
||||
} catch (SQLException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new database connection
|
||||
*/
|
||||
private Connection createNewConnection() {
|
||||
try {
|
||||
if (username != null && password != null) {
|
||||
return DriverManager.getConnection(databaseUrl, username, password);
|
||||
} else {
|
||||
return DriverManager.getConnection(databaseUrl);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().severe("Failed to create new database connection: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the pool with initial connections
|
||||
*/
|
||||
public void initialize() {
|
||||
int initialConnections = Math.min(3, maxConnections);
|
||||
for (int i = 0; i < initialConnections; i++) {
|
||||
Connection connection = createNewConnection();
|
||||
if (connection != null) {
|
||||
availableConnections.offer(connection);
|
||||
connectionCount.incrementAndGet();
|
||||
}
|
||||
}
|
||||
plugin.getLogger().info("Connection pool initialized with " + availableConnections.size() + " connections");
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the connection pool
|
||||
*/
|
||||
public void shutdown() {
|
||||
shutdown = true;
|
||||
|
||||
Connection connection;
|
||||
while ((connection = availableConnections.poll()) != null) {
|
||||
try {
|
||||
connection.close();
|
||||
connectionCount.decrementAndGet();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().warning("Error closing connection during shutdown: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
plugin.getLogger().info("Connection pool shut down. Remaining connections: " + connectionCount.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pool statistics
|
||||
*/
|
||||
public String getStats() {
|
||||
return String.format("Pool stats: %d/%d connections, %d available",
|
||||
connectionCount.get(), maxConnections, availableConnections.size());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,405 @@
|
||||
package com.example.playerdatasync.premium.integration;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.example.playerdatasync.premium.core.PlayerDataSyncPremium;
|
||||
import com.example.playerdatasync.premium.database.DatabaseManager;
|
||||
import com.example.playerdatasync.premium.managers.MessageManager;
|
||||
import com.example.playerdatasync.premium.utils.OfflinePlayerData;
|
||||
import com.example.playerdatasync.premium.utils.SchedulerUtils;
|
||||
|
||||
/**
|
||||
* Provides compatibility with inventory viewing plugins such as InvSee++ and OpenInv.
|
||||
* The manager intercepts their commands and serves data directly from the
|
||||
* PlayerDataSync database so that editing inventories works across servers.
|
||||
*/
|
||||
public class InventoryViewerIntegrationManager implements Listener {
|
||||
private static final Set<String> INVSEE_COMMANDS = new HashSet<>(Arrays.asList("invsee", "isee"));
|
||||
private static final Set<String> INVSEE_ENDER_COMMANDS = new HashSet<>(Arrays.asList("endersee", "enderinv"));
|
||||
private static final Set<String> OPENINV_COMMANDS = new HashSet<>(Arrays.asList("openinv", "oi"));
|
||||
private static final Set<String> OPENINV_ENDER_COMMANDS = new HashSet<>(Arrays.asList("openender", "enderchest", "openec", "ec"));
|
||||
|
||||
private final PlayerDataSyncPremium plugin;
|
||||
private final DatabaseManager databaseManager;
|
||||
private final MessageManager messageManager;
|
||||
private boolean invSeeEnabled;
|
||||
private boolean openInvEnabled;
|
||||
private final ItemStack fillerItem;
|
||||
|
||||
public InventoryViewerIntegrationManager(PlayerDataSyncPremium plugin, DatabaseManager databaseManager,
|
||||
boolean invSeeEnabled, boolean openInvEnabled) {
|
||||
this.plugin = plugin;
|
||||
this.databaseManager = databaseManager;
|
||||
this.messageManager = plugin.getMessageManager();
|
||||
this.invSeeEnabled = invSeeEnabled;
|
||||
this.openInvEnabled = openInvEnabled;
|
||||
this.fillerItem = createFillerItem();
|
||||
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
detectExternalPlugins();
|
||||
}
|
||||
|
||||
private void detectExternalPlugins() {
|
||||
if (invSeeEnabled && plugin.getServer().getPluginManager().getPlugin("InvSee++") != null) {
|
||||
plugin.getLogger().info("InvSee++ detected. Routing inventory viewing through PlayerDataSync storage.");
|
||||
}
|
||||
if (openInvEnabled && plugin.getServer().getPluginManager().getPlugin("OpenInv") != null) {
|
||||
plugin.getLogger().info("OpenInv detected. Routing inventory viewing through PlayerDataSync storage.");
|
||||
}
|
||||
}
|
||||
|
||||
public void updateSettings(boolean invSeeEnabled, boolean openInvEnabled) {
|
||||
this.invSeeEnabled = invSeeEnabled;
|
||||
this.openInvEnabled = openInvEnabled;
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
HandlerList.unregisterAll(this);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryCommand(PlayerCommandPreprocessEvent event) {
|
||||
if (!invSeeEnabled && !openInvEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
String rawMessage = event.getMessage();
|
||||
if (rawMessage == null || rawMessage.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String trimmed = rawMessage.trim();
|
||||
if (!trimmed.startsWith("/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
String[] parts = trimmed.substring(1).split("\\s+");
|
||||
if (parts.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
String baseCommand = parts[0].toLowerCase(Locale.ROOT);
|
||||
boolean inventoryCommand = (invSeeEnabled && INVSEE_COMMANDS.contains(baseCommand))
|
||||
|| (openInvEnabled && OPENINV_COMMANDS.contains(baseCommand));
|
||||
boolean enderCommand = (invSeeEnabled && INVSEE_ENDER_COMMANDS.contains(baseCommand))
|
||||
|| (openInvEnabled && OPENINV_ENDER_COMMANDS.contains(baseCommand));
|
||||
|
||||
if (!inventoryCommand && !enderCommand) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = event.getPlayer();
|
||||
|
||||
if (inventoryCommand && !player.hasPermission("playerdatasync.integration.invsee")) {
|
||||
player.sendMessage(messageManager.get("prefix") + " " + messageManager.get("no_permission"));
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (enderCommand && !player.hasPermission("playerdatasync.integration.enderchest")) {
|
||||
player.sendMessage(messageManager.get("prefix") + " " + messageManager.get("no_permission"));
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parts.length < 2) {
|
||||
String usageKey = enderCommand ? "inventory_view_usage_ender" : "inventory_view_usage_inventory";
|
||||
player.sendMessage(messageManager.get("prefix") + " " + messageManager.get(usageKey));
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
String targetName = parts[1];
|
||||
event.setCancelled(true);
|
||||
handleInventoryRequest(player, targetName, enderCommand);
|
||||
}
|
||||
|
||||
private void handleInventoryRequest(Player viewer, String targetName, boolean enderChest) {
|
||||
// Use UUID-based lookup instead of deprecated getOfflinePlayer(String)
|
||||
OfflinePlayer offline = null;
|
||||
try {
|
||||
// Try to find online player first
|
||||
Player onlinePlayer = Bukkit.getPlayer(targetName);
|
||||
if (onlinePlayer != null) {
|
||||
offline = onlinePlayer;
|
||||
} else {
|
||||
// For offline players, we still need to use deprecated method for compatibility
|
||||
// This is necessary for 1.8-1.16 compatibility
|
||||
@SuppressWarnings("deprecation")
|
||||
OfflinePlayer tempPlayer = Bukkit.getOfflinePlayer(targetName);
|
||||
offline = tempPlayer;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("Could not get offline player for " + targetName + ": " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (offline == null) {
|
||||
return;
|
||||
}
|
||||
UUID targetUuid = offline != null ? offline.getUniqueId() : null;
|
||||
String displayName = offline != null && offline.getName() != null ? offline.getName() : targetName;
|
||||
|
||||
viewer.sendMessage(messageManager.get("prefix") + " "
|
||||
+ messageManager.get("inventory_view_loading").replace("{player}", displayName));
|
||||
|
||||
SchedulerUtils.runTaskAsync(plugin, () -> {
|
||||
OfflinePlayerData data;
|
||||
try {
|
||||
data = databaseManager.loadOfflinePlayerData(targetUuid, displayName);
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().severe("Failed to load offline data for " + displayName + ": " + ex.getMessage());
|
||||
data = null;
|
||||
}
|
||||
|
||||
if (data == null) {
|
||||
String message = messageManager.get("prefix") + " "
|
||||
+ messageManager.get("inventory_view_open_failed").replace("{player}", displayName);
|
||||
SchedulerUtils.runTask(plugin, player, () -> {
|
||||
if (viewer.isOnline()) {
|
||||
viewer.sendMessage(message);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
OfflinePlayerData finalData = data;
|
||||
SchedulerUtils.runTask(plugin, player, () -> {
|
||||
if (!viewer.isOnline()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!finalData.existsInDatabase()) {
|
||||
viewer.sendMessage(messageManager.get("prefix") + " "
|
||||
+ messageManager.get("inventory_view_no_data").replace("{player}", finalData.getDisplayName()));
|
||||
}
|
||||
|
||||
if (enderChest) {
|
||||
openEnderChestInventory(viewer, finalData);
|
||||
} else {
|
||||
openMainInventory(viewer, finalData);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void openMainInventory(Player viewer, OfflinePlayerData data) {
|
||||
OfflineInventoryHolder holder = new OfflineInventoryHolder(data, false);
|
||||
Inventory inventory = Bukkit.createInventory(holder, 45,
|
||||
messageManager.get("inventory_view_title_inventory").replace("{player}", data.getDisplayName()));
|
||||
holder.setInventory(inventory);
|
||||
|
||||
ItemStack[] main = data.getInventoryContents();
|
||||
for (int slot = 0; slot < 36; slot++) {
|
||||
inventory.setItem(slot, slot < main.length ? main[slot] : null);
|
||||
}
|
||||
|
||||
ItemStack[] armor = data.getArmorContents();
|
||||
inventory.setItem(36, armor.length > 3 ? armor[3] : null); // Helmet position
|
||||
inventory.setItem(37, armor.length > 2 ? armor[2] : null); // Chestplate
|
||||
inventory.setItem(38, armor.length > 1 ? armor[1] : null); // Leggings
|
||||
inventory.setItem(39, armor.length > 0 ? armor[0] : null); // Boots
|
||||
|
||||
inventory.setItem(40, data.getOffhandItem());
|
||||
|
||||
for (int slot = 41; slot < 45; slot++) {
|
||||
inventory.setItem(slot, fillerItem.clone());
|
||||
}
|
||||
|
||||
viewer.openInventory(inventory);
|
||||
}
|
||||
|
||||
private void openEnderChestInventory(Player viewer, OfflinePlayerData data) {
|
||||
OfflineInventoryHolder holder = new OfflineInventoryHolder(data, true);
|
||||
Inventory inventory = Bukkit.createInventory(holder, 27,
|
||||
messageManager.get("inventory_view_title_ender").replace("{player}", data.getDisplayName()));
|
||||
holder.setInventory(inventory);
|
||||
|
||||
ItemStack[] contents = data.getEnderChestContents();
|
||||
for (int i = 0; i < inventory.getSize(); i++) {
|
||||
inventory.setItem(i, i < contents.length ? contents[i] : null);
|
||||
}
|
||||
|
||||
viewer.openInventory(inventory);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
if (!(event.getInventory().getHolder() instanceof OfflineInventoryHolder holder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!holder.isEnderChest()) {
|
||||
int rawSlot = event.getRawSlot();
|
||||
int topSize = event.getView().getTopInventory().getSize();
|
||||
if (rawSlot < topSize && rawSlot >= 41) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onInventoryDrag(InventoryDragEvent event) {
|
||||
if (!(event.getInventory().getHolder() instanceof OfflineInventoryHolder holder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (holder.isEnderChest()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int topSize = event.getView().getTopInventory().getSize();
|
||||
for (int rawSlot : event.getRawSlots()) {
|
||||
if (rawSlot < topSize && rawSlot >= 41) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryClose(InventoryCloseEvent event) {
|
||||
if (!(event.getInventory().getHolder() instanceof OfflineInventoryHolder holder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Inventory inventory = event.getInventory();
|
||||
OfflinePlayerData data = holder.getData();
|
||||
UUID viewerId = ((Player) event.getPlayer()).getUniqueId();
|
||||
|
||||
if (holder.isEnderChest()) {
|
||||
ItemStack[] contents = Arrays.copyOf(inventory.getContents(), inventory.getSize());
|
||||
data.setEnderChestContents(contents);
|
||||
SchedulerUtils.runTaskAsync(plugin, () -> {
|
||||
boolean success = databaseManager.saveOfflineEnderChestData(data);
|
||||
if (!success) {
|
||||
notifySaveFailure(viewerId, data.getDisplayName());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ItemStack[] main = new ItemStack[36];
|
||||
for (int i = 0; i < 36; i++) {
|
||||
main[i] = inventory.getItem(i);
|
||||
}
|
||||
|
||||
ItemStack[] armor = new ItemStack[4];
|
||||
armor[3] = inventory.getItem(36); // helmet
|
||||
armor[2] = inventory.getItem(37); // chestplate
|
||||
armor[1] = inventory.getItem(38); // leggings
|
||||
armor[0] = inventory.getItem(39); // boots
|
||||
ItemStack offhand = inventory.getItem(40);
|
||||
|
||||
data.setInventoryContents(main);
|
||||
data.setArmorContents(armor);
|
||||
data.setOffhandItem(offhand);
|
||||
|
||||
SchedulerUtils.runTaskAsync(plugin, () -> {
|
||||
boolean success = databaseManager.saveOfflineInventoryData(data);
|
||||
if (!success) {
|
||||
notifySaveFailure(viewerId, data.getDisplayName());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void notifySaveFailure(UUID viewerId, String playerName) {
|
||||
SchedulerUtils.runTask(plugin, player, () -> {
|
||||
Player viewer = Bukkit.getPlayer(viewerId);
|
||||
if (viewer != null && viewer.isOnline()) {
|
||||
viewer.sendMessage(messageManager.get("prefix") + " "
|
||||
+ messageManager.get("inventory_view_save_failed").replace("{player}", playerName));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ItemStack createFillerItem() {
|
||||
Material paneMaterial;
|
||||
|
||||
// GRAY_STAINED_GLASS_PANE only exists in 1.13+
|
||||
// For 1.8-1.12, use STAINED_GLASS_PANE with durability/data value 7 (gray)
|
||||
try {
|
||||
if (com.example.playerdatasync.premium.utils.VersionCompatibility.isAtLeast(1, 13, 0)) {
|
||||
paneMaterial = Material.GRAY_STAINED_GLASS_PANE;
|
||||
} else {
|
||||
// For 1.8-1.12, use STAINED_GLASS_PANE
|
||||
paneMaterial = Material.valueOf("STAINED_GLASS_PANE");
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Fallback if STAINED_GLASS_PANE doesn't exist (shouldn't happen, but be safe)
|
||||
paneMaterial = Material.GLASS_PANE;
|
||||
plugin.getLogger().warning("Could not find STAINED_GLASS_PANE, using GLASS_PANE as fallback");
|
||||
}
|
||||
|
||||
ItemStack pane = new ItemStack(paneMaterial);
|
||||
|
||||
// Set durability/data value for 1.8-1.12 (7 = gray color)
|
||||
if (!com.example.playerdatasync.premium.utils.VersionCompatibility.isAtLeast(1, 13, 0)) {
|
||||
try {
|
||||
// setDurability is deprecated but necessary for 1.8-1.12 compatibility
|
||||
pane.setDurability((short) 7); // Gray color
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("Could not set glass pane color for filler item: " + e.getMessage());
|
||||
// Continue with default material
|
||||
}
|
||||
}
|
||||
|
||||
ItemMeta meta = pane.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(" ");
|
||||
meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES);
|
||||
pane.setItemMeta(meta);
|
||||
}
|
||||
return pane;
|
||||
}
|
||||
|
||||
private static final class OfflineInventoryHolder implements InventoryHolder {
|
||||
private final OfflinePlayerData data;
|
||||
private final boolean enderChest;
|
||||
private Inventory inventory;
|
||||
|
||||
private OfflineInventoryHolder(OfflinePlayerData data, boolean enderChest) {
|
||||
this.data = data;
|
||||
this.enderChest = enderChest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
|
||||
private void setInventory(Inventory inventory) {
|
||||
this.inventory = inventory;
|
||||
}
|
||||
|
||||
private OfflinePlayerData getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
private boolean isEnderChest() {
|
||||
return enderChest;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.example.playerdatasync.premium.listeners;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.player.PlayerKickEvent;
|
||||
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
|
||||
import com.example.playerdatasync.premium.core.PlayerDataSyncPremium;
|
||||
import com.example.playerdatasync.premium.database.DatabaseManager;
|
||||
import com.example.playerdatasync.premium.managers.AdvancementSyncManager;
|
||||
import com.example.playerdatasync.premium.managers.MessageManager;
|
||||
|
||||
public class PlayerDataListener implements Listener {
|
||||
private final PlayerDataSyncPremium plugin;
|
||||
private final DatabaseManager dbManager;
|
||||
private final MessageManager messageManager;
|
||||
|
||||
public PlayerDataListener(PlayerDataSyncPremium plugin, DatabaseManager dbManager) {
|
||||
this.plugin = plugin;
|
||||
this.dbManager = dbManager;
|
||||
this.messageManager = plugin.getMessageManager();
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
if (plugin.getConfigManager() != null && plugin.getConfigManager().shouldShowSyncMessages()
|
||||
&& player.hasPermission("playerdatasync.message.show.loading")) {
|
||||
player.sendMessage(messageManager.get("prefix") + " " + messageManager.get("loading"));
|
||||
}
|
||||
|
||||
// Load data almost immediately after join to minimize empty inventories during server switches
|
||||
SchedulerUtils.runTaskLaterAsync(plugin, () -> {
|
||||
try {
|
||||
dbManager.loadPlayer(player);
|
||||
if (player.isOnline() && plugin.getConfigManager() != null
|
||||
&& plugin.getConfigManager().shouldShowSyncMessages()
|
||||
&& player.hasPermission("playerdatasync.message.show.loaded")) {
|
||||
SchedulerUtils.runTask(plugin, player, () ->
|
||||
player.sendMessage(messageManager.get("prefix") + " " + messageManager.get("loaded")));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe("Error loading data for " + player.getName() + ": " + e.getMessage());
|
||||
if (player.isOnline() && plugin.getConfigManager() != null
|
||||
&& plugin.getConfigManager().shouldShowSyncMessages()) {
|
||||
SchedulerUtils.runTask(plugin, player, () ->
|
||||
player.sendMessage(messageManager.get("prefix") + " " + messageManager.get("load_failed")));
|
||||
}
|
||||
}
|
||||
}, 1L);
|
||||
|
||||
AdvancementSyncManager advancementSyncManager = plugin.getAdvancementSyncManager();
|
||||
if (advancementSyncManager != null) {
|
||||
SchedulerUtils.runTaskLater(plugin, player, () -> advancementSyncManager.handlePlayerJoin(player), 2L);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
|
||||
// Save data synchronously so the database is updated before the player
|
||||
// joins another server. Using an async task here can lead to race
|
||||
// conditions when switching servers quickly via BungeeCord or similar
|
||||
// proxies, causing recent changes not to be stored in time.
|
||||
try {
|
||||
long startTime = System.currentTimeMillis();
|
||||
boolean saved = dbManager.savePlayer(player);
|
||||
long endTime = System.currentTimeMillis();
|
||||
|
||||
// Log slow saves for performance monitoring
|
||||
if (saved && endTime - startTime > 1000) { // More than 1 second
|
||||
plugin.getLogger().warning("Slow save detected for " + player.getName() +
|
||||
": " + (endTime - startTime) + "ms");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe("Failed to save data for " + player.getName() + ": " + e.getMessage());
|
||||
plugin.getLogger().log(java.util.logging.Level.SEVERE, "Stack trace:", e);
|
||||
}
|
||||
|
||||
AdvancementSyncManager advancementSyncManager = plugin.getAdvancementSyncManager();
|
||||
if (advancementSyncManager != null) {
|
||||
advancementSyncManager.handlePlayerQuit(player);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
|
||||
if (!plugin.getConfig().getBoolean("autosave.on_world_change", true)) return;
|
||||
|
||||
Player player = event.getPlayer();
|
||||
|
||||
// Save player data asynchronously when changing worlds
|
||||
SchedulerUtils.runTaskAsync(plugin, () -> {
|
||||
try {
|
||||
dbManager.savePlayer(player);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("Failed to save data for " + player.getName() +
|
||||
" on world change: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerDeath(PlayerDeathEvent event) {
|
||||
if (!plugin.getConfig().getBoolean("autosave.on_death", true)) return;
|
||||
|
||||
Player player = event.getEntity();
|
||||
|
||||
// Fix for Issue #41: Potion Effect on Death
|
||||
// Save player data BEFORE death effects are cleared
|
||||
// This ensures potion effects are saved, but they won't be restored on respawn
|
||||
// because Minecraft clears them on death
|
||||
SchedulerUtils.runTaskAsync(plugin, () -> {
|
||||
try {
|
||||
// Capture data before death clears effects
|
||||
dbManager.savePlayer(player);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("Failed to save data for " + player.getName() +
|
||||
" on death: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
// Schedule a delayed save after respawn to ensure death state is saved
|
||||
// This prevents potion effects from being restored after death
|
||||
SchedulerUtils.runTaskLater(plugin, player, () -> {
|
||||
if (player.isOnline()) {
|
||||
// Clear any potion effects that might have been restored
|
||||
// This ensures death clears effects as expected
|
||||
player.getActivePotionEffects().clear();
|
||||
}
|
||||
}, 1L);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onPlayerKick(PlayerKickEvent event) {
|
||||
// Save data when player is kicked (might be server switch)
|
||||
if (!plugin.getConfig().getBoolean("autosave.on_kick", true)) return;
|
||||
|
||||
Player player = event.getPlayer();
|
||||
|
||||
plugin.logDebug("Player " + player.getName() + " was kicked, saving data");
|
||||
|
||||
try {
|
||||
long startTime = System.currentTimeMillis();
|
||||
dbManager.savePlayer(player);
|
||||
long endTime = System.currentTimeMillis();
|
||||
|
||||
plugin.logDebug("Saved data for kicked player " + player.getName() +
|
||||
" in " + (endTime - startTime) + "ms");
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe("Failed to save data for kicked player " + player.getName() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onPlayerTeleport(PlayerTeleportEvent event) {
|
||||
// Check if this is a server-to-server teleport (BungeeCord)
|
||||
if (!plugin.getConfig().getBoolean("autosave.on_server_switch", true)) return;
|
||||
|
||||
if (event.getCause() == PlayerTeleportEvent.TeleportCause.PLUGIN) {
|
||||
Player player = event.getPlayer();
|
||||
|
||||
// Check if the teleport is to a different server (BungeeCord behavior)
|
||||
if (event.getTo() != null && event.getTo().getWorld() != null) {
|
||||
plugin.logDebug("Player " + player.getName() + " teleported via plugin, saving data");
|
||||
|
||||
// Save data before teleport
|
||||
try {
|
||||
long startTime = System.currentTimeMillis();
|
||||
dbManager.savePlayer(player);
|
||||
long endTime = System.currentTimeMillis();
|
||||
|
||||
plugin.logDebug("Saved data for teleporting player " + player.getName() +
|
||||
" in " + (endTime - startTime) + "ms");
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe("Failed to save data for teleporting player " + player.getName() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle player respawn - Respawn to Lobby feature
|
||||
* Sends player to lobby server after death if enabled
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.NORMAL)
|
||||
public void onPlayerRespawn(PlayerRespawnEvent event) {
|
||||
// Check if respawn to lobby is enabled
|
||||
if (!plugin.getConfig().getBoolean("respawn_to_lobby.enabled", false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if BungeeCord integration is enabled (required for server switching)
|
||||
if (!plugin.isBungeecordIntegrationEnabled()) {
|
||||
plugin.getLogger().warning("Respawn to lobby is enabled but BungeeCord integration is disabled. " +
|
||||
"Please enable BungeeCord integration in config.yml");
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = event.getPlayer();
|
||||
String lobbyServer = plugin.getConfig().getString("respawn_to_lobby.server", "lobby");
|
||||
|
||||
// Check if current server is already the lobby server
|
||||
String currentServerId = plugin.getConfig().getString("server.id", "default");
|
||||
if (currentServerId.equalsIgnoreCase(lobbyServer)) {
|
||||
plugin.logDebug("Player " + player.getName() + " is already on lobby server, skipping respawn transfer");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save player data before transferring
|
||||
plugin.logDebug("Saving data for " + player.getName() + " before respawn to lobby");
|
||||
SchedulerUtils.runTaskAsync(plugin, () -> {
|
||||
try {
|
||||
boolean saved = dbManager.savePlayer(player);
|
||||
if (saved) {
|
||||
plugin.logDebug("Data saved for " + player.getName() + " before respawn to lobby");
|
||||
} else {
|
||||
plugin.getLogger().warning("Failed to save data for " + player.getName() + " before respawn to lobby");
|
||||
}
|
||||
|
||||
// Transfer player to lobby server after save completes
|
||||
SchedulerUtils.runTask(plugin, player, () -> {
|
||||
if (player.isOnline()) {
|
||||
plugin.getLogger().info("Transferring " + player.getName() + " to lobby server '" + lobbyServer + "' after respawn");
|
||||
plugin.connectPlayerToServer(player, lobbyServer);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe("Error saving data for " + player.getName() + " before respawn to lobby: " + e.getMessage());
|
||||
plugin.getLogger().log(java.util.logging.Level.SEVERE, "Stack trace:", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.example.playerdatasync.premium.listeners;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import com.example.playerdatasync.premium.core.PlayerDataSyncPremium;
|
||||
import com.example.playerdatasync.premium.database.DatabaseManager;
|
||||
import com.example.playerdatasync.premium.managers.MessageManager;
|
||||
|
||||
/**
|
||||
* Handles server switch requests that originate from in-game commands.
|
||||
* This ensures player data is safely stored before a BungeeCord transfer
|
||||
* and prevents duplication by clearing the inventory only after a successful save.
|
||||
*/
|
||||
public class ServerSwitchListener implements Listener {
|
||||
private final PlayerDataSyncPremium plugin;
|
||||
private final DatabaseManager databaseManager;
|
||||
private final MessageManager messageManager;
|
||||
|
||||
public ServerSwitchListener(PlayerDataSyncPremium plugin, DatabaseManager databaseManager) {
|
||||
this.plugin = plugin;
|
||||
this.databaseManager = databaseManager;
|
||||
this.messageManager = plugin.getMessageManager();
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onServerSwitchCommand(PlayerCommandPreprocessEvent event) {
|
||||
if (!plugin.isBungeecordIntegrationEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String rawMessage = event.getMessage();
|
||||
if (rawMessage == null || rawMessage.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String trimmed = rawMessage.trim();
|
||||
if (!trimmed.startsWith("/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
String[] parts = trimmed.split("\\s+");
|
||||
if (parts.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
String baseCommand = parts[0].startsWith("/") ? parts[0].substring(1) : parts[0];
|
||||
if (!baseCommand.equalsIgnoreCase("server")) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = event.getPlayer();
|
||||
if (parts.length < 2) {
|
||||
player.sendMessage(messageManager.get("prefix") + " "
|
||||
+ messageManager.get("invalid_syntax").replace("{usage}", "/server <server>"));
|
||||
return;
|
||||
}
|
||||
|
||||
String targetServer = parts[1];
|
||||
event.setCancelled(true);
|
||||
|
||||
if (plugin.getConfigManager() != null && plugin.getConfigManager().shouldShowSyncMessages()
|
||||
&& player.hasPermission("playerdatasync.message.show.saving")) {
|
||||
player.sendMessage(messageManager.get("prefix") + " " + messageManager.get("server_switch_save"));
|
||||
}
|
||||
|
||||
SchedulerUtils.runTaskAsync(plugin, () -> {
|
||||
boolean saveSuccessful = databaseManager.savePlayer(player);
|
||||
|
||||
SchedulerUtils.runTask(plugin, player, () -> {
|
||||
if (!player.isOnline()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (saveSuccessful) {
|
||||
if (plugin.getConfigManager() != null && plugin.getConfigManager().shouldShowSyncMessages()
|
||||
&& player.hasPermission("playerdatasync.message.show.saving")) {
|
||||
player.sendMessage(messageManager.get("prefix") + " " + messageManager.get("server_switch_saved"));
|
||||
}
|
||||
|
||||
player.getInventory().clear();
|
||||
player.getInventory().setArmorContents(new ItemStack[player.getInventory().getArmorContents().length]);
|
||||
player.getInventory().setItemInOffHand(null);
|
||||
player.updateInventory();
|
||||
} else if (plugin.getConfigManager() != null && plugin.getConfigManager().shouldShowSyncMessages()
|
||||
&& player.hasPermission("playerdatasync.message.show.errors")) {
|
||||
player.sendMessage(messageManager.get("prefix") + " "
|
||||
+ messageManager.get("sync_failed").replace("{error}", "Unable to save data before server switch."));
|
||||
}
|
||||
|
||||
plugin.connectPlayerToServer(player, targetServer);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
package com.example.playerdatasync.premium.managers;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.advancement.Advancement;
|
||||
import org.bukkit.advancement.AdvancementProgress;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerAdvancementDoneEvent;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.example.playerdatasync.premium.core.PlayerDataSyncPremium;
|
||||
|
||||
/**
|
||||
* Handles advancement synchronization in a staged manner so we can
|
||||
* import large advancement sets without blocking the main server thread.
|
||||
*/
|
||||
public class AdvancementSyncManager implements Listener {
|
||||
private final PlayerDataSyncPremium plugin;
|
||||
private final Map<UUID, PlayerAdvancementState> states = new ConcurrentHashMap<>();
|
||||
private final CopyOnWriteArrayList<NamespacedKey> cachedAdvancements = new CopyOnWriteArrayList<>();
|
||||
|
||||
private volatile boolean globalImportRunning = false;
|
||||
private volatile boolean globalImportCompleted = false;
|
||||
private BukkitTask globalImportTask;
|
||||
|
||||
public AdvancementSyncManager(PlayerDataSyncPremium plugin) {
|
||||
this.plugin = plugin;
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
|
||||
if (plugin.getConfig().getBoolean("performance.preload_advancements_on_startup", true)) {
|
||||
// Delay by one tick so Bukkit finished loading advancements.
|
||||
SchedulerUtils.runTask(plugin, player, () -> startGlobalImport(false));
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
if (globalImportTask != null) {
|
||||
globalImportTask.cancel();
|
||||
globalImportTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void reloadFromConfig() {
|
||||
if (plugin.getConfig().getBoolean("performance.preload_advancements_on_startup", true)) {
|
||||
if (!globalImportCompleted && !globalImportRunning) {
|
||||
startGlobalImport(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onAdvancementCompleted(PlayerAdvancementDoneEvent event) {
|
||||
Advancement advancement = event.getAdvancement();
|
||||
if (advancement == null) {
|
||||
return;
|
||||
}
|
||||
recordAdvancement(event.getPlayer().getUniqueId(), advancement.getKey().toString());
|
||||
}
|
||||
|
||||
public void recordAdvancement(UUID uuid, String key) {
|
||||
PlayerAdvancementState state = states.computeIfAbsent(uuid, id -> new PlayerAdvancementState());
|
||||
state.completedAdvancements.add(key);
|
||||
if (state.importInProgress) {
|
||||
state.pendingDuringImport.add(key);
|
||||
}
|
||||
state.lastUpdated = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public void handlePlayerJoin(Player player) {
|
||||
if (!plugin.getConfig().getBoolean("performance.automatic_player_advancement_import", true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerAdvancementState state = states.computeIfAbsent(player.getUniqueId(), key -> new PlayerAdvancementState());
|
||||
if (state.importFinished || state.importInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
queuePlayerImport(player, false);
|
||||
}
|
||||
|
||||
public void handlePlayerQuit(Player player) {
|
||||
PlayerAdvancementState state = states.get(player.getUniqueId());
|
||||
if (state != null) {
|
||||
state.importInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void seedFromDatabase(UUID uuid, String csv) {
|
||||
PlayerAdvancementState state = states.computeIfAbsent(uuid, key -> new PlayerAdvancementState());
|
||||
state.completedAdvancements.clear();
|
||||
state.pendingDuringImport.clear();
|
||||
|
||||
if (csv == null) {
|
||||
state.importFinished = false;
|
||||
state.lastUpdated = System.currentTimeMillis();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!csv.isEmpty()) {
|
||||
String[] parts = csv.split(",");
|
||||
for (String part : parts) {
|
||||
String trimmed = part.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
state.completedAdvancements.add(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.importFinished = true;
|
||||
state.lastUpdated = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public void forgetPlayer(UUID uuid) {
|
||||
states.remove(uuid);
|
||||
}
|
||||
|
||||
public String serializeForSave(Player player) {
|
||||
PlayerAdvancementState state = states.computeIfAbsent(player.getUniqueId(), key -> new PlayerAdvancementState());
|
||||
|
||||
if (!state.importFinished && !state.importInProgress) {
|
||||
if (plugin.getConfig().getBoolean("performance.automatic_player_advancement_import", true)) {
|
||||
queuePlayerImport(player, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.completedAdvancements.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return state.completedAdvancements.stream()
|
||||
.sorted()
|
||||
.collect(Collectors.joining(","));
|
||||
}
|
||||
|
||||
public void queuePlayerImport(Player player, boolean force) {
|
||||
PlayerAdvancementState state = states.computeIfAbsent(player.getUniqueId(), key -> new PlayerAdvancementState());
|
||||
if (!force) {
|
||||
if (state.importInProgress || state.importFinished) {
|
||||
return;
|
||||
}
|
||||
} else if (state.importInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<NamespacedKey> keys = getCachedAdvancementKeys();
|
||||
if (keys.isEmpty()) {
|
||||
if (globalImportRunning || !globalImportCompleted) {
|
||||
if (!state.awaitingGlobalCache) {
|
||||
state.importInProgress = true;
|
||||
state.awaitingGlobalCache = true;
|
||||
SchedulerUtils.runTaskLater(plugin, player, () -> {
|
||||
state.importInProgress = false;
|
||||
state.awaitingGlobalCache = false;
|
||||
if (player.isOnline()) {
|
||||
queuePlayerImport(player, force);
|
||||
}
|
||||
}, 20L);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// No advancements available at all (unlikely but possible if server has none)
|
||||
state.importFinished = true;
|
||||
state.importInProgress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
final int batchSize = Math.max(1,
|
||||
plugin.getConfig().getInt("performance.player_advancement_import_batch_size", 150));
|
||||
|
||||
state.importInProgress = true;
|
||||
state.pendingDuringImport.clear();
|
||||
state.lastImportStart = System.currentTimeMillis();
|
||||
|
||||
Iterator<NamespacedKey> iterator = keys.iterator();
|
||||
Set<String> imported = new HashSet<>();
|
||||
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!player.isOnline()) {
|
||||
state.importInProgress = false;
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
int processed = 0;
|
||||
while (iterator.hasNext() && processed < batchSize) {
|
||||
processed++;
|
||||
NamespacedKey key = iterator.next();
|
||||
Advancement advancement = Bukkit.getAdvancement(key);
|
||||
if (advancement == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AdvancementProgress progress = player.getAdvancementProgress(advancement);
|
||||
if (progress != null && progress.isDone()) {
|
||||
imported.add(key.toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (!iterator.hasNext()) {
|
||||
state.completedAdvancements.clear();
|
||||
state.completedAdvancements.addAll(imported);
|
||||
if (!state.pendingDuringImport.isEmpty()) {
|
||||
state.completedAdvancements.addAll(state.pendingDuringImport);
|
||||
state.pendingDuringImport.clear();
|
||||
}
|
||||
|
||||
state.importFinished = true;
|
||||
state.importInProgress = false;
|
||||
state.lastImportDuration = System.currentTimeMillis() - state.lastImportStart;
|
||||
state.lastUpdated = System.currentTimeMillis();
|
||||
|
||||
if (plugin.isPerformanceLoggingEnabled()) {
|
||||
plugin.getLogger().info("Imported " + state.completedAdvancements.size() +
|
||||
" achievements for " + player.getName() + " in " + state.lastImportDuration + "ms");
|
||||
}
|
||||
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(plugin, 1L, 1L);
|
||||
}
|
||||
|
||||
public boolean startGlobalImport(boolean force) {
|
||||
if (globalImportRunning) {
|
||||
return false;
|
||||
}
|
||||
if (globalImportCompleted && !force) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Iterator<Advancement> iterator = Bukkit.getServer().advancementIterator();
|
||||
cachedAdvancements.clear();
|
||||
globalImportRunning = true;
|
||||
globalImportCompleted = false;
|
||||
|
||||
final int batchSize = Math.max(1,
|
||||
plugin.getConfig().getInt("performance.advancement_import_batch_size", 250));
|
||||
|
||||
globalImportTask = new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int processed = 0;
|
||||
while (iterator.hasNext() && processed < batchSize) {
|
||||
processed++;
|
||||
Advancement advancement = iterator.next();
|
||||
if (advancement != null) {
|
||||
cachedAdvancements.add(advancement.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
if (!iterator.hasNext()) {
|
||||
globalImportRunning = false;
|
||||
globalImportCompleted = true;
|
||||
if (plugin.isPerformanceLoggingEnabled()) {
|
||||
plugin.getLogger().info("Cached " + cachedAdvancements.size() +
|
||||
" advancement definitions for staged synchronization");
|
||||
}
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(plugin, 1L, 1L);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<NamespacedKey> getCachedAdvancementKeys() {
|
||||
if (cachedAdvancements.isEmpty() && !globalImportRunning) {
|
||||
startGlobalImport(false);
|
||||
}
|
||||
return new ArrayList<>(cachedAdvancements);
|
||||
}
|
||||
|
||||
public String getGlobalImportStatus() {
|
||||
if (globalImportRunning) {
|
||||
return "running (" + cachedAdvancements.size() + " cached so far)";
|
||||
}
|
||||
if (globalImportCompleted) {
|
||||
return "ready (" + cachedAdvancements.size() + " cached)";
|
||||
}
|
||||
return "not started";
|
||||
}
|
||||
|
||||
public String getPlayerStatus(UUID uuid) {
|
||||
PlayerAdvancementState state = states.get(uuid);
|
||||
if (state == null) {
|
||||
return "no data";
|
||||
}
|
||||
if (state.importInProgress) {
|
||||
return "importing (" + state.completedAdvancements.size() + " known so far)";
|
||||
}
|
||||
if (state.importFinished) {
|
||||
return "ready (" + state.completedAdvancements.size() + " cached)";
|
||||
}
|
||||
return "pending import";
|
||||
}
|
||||
|
||||
public void forceRescan(Player player) {
|
||||
queuePlayerImport(player, true);
|
||||
}
|
||||
|
||||
private static class PlayerAdvancementState {
|
||||
private final Set<String> completedAdvancements = ConcurrentHashMap.newKeySet();
|
||||
private final Set<String> pendingDuringImport = ConcurrentHashMap.newKeySet();
|
||||
private volatile boolean importFinished = false;
|
||||
private volatile boolean importInProgress = false;
|
||||
private volatile boolean awaitingGlobalCache = false;
|
||||
private volatile long lastImportStart = 0;
|
||||
private volatile long lastImportDuration = 0;
|
||||
// Field reserved for future use (tracking last update time)
|
||||
@SuppressWarnings("unused")
|
||||
private volatile long lastUpdated = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package com.example.playerdatasync.premium.managers;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.sql.*;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import com.example.playerdatasync.premium.core.PlayerDataSyncPremium;
|
||||
|
||||
/**
|
||||
* Advanced backup and restore system for PlayerDataSync
|
||||
* Supports automatic backups, compression, and data integrity verification
|
||||
*/
|
||||
public class BackupManager {
|
||||
private final PlayerDataSyncPremium plugin;
|
||||
private BukkitTask backupTask;
|
||||
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
|
||||
|
||||
public BackupManager(PlayerDataSyncPremium plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start automatic backup task
|
||||
*/
|
||||
public void startAutomaticBackups() {
|
||||
if (!plugin.getConfigManager().isBackupEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int intervalMinutes = plugin.getConfigManager().getBackupInterval();
|
||||
if (intervalMinutes <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
long ticks = intervalMinutes * 60L * 20L; // Convert minutes to ticks
|
||||
|
||||
backupTask = SchedulerUtils.runTaskTimerAsync(plugin, () -> {
|
||||
try {
|
||||
createBackup("automatic");
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe("Automatic backup failed: " + e.getMessage());
|
||||
}
|
||||
}, ticks, ticks);
|
||||
|
||||
plugin.getLogger().info("Automatic backups enabled with " + intervalMinutes + " minute intervals");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop automatic backup task
|
||||
*/
|
||||
public void stopAutomaticBackups() {
|
||||
if (backupTask != null) {
|
||||
backupTask.cancel();
|
||||
backupTask = null;
|
||||
plugin.getLogger().info("Automatic backups disabled");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a backup with specified type
|
||||
*/
|
||||
public CompletableFuture<BackupResult> createBackup(String type) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
String timestamp = dateFormat.format(new java.util.Date());
|
||||
String backupName = "backup_" + type + "_" + timestamp;
|
||||
File backupDir = new File(plugin.getDataFolder(), "backups");
|
||||
|
||||
if (!backupDir.exists()) {
|
||||
backupDir.mkdirs();
|
||||
}
|
||||
|
||||
File backupFile = new File(backupDir, backupName + ".zip");
|
||||
|
||||
// Create backup
|
||||
try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(backupFile))) {
|
||||
// Backup database
|
||||
backupDatabase(zipOut, backupName);
|
||||
|
||||
// Backup configuration
|
||||
backupConfiguration(zipOut, backupName);
|
||||
|
||||
// Backup logs
|
||||
backupLogs(zipOut, backupName);
|
||||
}
|
||||
|
||||
// Clean old backups
|
||||
cleanOldBackups();
|
||||
|
||||
plugin.getLogger().info("Backup created: " + backupFile.getName());
|
||||
return new BackupResult(true, backupFile.getName(), backupFile.length());
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe("Backup creation failed: " + e.getMessage());
|
||||
return new BackupResult(false, null, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup database data
|
||||
*/
|
||||
private void backupDatabase(ZipOutputStream zipOut, String backupName) throws SQLException, IOException {
|
||||
Connection connection = plugin.getConnection();
|
||||
if (connection == null) {
|
||||
throw new SQLException("No database connection available");
|
||||
}
|
||||
|
||||
try {
|
||||
String tableName = plugin.getTablePrefix();
|
||||
// Create SQL dump
|
||||
StringBuilder sqlDump = new StringBuilder();
|
||||
sqlDump.append("-- PlayerDataSync Database Backup\n");
|
||||
sqlDump.append("-- Created: ").append(new java.util.Date()).append("\n\n");
|
||||
|
||||
// Get table structure
|
||||
DatabaseMetaData metaData = connection.getMetaData();
|
||||
try (ResultSet rs = metaData.getTables(null, null, tableName, new String[]{"TABLE"})) {
|
||||
if (rs.next()) {
|
||||
sqlDump.append("CREATE TABLE IF NOT EXISTS ").append(tableName).append(" (\n");
|
||||
try (ResultSet columns = metaData.getColumns(null, null, tableName, null)) {
|
||||
List<String> columnDefs = new ArrayList<>();
|
||||
while (columns.next()) {
|
||||
String columnName = columns.getString("COLUMN_NAME");
|
||||
String dataType = columns.getString("TYPE_NAME");
|
||||
int columnSize = columns.getInt("COLUMN_SIZE");
|
||||
String nullable = columns.getString("IS_NULLABLE");
|
||||
|
||||
StringBuilder columnDef = new StringBuilder(" ").append(columnName).append(" ");
|
||||
if (dataType.equals("VARCHAR")) {
|
||||
columnDef.append("VARCHAR(").append(columnSize).append(")");
|
||||
} else {
|
||||
columnDef.append(dataType);
|
||||
}
|
||||
|
||||
if ("NO".equals(nullable)) {
|
||||
columnDef.append(" NOT NULL");
|
||||
}
|
||||
|
||||
columnDefs.add(columnDef.toString());
|
||||
}
|
||||
sqlDump.append(String.join(",\n", columnDefs));
|
||||
}
|
||||
sqlDump.append("\n);\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Get table data
|
||||
try (PreparedStatement ps = connection.prepareStatement("SELECT * FROM " + tableName)) {
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
sqlDump.append("INSERT INTO ").append(tableName).append(" VALUES (");
|
||||
ResultSetMetaData rsMeta = rs.getMetaData();
|
||||
List<String> values = new ArrayList<>();
|
||||
for (int i = 1; i <= rsMeta.getColumnCount(); i++) {
|
||||
String value = rs.getString(i);
|
||||
if (value == null) {
|
||||
values.add("NULL");
|
||||
} else {
|
||||
values.add("'" + value.replace("'", "''") + "'");
|
||||
}
|
||||
}
|
||||
sqlDump.append(String.join(", ", values));
|
||||
sqlDump.append(");\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add to zip
|
||||
zipOut.putNextEntry(new ZipEntry(backupName + "/database.sql"));
|
||||
zipOut.write(sqlDump.toString().getBytes());
|
||||
zipOut.closeEntry();
|
||||
|
||||
} finally {
|
||||
plugin.returnConnection(connection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup configuration files
|
||||
*/
|
||||
private void backupConfiguration(ZipOutputStream zipOut, String backupName) throws IOException {
|
||||
File configFile = new File(plugin.getDataFolder(), "config.yml");
|
||||
if (configFile.exists()) {
|
||||
zipOut.putNextEntry(new ZipEntry(backupName + "/config.yml"));
|
||||
Files.copy(configFile.toPath(), zipOut);
|
||||
zipOut.closeEntry();
|
||||
}
|
||||
|
||||
// Backup message files
|
||||
File[] messageFiles = plugin.getDataFolder().listFiles((dir, name) -> name.startsWith("messages_") && name.endsWith(".yml"));
|
||||
if (messageFiles != null) {
|
||||
for (File messageFile : messageFiles) {
|
||||
zipOut.putNextEntry(new ZipEntry(backupName + "/" + messageFile.getName()));
|
||||
Files.copy(messageFile.toPath(), zipOut);
|
||||
zipOut.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup log files
|
||||
*/
|
||||
private void backupLogs(ZipOutputStream zipOut, String backupName) throws IOException {
|
||||
File logsDir = new File(plugin.getDataFolder(), "logs");
|
||||
if (logsDir.exists()) {
|
||||
Files.walk(logsDir.toPath())
|
||||
.filter(Files::isRegularFile)
|
||||
.forEach(logFile -> {
|
||||
try {
|
||||
String relativePath = logsDir.toPath().relativize(logFile).toString();
|
||||
zipOut.putNextEntry(new ZipEntry(backupName + "/logs/" + relativePath));
|
||||
Files.copy(logFile, zipOut);
|
||||
zipOut.closeEntry();
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().warning("Failed to backup log file: " + logFile + " - " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean old backups
|
||||
*/
|
||||
private void cleanOldBackups() {
|
||||
int keepBackups = plugin.getConfigManager().getBackupsToKeep();
|
||||
File backupDir = new File(plugin.getDataFolder(), "backups");
|
||||
|
||||
if (!backupDir.exists()) return;
|
||||
|
||||
File[] backupFiles = backupDir.listFiles((dir, name) -> name.endsWith(".zip"));
|
||||
if (backupFiles == null || backupFiles.length <= keepBackups) return;
|
||||
|
||||
// Sort by modification time (oldest first)
|
||||
Arrays.sort(backupFiles, Comparator.comparingLong(File::lastModified));
|
||||
|
||||
// Delete oldest backups
|
||||
int toDelete = backupFiles.length - keepBackups;
|
||||
for (int i = 0; i < toDelete; i++) {
|
||||
if (backupFiles[i].delete()) {
|
||||
plugin.getLogger().info("Deleted old backup: " + backupFiles[i].getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List available backups
|
||||
*/
|
||||
public List<BackupInfo> listBackups() {
|
||||
List<BackupInfo> backups = new ArrayList<>();
|
||||
File backupDir = new File(plugin.getDataFolder(), "backups");
|
||||
|
||||
if (!backupDir.exists()) return backups;
|
||||
|
||||
File[] backupFiles = backupDir.listFiles((dir, name) -> name.endsWith(".zip"));
|
||||
if (backupFiles == null) return backups;
|
||||
|
||||
for (File backupFile : backupFiles) {
|
||||
backups.add(new BackupInfo(
|
||||
backupFile.getName(),
|
||||
backupFile.length(),
|
||||
new java.util.Date(backupFile.lastModified())
|
||||
));
|
||||
}
|
||||
|
||||
// Sort by date (newest first)
|
||||
backups.sort(Comparator.comparing(BackupInfo::getCreatedDate).reversed());
|
||||
return backups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore from backup
|
||||
*/
|
||||
public CompletableFuture<Boolean> restoreFromBackup(String backupName) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
File backupFile = new File(plugin.getDataFolder(), "backups/" + backupName);
|
||||
if (!backupFile.exists()) {
|
||||
plugin.getLogger().severe("Backup file not found: " + backupName);
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Implement restore functionality
|
||||
plugin.getLogger().info("Restore from backup: " + backupName + " (not implemented yet)");
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe("Restore failed: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup result container
|
||||
*/
|
||||
public static class BackupResult {
|
||||
private final boolean success;
|
||||
private final String fileName;
|
||||
private final long fileSize;
|
||||
|
||||
public BackupResult(boolean success, String fileName, long fileSize) {
|
||||
this.success = success;
|
||||
this.fileName = fileName;
|
||||
this.fileSize = fileSize;
|
||||
}
|
||||
|
||||
public boolean isSuccess() { return success; }
|
||||
public String getFileName() { return fileName; }
|
||||
public long getFileSize() { return fileSize; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup info container
|
||||
*/
|
||||
public static class BackupInfo {
|
||||
private final String fileName;
|
||||
private final long fileSize;
|
||||
private final java.util.Date createdDate;
|
||||
|
||||
public BackupInfo(String fileName, long fileSize, java.util.Date createdDate) {
|
||||
this.fileName = fileName;
|
||||
this.fileSize = fileSize;
|
||||
this.createdDate = createdDate;
|
||||
}
|
||||
|
||||
public String getFileName() { return fileName; }
|
||||
public long getFileSize() { return fileSize; }
|
||||
public java.util.Date getCreatedDate() { return createdDate; }
|
||||
|
||||
public String getFormattedSize() {
|
||||
if (fileSize < 1024) return fileSize + " B";
|
||||
if (fileSize < 1024 * 1024) return String.format("%.1f KB", fileSize / 1024.0);
|
||||
return String.format("%.1f MB", fileSize / (1024.0 * 1024.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,545 @@
|
||||
package com.example.playerdatasync.premium.managers;
|
||||
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import com.example.playerdatasync.premium.core.PlayerDataSyncPremium;
|
||||
|
||||
/**
|
||||
* Configuration manager for PlayerDataSync
|
||||
* Handles validation, migration, and advanced configuration features
|
||||
*/
|
||||
public class ConfigManager {
|
||||
private final PlayerDataSyncPremium plugin;
|
||||
private FileConfiguration config;
|
||||
private File configFile;
|
||||
|
||||
// Configuration version for migration
|
||||
private static final int CURRENT_CONFIG_VERSION = 6;
|
||||
|
||||
public ConfigManager(PlayerDataSyncPremium plugin) {
|
||||
this.plugin = plugin;
|
||||
this.config = plugin.getConfig();
|
||||
this.configFile = new File(plugin.getDataFolder(), "config.yml");
|
||||
|
||||
validateAndMigrateConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and migrate configuration if needed
|
||||
*/
|
||||
private void validateAndMigrateConfig() {
|
||||
// Check if config is completely empty
|
||||
if (config.getKeys(false).isEmpty()) {
|
||||
plugin.getLogger().severe("Configuration file is completely empty! This indicates a serious problem.");
|
||||
plugin.getLogger().severe("Please check if the plugin JAR file is corrupted or if there are permission issues.");
|
||||
return;
|
||||
}
|
||||
|
||||
int configVersion = config.getInt("config-version", 1);
|
||||
|
||||
if (configVersion < CURRENT_CONFIG_VERSION) {
|
||||
plugin.getLogger().info("Migrating configuration from version " + configVersion + " to " + CURRENT_CONFIG_VERSION);
|
||||
migrateConfig(configVersion);
|
||||
}
|
||||
|
||||
// Validate configuration values
|
||||
validateConfiguration();
|
||||
|
||||
// Set current version
|
||||
config.set("config-version", CURRENT_CONFIG_VERSION);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate configuration from older versions
|
||||
*/
|
||||
private void migrateConfig(int fromVersion) {
|
||||
try {
|
||||
if (fromVersion < 2) {
|
||||
migrateFromV1ToV2();
|
||||
fromVersion = 2;
|
||||
}
|
||||
|
||||
if (fromVersion < 3) {
|
||||
migrateFromV2ToV3();
|
||||
fromVersion = 3;
|
||||
}
|
||||
|
||||
if (fromVersion < 4) {
|
||||
migrateFromV3ToV4();
|
||||
fromVersion = 4;
|
||||
}
|
||||
|
||||
if (fromVersion < 5) {
|
||||
migrateFromV4ToV5();
|
||||
fromVersion = 5;
|
||||
}
|
||||
|
||||
if (fromVersion < 6) {
|
||||
migrateFromV5ToV6();
|
||||
}
|
||||
|
||||
plugin.getLogger().info("Configuration migration completed successfully.");
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe("Configuration migration failed: " + e.getMessage());
|
||||
plugin.getLogger().log(java.util.logging.Level.SEVERE, "Stack trace:", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate configuration from version 1 to version 2
|
||||
*/
|
||||
private void migrateFromV1ToV2() {
|
||||
// Move language setting to messages section
|
||||
if (config.contains("language")) {
|
||||
String language = config.getString("language", "en");
|
||||
config.set("messages.language", language);
|
||||
config.set("language", null);
|
||||
}
|
||||
|
||||
// Move metrics setting to metrics section
|
||||
if (config.contains("metrics")) {
|
||||
boolean metrics = config.getBoolean("metrics", true);
|
||||
config.set("metrics.bstats", metrics);
|
||||
config.set("metrics", null);
|
||||
}
|
||||
|
||||
// Add new sections with default values
|
||||
addDefaultIfMissing("autosave.enabled", true);
|
||||
addDefaultIfMissing("autosave.on_world_change", true);
|
||||
addDefaultIfMissing("autosave.on_death", true);
|
||||
addDefaultIfMissing("autosave.async", true);
|
||||
|
||||
addDefaultIfMissing("performance.batch_size", 50);
|
||||
addDefaultIfMissing("performance.cache_size", 100);
|
||||
addDefaultIfMissing("performance.connection_pooling", true);
|
||||
addDefaultIfMissing("performance.async_loading", true);
|
||||
|
||||
addDefaultIfMissing("security.encrypt_data", false);
|
||||
addDefaultIfMissing("security.hash_uuids", false);
|
||||
addDefaultIfMissing("security.audit_log", true);
|
||||
|
||||
addDefaultIfMissing("logging.level", "INFO");
|
||||
addDefaultIfMissing("logging.log_database", false);
|
||||
addDefaultIfMissing("logging.log_performance", false);
|
||||
addDefaultIfMissing("logging.debug_mode", false);
|
||||
}
|
||||
|
||||
private void migrateFromV2ToV3() {
|
||||
addDefaultIfMissing("database.table_prefix", "player_data");
|
||||
|
||||
String prefix = config.getString("database.table_prefix", "player_data");
|
||||
String sanitized = sanitizeTablePrefix(prefix);
|
||||
if (!sanitized.equals(prefix)) {
|
||||
config.set("database.table_prefix", sanitized);
|
||||
plugin.getLogger().info("Sanitized database.table_prefix from '" + prefix + "' to '" + sanitized + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateFromV3ToV4() {
|
||||
// No longer required. Previous versions introduced editor configuration defaults
|
||||
// that have since been removed.
|
||||
}
|
||||
|
||||
private void migrateFromV4ToV5() {
|
||||
if (config.contains("editor")) {
|
||||
plugin.getLogger().info("Removing deprecated editor.* configuration entries.");
|
||||
config.set("editor", null);
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateFromV5ToV6() {
|
||||
addDefaultIfMissing("integrations.invsee", true);
|
||||
addDefaultIfMissing("integrations.openinv", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize default configuration if completely missing
|
||||
*/
|
||||
public void initializeDefaultConfig() {
|
||||
plugin.getLogger().info("Initializing default configuration...");
|
||||
|
||||
// Add all essential configuration sections
|
||||
addDefaultIfMissing("config-version", CURRENT_CONFIG_VERSION);
|
||||
|
||||
// Server configuration
|
||||
addDefaultIfMissing("server.id", "default");
|
||||
|
||||
// Database configuration
|
||||
addDefaultIfMissing("database.type", "mysql");
|
||||
addDefaultIfMissing("database.mysql.host", "localhost");
|
||||
addDefaultIfMissing("database.mysql.port", 3306);
|
||||
addDefaultIfMissing("database.mysql.database", "minecraft");
|
||||
addDefaultIfMissing("database.mysql.user", "root");
|
||||
addDefaultIfMissing("database.mysql.password", "password");
|
||||
addDefaultIfMissing("database.mysql.ssl", false);
|
||||
addDefaultIfMissing("database.mysql.connection_timeout", 5000);
|
||||
addDefaultIfMissing("database.mysql.max_connections", 10);
|
||||
addDefaultIfMissing("database.table_prefix", "player_data");
|
||||
addDefaultIfMissing("database.sqlite.file", "plugins/PlayerDataSync/playerdata.db");
|
||||
|
||||
// Sync configuration
|
||||
addDefaultIfMissing("sync.coordinates", true);
|
||||
addDefaultIfMissing("sync.position", true);
|
||||
addDefaultIfMissing("sync.xp", true);
|
||||
addDefaultIfMissing("sync.gamemode", true);
|
||||
addDefaultIfMissing("sync.inventory", true);
|
||||
addDefaultIfMissing("sync.enderchest", true);
|
||||
addDefaultIfMissing("sync.armor", true);
|
||||
addDefaultIfMissing("sync.offhand", true);
|
||||
addDefaultIfMissing("sync.health", true);
|
||||
addDefaultIfMissing("sync.hunger", true);
|
||||
addDefaultIfMissing("sync.effects", true);
|
||||
addDefaultIfMissing("sync.achievements", true);
|
||||
addDefaultIfMissing("sync.statistics", true);
|
||||
addDefaultIfMissing("sync.attributes", true);
|
||||
addDefaultIfMissing("sync.permissions", false);
|
||||
addDefaultIfMissing("sync.economy", false);
|
||||
|
||||
// Autosave configuration
|
||||
addDefaultIfMissing("autosave.enabled", true);
|
||||
addDefaultIfMissing("autosave.interval", 1);
|
||||
addDefaultIfMissing("autosave.on_world_change", true);
|
||||
addDefaultIfMissing("autosave.on_death", true);
|
||||
addDefaultIfMissing("autosave.async", true);
|
||||
|
||||
// Performance configuration
|
||||
addDefaultIfMissing("performance.batch_size", 50);
|
||||
addDefaultIfMissing("performance.cache_size", 100);
|
||||
addDefaultIfMissing("performance.cache_ttl", 300000);
|
||||
addDefaultIfMissing("performance.cache_compression", true);
|
||||
addDefaultIfMissing("performance.connection_pooling", true);
|
||||
addDefaultIfMissing("performance.async_loading", true);
|
||||
addDefaultIfMissing("performance.disable_achievement_sync_on_large_amounts", true);
|
||||
addDefaultIfMissing("performance.achievement_batch_size", 50);
|
||||
addDefaultIfMissing("performance.achievement_timeout_ms", 5000);
|
||||
addDefaultIfMissing("performance.max_achievements_per_player", 2000);
|
||||
addDefaultIfMissing("performance.preload_advancements_on_startup", true);
|
||||
addDefaultIfMissing("performance.advancement_import_batch_size", 250);
|
||||
addDefaultIfMissing("performance.player_advancement_import_batch_size", 150);
|
||||
addDefaultIfMissing("performance.automatic_player_advancement_import", true);
|
||||
|
||||
// Compatibility configuration
|
||||
addDefaultIfMissing("compatibility.safe_attribute_sync", true);
|
||||
addDefaultIfMissing("compatibility.disable_attributes_on_error", false);
|
||||
addDefaultIfMissing("compatibility.version_check", true);
|
||||
addDefaultIfMissing("compatibility.legacy_1_20_support", true);
|
||||
addDefaultIfMissing("compatibility.modern_1_21_support", true);
|
||||
addDefaultIfMissing("compatibility.disable_achievements_on_critical_error", true);
|
||||
|
||||
// Security configuration
|
||||
addDefaultIfMissing("security.encrypt_data", false);
|
||||
addDefaultIfMissing("security.hash_uuids", false);
|
||||
addDefaultIfMissing("security.audit_log", true);
|
||||
|
||||
// Logging configuration
|
||||
addDefaultIfMissing("logging.level", "INFO");
|
||||
addDefaultIfMissing("logging.log_database", false);
|
||||
addDefaultIfMissing("logging.log_performance", false);
|
||||
addDefaultIfMissing("logging.debug_mode", false);
|
||||
|
||||
// Update checker configuration
|
||||
addDefaultIfMissing("update_checker.enabled", true);
|
||||
addDefaultIfMissing("update_checker.notify_ops", true);
|
||||
addDefaultIfMissing("update_checker.auto_download", false);
|
||||
addDefaultIfMissing("update_checker.timeout", 10000);
|
||||
|
||||
// Metrics configuration
|
||||
addDefaultIfMissing("metrics.bstats", true);
|
||||
addDefaultIfMissing("metrics.custom_metrics", true);
|
||||
|
||||
addDefaultIfMissing("integrations.invsee", true);
|
||||
addDefaultIfMissing("integrations.openinv", true);
|
||||
|
||||
// Editor integration defaults
|
||||
// Messages configuration
|
||||
addDefaultIfMissing("messages.enabled", true);
|
||||
addDefaultIfMissing("messages.show_sync_messages", true);
|
||||
addDefaultIfMissing("messages.language", "en");
|
||||
addDefaultIfMissing("messages.prefix", "&8[&bPDS&8]");
|
||||
addDefaultIfMissing("messages.colors", true);
|
||||
|
||||
plugin.getLogger().info("Default configuration initialized successfully!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Add default value if key is missing
|
||||
*/
|
||||
private void addDefaultIfMissing(String path, Object defaultValue) {
|
||||
if (!config.contains(path)) {
|
||||
config.set(path, defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate configuration values
|
||||
*/
|
||||
private void validateConfiguration() {
|
||||
List<String> warnings = new ArrayList<>();
|
||||
|
||||
// Validate database settings
|
||||
String dbType = config.getString("database.type", "mysql").toLowerCase();
|
||||
if (!dbType.equals("mysql") && !dbType.equals("sqlite") && !dbType.equals("postgresql")) {
|
||||
warnings.add("Invalid database type: " + dbType + ". Using MySQL as default.");
|
||||
config.set("database.type", "mysql");
|
||||
}
|
||||
|
||||
String tablePrefix = config.getString("database.table_prefix", "player_data");
|
||||
String sanitizedPrefix = sanitizeTablePrefix(tablePrefix);
|
||||
if (sanitizedPrefix.isEmpty()) {
|
||||
warnings.add("database.table_prefix is empty or invalid. Using default 'player_data'.");
|
||||
sanitizedPrefix = "player_data";
|
||||
}
|
||||
if (!sanitizedPrefix.equals(tablePrefix)) {
|
||||
warnings.add("Sanitized database.table_prefix from '" + tablePrefix + "' to '" + sanitizedPrefix + "'.");
|
||||
config.set("database.table_prefix", sanitizedPrefix);
|
||||
}
|
||||
|
||||
// Validate autosave interval
|
||||
int interval = config.getInt("autosave.interval", 1);
|
||||
if (interval < 0) {
|
||||
warnings.add("Invalid autosave interval: " + interval + ". Using 1 second as default.");
|
||||
config.set("autosave.interval", 1);
|
||||
}
|
||||
|
||||
// Validate cache size
|
||||
int cacheSize = config.getInt("performance.cache_size", 100);
|
||||
if (cacheSize < 10 || cacheSize > 10000) {
|
||||
warnings.add("Invalid cache size: " + cacheSize + ". Using 100 as default.");
|
||||
config.set("performance.cache_size", 100);
|
||||
}
|
||||
|
||||
// Validate batch size
|
||||
int batchSize = config.getInt("performance.batch_size", 50);
|
||||
if (batchSize < 1 || batchSize > 1000) {
|
||||
warnings.add("Invalid batch size: " + batchSize + ". Using 50 as default.");
|
||||
config.set("performance.batch_size", 50);
|
||||
}
|
||||
|
||||
// Validate logging level
|
||||
String logLevelRaw = config.getString("logging.level", "INFO");
|
||||
Level resolvedLevel = parseLogLevel(logLevelRaw);
|
||||
if (resolvedLevel == null) {
|
||||
warnings.add("Invalid logging level: " + logLevelRaw + ". Using INFO as default.");
|
||||
config.set("logging.level", "INFO");
|
||||
} else {
|
||||
config.set("logging.level", resolvedLevel.getName());
|
||||
}
|
||||
|
||||
// Report warnings
|
||||
if (!warnings.isEmpty()) {
|
||||
plugin.getLogger().warning("Configuration validation found issues:");
|
||||
for (String warning : warnings) {
|
||||
plugin.getLogger().warning("- " + warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save configuration to file
|
||||
*/
|
||||
public void saveConfig() {
|
||||
try {
|
||||
config.save(configFile);
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().severe("Could not save configuration: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload configuration from file
|
||||
*/
|
||||
public void reloadConfig() {
|
||||
config = YamlConfiguration.loadConfiguration(configFile);
|
||||
validateAndMigrateConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration value with type safety
|
||||
*/
|
||||
public <T> T get(String path, T defaultValue, Class<T> type) {
|
||||
Object value = config.get(path, defaultValue);
|
||||
|
||||
if (type.isInstance(value)) {
|
||||
return type.cast(value);
|
||||
} else {
|
||||
plugin.getLogger().warning("Configuration value at '" + path + "' is not of expected type " + type.getSimpleName());
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if debugging is enabled
|
||||
*/
|
||||
public boolean isDebugMode() {
|
||||
return config.getBoolean("logging.debug_mode", false);
|
||||
}
|
||||
|
||||
public String getTablePrefix() {
|
||||
return sanitizeTablePrefix(config.getString("database.table_prefix", "player_data"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if database logging is enabled
|
||||
*/
|
||||
public boolean isDatabaseLoggingEnabled() {
|
||||
return config.getBoolean("logging.log_database", false);
|
||||
}
|
||||
|
||||
private String sanitizeTablePrefix(String prefix) {
|
||||
if (prefix == null) {
|
||||
return "player_data";
|
||||
}
|
||||
|
||||
String sanitized = prefix.trim().replaceAll("[^a-zA-Z0-9_]", "_");
|
||||
if (sanitized.isEmpty()) {
|
||||
return "player_data";
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if performance logging is enabled
|
||||
*/
|
||||
public boolean isPerformanceLoggingEnabled() {
|
||||
return config.getBoolean("logging.log_performance", false);
|
||||
}
|
||||
|
||||
public String getServerId() {
|
||||
return config.getString("server.id", "default");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logging level
|
||||
*/
|
||||
public Level getLoggingLevel() {
|
||||
Level level = parseLogLevel(config.getString("logging.level", "INFO"));
|
||||
return level != null ? level : Level.INFO;
|
||||
}
|
||||
|
||||
private Level parseLogLevel(String levelStr) {
|
||||
if (levelStr == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalized = levelStr.trim().toUpperCase(Locale.ROOT);
|
||||
switch (normalized) {
|
||||
case "WARN":
|
||||
case "WARNING":
|
||||
return Level.WARNING;
|
||||
case "ERROR":
|
||||
case "SEVERE":
|
||||
return Level.SEVERE;
|
||||
case "DEBUG":
|
||||
case "FINE":
|
||||
return Level.FINE;
|
||||
case "TRACE":
|
||||
case "FINER":
|
||||
return Level.FINER;
|
||||
case "FINEST":
|
||||
return Level.FINEST;
|
||||
case "CONFIG":
|
||||
return Level.CONFIG;
|
||||
case "ALL":
|
||||
return Level.ALL;
|
||||
case "OFF":
|
||||
return Level.OFF;
|
||||
case "INFO":
|
||||
return Level.INFO;
|
||||
default:
|
||||
try {
|
||||
return Level.parse(normalized);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a feature is enabled
|
||||
*/
|
||||
public boolean isFeatureEnabled(String feature) {
|
||||
return config.getBoolean("sync." + feature, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if data encryption is enabled
|
||||
*/
|
||||
public boolean isEncryptionEnabled() {
|
||||
return config.getBoolean("security.encrypt_data", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if UUID hashing is enabled
|
||||
*/
|
||||
public boolean isUuidHashingEnabled() {
|
||||
return config.getBoolean("security.hash_uuids", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if audit logging is enabled
|
||||
*/
|
||||
public boolean isAuditLogEnabled() {
|
||||
return config.getBoolean("security.audit_log", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cleanup settings
|
||||
*/
|
||||
public boolean isCleanupEnabled() {
|
||||
return config.getBoolean("data_management.cleanup.enabled", false);
|
||||
}
|
||||
|
||||
public int getCleanupDays() {
|
||||
return config.getInt("data_management.cleanup.days_inactive", 90);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backup settings
|
||||
*/
|
||||
public boolean isBackupEnabled() {
|
||||
return config.getBoolean("data_management.backup.enabled", true);
|
||||
}
|
||||
|
||||
public int getBackupInterval() {
|
||||
return config.getInt("data_management.backup.interval", 1440);
|
||||
}
|
||||
|
||||
public int getBackupsToKeep() {
|
||||
return config.getInt("data_management.backup.keep_backups", 7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation settings
|
||||
*/
|
||||
public boolean isValidationEnabled() {
|
||||
return config.getBoolean("data_management.validation.enabled", true);
|
||||
}
|
||||
|
||||
public boolean isStrictValidation() {
|
||||
return config.getBoolean("data_management.validation.strict_mode", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if sync messages should be shown to players
|
||||
*/
|
||||
public boolean shouldShowSyncMessages() {
|
||||
return config.getBoolean("messages.show_sync_messages", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying configuration
|
||||
*/
|
||||
public FileConfiguration getConfig() {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package com.example.playerdatasync.premium.managers;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import com.example.playerdatasync.premium.api.LicenseValidator;
|
||||
import com.example.playerdatasync.premium.api.LicenseValidator.LicenseValidationResult;
|
||||
import com.example.playerdatasync.premium.utils.SchedulerUtils;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* License Manager for PlayerDataSync Premium
|
||||
* Handles license validation, caching, and periodic re-validation
|
||||
*/
|
||||
public class LicenseManager {
|
||||
private final JavaPlugin plugin;
|
||||
private final LicenseValidator validator;
|
||||
private String licenseKey;
|
||||
private boolean licenseValid = false;
|
||||
private boolean licenseChecked = false;
|
||||
private long lastValidationTime = 0;
|
||||
private static final long REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1000; // Revalidate every 24 hours
|
||||
private int validationTaskId = -1;
|
||||
|
||||
public LicenseManager(JavaPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
this.validator = new LicenseValidator(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize license manager and validate license from config
|
||||
*/
|
||||
public void initialize() {
|
||||
FileConfiguration config = plugin.getConfig();
|
||||
licenseKey = config.getString("license.key", null);
|
||||
|
||||
if (licenseKey == null || licenseKey.trim().isEmpty()) {
|
||||
plugin.getLogger().severe("================================================");
|
||||
plugin.getLogger().severe("PlayerDataSync Premium - NO LICENSE KEY FOUND!");
|
||||
plugin.getLogger().severe("Please enter your license key in config.yml:");
|
||||
plugin.getLogger().severe("license:");
|
||||
plugin.getLogger().severe(" key: YOUR-LICENSE-KEY-HERE");
|
||||
plugin.getLogger().severe("================================================");
|
||||
plugin.getLogger().severe("The plugin will be disabled until a valid license is configured.");
|
||||
SchedulerUtils.runTask(plugin, () -> {
|
||||
plugin.getServer().getPluginManager().disablePlugin(plugin);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate license on startup
|
||||
validateLicense();
|
||||
|
||||
// Schedule periodic re-validation (every 24 hours)
|
||||
long intervalTicks = 20 * 60 * 60; // 1 hour in ticks
|
||||
validationTaskId = SchedulerUtils.runTaskTimerAsync(plugin, () -> {
|
||||
if (shouldRevalidate()) {
|
||||
plugin.getLogger().info("[LicenseManager] Performing scheduled license re-validation...");
|
||||
validateLicense();
|
||||
}
|
||||
}, intervalTicks, intervalTicks).getTaskId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the license key
|
||||
*/
|
||||
public void validateLicense() {
|
||||
if (licenseKey == null || licenseKey.trim().isEmpty()) {
|
||||
licenseValid = false;
|
||||
licenseChecked = true;
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getLogger().info("[LicenseManager] Validating license key...");
|
||||
|
||||
validator.validateLicenseAsync(licenseKey).thenAccept(result -> {
|
||||
SchedulerUtils.runTask(plugin, () -> {
|
||||
licenseValid = result.isValid();
|
||||
licenseChecked = true;
|
||||
lastValidationTime = System.currentTimeMillis();
|
||||
|
||||
if (licenseValid) {
|
||||
plugin.getLogger().info("================================================");
|
||||
plugin.getLogger().info("PlayerDataSync Premium - LICENSE VALIDATED!");
|
||||
if (result.getPurchase() != null) {
|
||||
plugin.getLogger().info("Purchase ID: " + result.getPurchase().getId());
|
||||
plugin.getLogger().info("User ID: " + result.getPurchase().getUserId());
|
||||
}
|
||||
plugin.getLogger().info("================================================");
|
||||
} else {
|
||||
plugin.getLogger().severe("================================================");
|
||||
plugin.getLogger().severe("PlayerDataSync Premium - LICENSE VALIDATION FAILED!");
|
||||
plugin.getLogger().severe("Reason: " + (result.getMessage() != null ? result.getMessage() : "Unknown error"));
|
||||
plugin.getLogger().severe("Please check your license key in config.yml");
|
||||
plugin.getLogger().severe("License key: " + maskLicenseKey(licenseKey));
|
||||
plugin.getLogger().severe("================================================");
|
||||
plugin.getLogger().severe("The plugin will be disabled in 30 seconds if the license is not valid.");
|
||||
|
||||
// Disable plugin after 30 seconds if license is invalid
|
||||
SchedulerUtils.runTaskLater(plugin, () -> {
|
||||
if (!isLicenseValid()) {
|
||||
plugin.getLogger().severe("License is still invalid. Disabling plugin...");
|
||||
plugin.getServer().getPluginManager().disablePlugin(plugin);
|
||||
}
|
||||
}, 600L); // 30 seconds
|
||||
}
|
||||
});
|
||||
}).exceptionally(throwable -> {
|
||||
plugin.getLogger().severe("[LicenseManager] License validation error: " + throwable.getMessage());
|
||||
plugin.getLogger().log(Level.FINE, "License validation exception", throwable);
|
||||
SchedulerUtils.runTask(plugin, () -> {
|
||||
licenseValid = false;
|
||||
licenseChecked = true;
|
||||
});
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if license should be revalidated
|
||||
*/
|
||||
private boolean shouldRevalidate() {
|
||||
return (System.currentTimeMillis() - lastValidationTime) >= REVALIDATION_INTERVAL_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if license is valid
|
||||
*/
|
||||
public boolean isLicenseValid() {
|
||||
return licenseValid && licenseChecked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if license has been checked
|
||||
*/
|
||||
public boolean isLicenseChecked() {
|
||||
return licenseChecked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the license key (masked for security)
|
||||
*/
|
||||
public String getLicenseKey() {
|
||||
return licenseKey != null ? maskLicenseKey(licenseKey) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new license key and validate it
|
||||
*/
|
||||
public void setLicenseKey(String newLicenseKey) {
|
||||
this.licenseKey = newLicenseKey;
|
||||
plugin.getConfig().set("license.key", newLicenseKey);
|
||||
plugin.saveConfig();
|
||||
validator.clearCache();
|
||||
validateLicense();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mask license key for logging (show only first and last 4 characters)
|
||||
*/
|
||||
private String maskLicenseKey(String key) {
|
||||
if (key == null || key.length() <= 8) {
|
||||
return "****";
|
||||
}
|
||||
return key.substring(0, 4) + "****" + key.substring(key.length() - 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown license manager
|
||||
*/
|
||||
public void shutdown() {
|
||||
if (validationTaskId != -1) {
|
||||
// Note: On Folia, tasks are cancelled automatically when plugin disables
|
||||
if (validationTaskId != -1) {
|
||||
try {
|
||||
Bukkit.getScheduler().cancelTask(validationTaskId);
|
||||
} catch (Exception e) {
|
||||
// Ignore errors on shutdown
|
||||
}
|
||||
}
|
||||
validationTaskId = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force revalidation of license
|
||||
*/
|
||||
public CompletableFuture<LicenseValidationResult> revalidateLicense() {
|
||||
validator.clearCache();
|
||||
return validator.validateLicenseAsync(licenseKey).thenApply(result -> {
|
||||
SchedulerUtils.runTask(plugin, () -> {
|
||||
licenseValid = result.isValid();
|
||||
lastValidationTime = System.currentTimeMillis();
|
||||
|
||||
if (licenseValid) {
|
||||
plugin.getLogger().info("[LicenseManager] License re-validation successful!");
|
||||
} else {
|
||||
plugin.getLogger().warning("[LicenseManager] License re-validation failed: " + result.getMessage());
|
||||
}
|
||||
});
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.example.playerdatasync.premium.managers;
|
||||
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.ChatColor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import com.example.playerdatasync.premium.core.PlayerDataSyncPremium;
|
||||
|
||||
public class MessageManager {
|
||||
private final PlayerDataSyncPremium plugin;
|
||||
private FileConfiguration messages;
|
||||
|
||||
public MessageManager(PlayerDataSyncPremium plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void load(String language) {
|
||||
String normalized = normalizeLanguage(language);
|
||||
|
||||
// Always load English as base defaults (from JAR first, else data folder)
|
||||
YamlConfiguration baseEn = new YamlConfiguration();
|
||||
InputStream enStream = plugin.getResource("messages_en.yml");
|
||||
if (enStream != null) {
|
||||
baseEn = YamlConfiguration.loadConfiguration(new InputStreamReader(enStream, StandardCharsets.UTF_8));
|
||||
} else {
|
||||
File enFile = new File(plugin.getDataFolder(), "messages_en.yml");
|
||||
if (enFile.exists()) {
|
||||
baseEn = YamlConfiguration.loadConfiguration(enFile);
|
||||
}
|
||||
}
|
||||
|
||||
// Now try to load the requested language, overlaying on top of English defaults
|
||||
YamlConfiguration selected = null;
|
||||
File file = new File(plugin.getDataFolder(), "messages_" + normalized + ".yml");
|
||||
try {
|
||||
if (!file.exists()) {
|
||||
plugin.saveResource("messages_" + normalized + ".yml", false);
|
||||
}
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Resource not embedded for this language
|
||||
}
|
||||
|
||||
if (file.exists()) {
|
||||
selected = YamlConfiguration.loadConfiguration(file);
|
||||
} else {
|
||||
InputStream jarStream = plugin.getResource("messages_" + normalized + ".yml");
|
||||
if (jarStream != null) {
|
||||
selected = YamlConfiguration.loadConfiguration(new InputStreamReader(jarStream, StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
if (selected == null) {
|
||||
// If requested language isn't available, use English directly
|
||||
this.messages = baseEn;
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply English as defaults so missing keys fall back
|
||||
selected.setDefaults(baseEn);
|
||||
selected.options().copyDefaults(true);
|
||||
this.messages = selected;
|
||||
}
|
||||
|
||||
public void loadFromConfig() {
|
||||
String lang = plugin.getConfig().getString("messages.language", "en");
|
||||
load(lang);
|
||||
}
|
||||
|
||||
private String normalizeLanguage(String language) {
|
||||
if (language == null || language.trim().isEmpty()) return "en";
|
||||
String lang = language.trim().toLowerCase().replace('-', '_');
|
||||
// Map common locale variants to base language files
|
||||
if (lang.startsWith("de")) return "de";
|
||||
if (lang.startsWith("en")) return "en";
|
||||
return lang;
|
||||
}
|
||||
|
||||
public String get(String key) {
|
||||
if (messages == null) return key;
|
||||
String raw = messages.getString(key, key);
|
||||
return ChatColor.translateAlternateColorCodes('&', raw);
|
||||
}
|
||||
|
||||
public String get(String key, String... params) {
|
||||
if (messages == null) return key;
|
||||
String raw = messages.getString(key, key);
|
||||
|
||||
// Replace placeholders with parameters
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
String placeholder = "{" + i + "}";
|
||||
if (raw.contains(placeholder)) {
|
||||
raw = raw.replace(placeholder, params[i] != null ? params[i] : "");
|
||||
}
|
||||
}
|
||||
|
||||
// Also support named placeholders for common cases
|
||||
if (params.length > 0) {
|
||||
raw = raw.replace("{version}", params[0] != null ? params[0] : "");
|
||||
raw = raw.replace("{error}", params[0] != null ? params[0] : "");
|
||||
raw = raw.replace("{url}", params[0] != null ? params[0] : "");
|
||||
}
|
||||
|
||||
return ChatColor.translateAlternateColorCodes('&', raw);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
package com.example.playerdatasync.premium.utils;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Enhanced inventory utilities for PlayerDataSync
|
||||
* Supports serialization of various inventory types and single items
|
||||
* Includes robust handling for custom enchantments from plugins like ExcellentEnchants
|
||||
*/
|
||||
public class InventoryUtils {
|
||||
|
||||
private static final String DOWNGRADE_ERROR_FRAGMENT = "Server downgrades are not supported";
|
||||
private static final String NEWER_VERSION_FRAGMENT = "Newer version";
|
||||
|
||||
// Statistics for deserialization issues
|
||||
private static int customEnchantmentFailures = 0;
|
||||
private static int versionCompatibilityFailures = 0;
|
||||
private static int otherDeserializationFailures = 0;
|
||||
|
||||
/**
|
||||
* Convert ItemStack array to Base64 string with validation
|
||||
* Preserves custom enchantments and NBT data from plugins like ExcellentEnchants
|
||||
*/
|
||||
public static String itemStackArrayToBase64(ItemStack[] items) throws IOException {
|
||||
if (items == null) return "";
|
||||
|
||||
// Validate and sanitize items before serialization
|
||||
// Note: sanitizeItemStackArray uses clone() which should preserve all NBT data including custom enchantments
|
||||
ItemStack[] sanitizedItems = sanitizeItemStackArray(items);
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
try (BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream)) {
|
||||
dataOutput.writeInt(sanitizedItems.length);
|
||||
for (ItemStack item : sanitizedItems) {
|
||||
// BukkitObjectOutputStream serializes the entire ItemStack including all NBT data,
|
||||
// which should preserve custom enchantments from plugins like ExcellentEnchants
|
||||
dataOutput.writeObject(item);
|
||||
}
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Base64 string to ItemStack array with validation and version compatibility
|
||||
* Preserves all NBT data including custom enchantments from plugins like ExcellentEnchants
|
||||
*/
|
||||
public static ItemStack[] itemStackArrayFromBase64(String data) throws IOException, ClassNotFoundException {
|
||||
if (data == null || data.isEmpty()) return new ItemStack[0];
|
||||
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data));
|
||||
ItemStack[] items;
|
||||
try (BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream)) {
|
||||
int length = dataInput.readInt();
|
||||
items = new ItemStack[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
try {
|
||||
// BukkitObjectInputStream deserializes the complete ItemStack including all NBT data
|
||||
// This preserves custom enchantments stored in PersistentDataContainer
|
||||
Object obj = dataInput.readObject();
|
||||
if (obj == null) {
|
||||
items[i] = null;
|
||||
continue;
|
||||
}
|
||||
items[i] = (ItemStack) obj;
|
||||
} catch (Exception e) {
|
||||
if (isVersionDowngradeIssue(e)) {
|
||||
versionCompatibilityFailures++;
|
||||
String enchantmentName = extractEnchantmentName(e);
|
||||
Bukkit.getLogger().warning("[PlayerDataSync] Version compatibility issue detected for item " + i
|
||||
+ (enchantmentName != null ? " (enchantment: " + enchantmentName + ")" : "")
|
||||
+ ": " + collectCompatibilityMessage(e) + ". Skipping unsupported item.");
|
||||
items[i] = null;
|
||||
} else if (isCustomEnchantmentIssue(e)) {
|
||||
customEnchantmentFailures++;
|
||||
String enchantmentName = extractEnchantmentName(e);
|
||||
|
||||
// Log detailed information about the custom enchantment issue
|
||||
Bukkit.getLogger().warning("[PlayerDataSync] Custom enchantment deserialization failed for item " + i
|
||||
+ (enchantmentName != null ? " (enchantment: " + enchantmentName + ")" : "")
|
||||
+ ". The enchantment plugin may not be loaded or the enchantment is not registered.");
|
||||
|
||||
// Extract more details from the error
|
||||
String errorDetails = extractErrorDetails(e);
|
||||
if (errorDetails != null && !errorDetails.isEmpty()) {
|
||||
Bukkit.getLogger().fine("[PlayerDataSync] Error details: " + errorDetails);
|
||||
}
|
||||
|
||||
// The item cannot be deserialized due to the custom enchantment issue
|
||||
// The NBT data is preserved in the database and will be available once
|
||||
// the enchantment plugin is properly loaded and recognizes the enchantment
|
||||
items[i] = null;
|
||||
|
||||
Bukkit.getLogger().info("[PlayerDataSync] Item " + i + " skipped. Data preserved in database. " +
|
||||
"Ensure the enchantment plugin (e.g., ExcellentEnchants) is loaded and the enchantment is registered.");
|
||||
} else {
|
||||
otherDeserializationFailures++;
|
||||
String errorType = e.getClass().getSimpleName();
|
||||
Bukkit.getLogger().warning("[PlayerDataSync] Failed to deserialize item " + i
|
||||
+ " (error type: " + errorType + "): " + collectCompatibilityMessage(e) + ". Skipping item.");
|
||||
items[i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate deserialized items
|
||||
// Note: We don't sanitize here to preserve all NBT data including custom enchantments
|
||||
// Only validate that items are not corrupted
|
||||
if (!validateItemStackArray(items)) {
|
||||
// If validation fails, sanitize the items (but preserve NBT data via clone())
|
||||
return sanitizeItemStackArray(items);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert single ItemStack to Base64 string
|
||||
*/
|
||||
public static String itemStackToBase64(ItemStack item) throws IOException {
|
||||
if (item == null) return "";
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
try (BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream)) {
|
||||
dataOutput.writeObject(item);
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Base64 string to single ItemStack with version compatibility
|
||||
* Preserves all NBT data including custom enchantments from plugins like ExcellentEnchants
|
||||
*/
|
||||
public static ItemStack itemStackFromBase64(String data) throws IOException, ClassNotFoundException {
|
||||
if (data == null || data.isEmpty()) return null;
|
||||
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data));
|
||||
try (BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream)) {
|
||||
try {
|
||||
// BukkitObjectInputStream deserializes the complete ItemStack including all NBT data
|
||||
// This preserves custom enchantments stored in PersistentDataContainer
|
||||
Object obj = dataInput.readObject();
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
return (ItemStack) obj;
|
||||
} catch (Exception e) {
|
||||
if (isVersionDowngradeIssue(e)) {
|
||||
versionCompatibilityFailures++;
|
||||
String enchantmentName = extractEnchantmentName(e);
|
||||
Bukkit.getLogger().warning("[PlayerDataSync] Version compatibility issue detected for single item"
|
||||
+ (enchantmentName != null ? " (enchantment: " + enchantmentName + ")" : "")
|
||||
+ ": " + collectCompatibilityMessage(e) + ". Returning null.");
|
||||
return null;
|
||||
} else if (isCustomEnchantmentIssue(e)) {
|
||||
customEnchantmentFailures++;
|
||||
String enchantmentName = extractEnchantmentName(e);
|
||||
Bukkit.getLogger().warning("[PlayerDataSync] Custom enchantment deserialization failed for single item"
|
||||
+ (enchantmentName != null ? " (enchantment: " + enchantmentName + ")" : "")
|
||||
+ ". The enchantment plugin may not be loaded or the enchantment is not registered.");
|
||||
String errorDetails = extractErrorDetails(e);
|
||||
if (errorDetails != null && !errorDetails.isEmpty()) {
|
||||
Bukkit.getLogger().fine("[PlayerDataSync] Error details: " + errorDetails);
|
||||
}
|
||||
Bukkit.getLogger().info("[PlayerDataSync] Item skipped. Data preserved in database. " +
|
||||
"Ensure the enchantment plugin (e.g., ExcellentEnchants) is loaded and the enchantment is registered.");
|
||||
return null;
|
||||
} else {
|
||||
otherDeserializationFailures++;
|
||||
String errorType = e.getClass().getSimpleName();
|
||||
Bukkit.getLogger().warning("[PlayerDataSync] Failed to deserialize single item (error type: "
|
||||
+ errorType + "): " + collectCompatibilityMessage(e) + ". Returning null.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate ItemStack array for corruption
|
||||
*/
|
||||
public static boolean validateItemStackArray(ItemStack[] items) {
|
||||
if (items == null) return true;
|
||||
|
||||
try {
|
||||
for (ItemStack item : items) {
|
||||
if (item != null) {
|
||||
// Basic validation - check if the item is valid
|
||||
item.getType();
|
||||
item.getAmount();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize ItemStack array (remove invalid items and validate)
|
||||
* IMPORTANT: Uses clone() which preserves all NBT data including custom enchantments
|
||||
* from plugins like ExcellentEnchants. The clone operation maintains the complete
|
||||
* ItemStack state including PersistentDataContainer entries.
|
||||
*/
|
||||
public static ItemStack[] sanitizeItemStackArray(ItemStack[] items) {
|
||||
if (items == null) return null;
|
||||
|
||||
ItemStack[] sanitized = new ItemStack[items.length];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
try {
|
||||
ItemStack item = items[i];
|
||||
if (item != null) {
|
||||
// Validate item type and amount
|
||||
if (item.getType() != null && item.getType() != org.bukkit.Material.AIR) {
|
||||
int amount = item.getAmount();
|
||||
// Ensure amount is within valid range (1-64 for most items, up to 127 for stackable items)
|
||||
if (amount > 0 && amount <= item.getMaxStackSize()) {
|
||||
// clone() preserves all NBT data including custom enchantments
|
||||
sanitized[i] = item.clone();
|
||||
} else {
|
||||
// Fix invalid stack sizes
|
||||
if (amount <= 0) {
|
||||
sanitized[i] = null; // Remove invalid items with 0 or negative amount
|
||||
} else {
|
||||
// Clamp to max stack size
|
||||
// clone() preserves all NBT data including custom enchantments
|
||||
ItemStack fixed = item.clone();
|
||||
fixed.setAmount(Math.min(amount, item.getMaxStackSize()));
|
||||
sanitized[i] = fixed;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sanitized[i] = null; // Remove AIR items
|
||||
}
|
||||
} else {
|
||||
sanitized[i] = null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Log exception but don't expose sensitive data
|
||||
Bukkit.getLogger().fine("[PlayerDataSync] Error sanitizing item at index " + i + ": " + e.getClass().getSimpleName());
|
||||
// Skip invalid items that cause exceptions
|
||||
sanitized[i] = null;
|
||||
}
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count non-null items in array
|
||||
*/
|
||||
public static int countItems(ItemStack[] items) {
|
||||
if (items == null) return 0;
|
||||
|
||||
int count = 0;
|
||||
for (ItemStack item : items) {
|
||||
if (item != null) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total storage size of items
|
||||
*/
|
||||
public static long calculateStorageSize(ItemStack[] items) {
|
||||
if (items == null) return 0;
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
try (BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream)) {
|
||||
dataOutput.writeInt(items.length);
|
||||
for (ItemStack item : items) {
|
||||
dataOutput.writeObject(item);
|
||||
}
|
||||
}
|
||||
return outputStream.size();
|
||||
} catch (IOException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress ItemStack array data
|
||||
*/
|
||||
public static String compressItemStackArray(ItemStack[] items) throws IOException {
|
||||
if (items == null) return "";
|
||||
|
||||
// For now, just use the standard serialization
|
||||
// In the future, this could implement actual compression
|
||||
return itemStackArrayToBase64(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompress ItemStack array data with version compatibility
|
||||
*/
|
||||
public static ItemStack[] decompressItemStackArray(String data) throws IOException, ClassNotFoundException {
|
||||
if (data == null || data.isEmpty()) return new ItemStack[0];
|
||||
|
||||
// For now, just use the standard deserialization
|
||||
// In the future, this could implement actual decompression
|
||||
return itemStackArrayFromBase64(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely deserialize ItemStack array with comprehensive error handling
|
||||
* Returns empty array if deserialization fails completely
|
||||
* Tracks statistics for debugging and monitoring
|
||||
*/
|
||||
public static ItemStack[] safeItemStackArrayFromBase64(String data) {
|
||||
if (data == null || data.isEmpty()) return new ItemStack[0];
|
||||
|
||||
int failuresBefore = customEnchantmentFailures + versionCompatibilityFailures + otherDeserializationFailures;
|
||||
|
||||
try {
|
||||
ItemStack[] result = itemStackArrayFromBase64(data);
|
||||
|
||||
// Log statistics if there were failures during this deserialization
|
||||
int failuresAfter = customEnchantmentFailures + versionCompatibilityFailures + otherDeserializationFailures;
|
||||
if (failuresAfter > failuresBefore) {
|
||||
int newFailures = failuresAfter - failuresBefore;
|
||||
Bukkit.getLogger().fine("[PlayerDataSync] Deserialization completed with " + newFailures +
|
||||
" item(s) skipped due to errors. " + getDeserializationStats());
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
otherDeserializationFailures++;
|
||||
String errorType = e.getClass().getSimpleName();
|
||||
Bukkit.getLogger().severe("[PlayerDataSync] Critical failure deserializing ItemStack array (error type: "
|
||||
+ errorType + "): " + collectCompatibilityMessage(e));
|
||||
Bukkit.getLogger().severe("[PlayerDataSync] This may indicate corrupted data or a serious compatibility issue.");
|
||||
return new ItemStack[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely deserialize single ItemStack with comprehensive error handling
|
||||
* Returns null if deserialization fails
|
||||
* Tracks statistics for debugging and monitoring
|
||||
*/
|
||||
public static ItemStack safeItemStackFromBase64(String data) {
|
||||
if (data == null || data.isEmpty()) return null;
|
||||
|
||||
try {
|
||||
return itemStackFromBase64(data);
|
||||
} catch (Exception e) {
|
||||
otherDeserializationFailures++;
|
||||
String errorType = e.getClass().getSimpleName();
|
||||
Bukkit.getLogger().warning("[PlayerDataSync] Failed to deserialize single ItemStack (error type: "
|
||||
+ errorType + "): " + collectCompatibilityMessage(e));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isVersionDowngradeIssue(Throwable throwable) {
|
||||
Throwable current = throwable;
|
||||
while (current != null) {
|
||||
String message = current.getMessage();
|
||||
if (message != null && (message.contains(NEWER_VERSION_FRAGMENT)
|
||||
|| message.contains(DOWNGRADE_ERROR_FRAGMENT))) {
|
||||
return true;
|
||||
}
|
||||
current = current.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the error is related to custom enchantments not being recognized
|
||||
* This happens when plugins like ExcellentEnchants store enchantments that aren't
|
||||
* recognized during deserialization
|
||||
*/
|
||||
private static boolean isCustomEnchantmentIssue(Throwable throwable) {
|
||||
Throwable current = throwable;
|
||||
while (current != null) {
|
||||
String message = current.getMessage();
|
||||
if (message != null) {
|
||||
// Check for common custom enchantment error patterns
|
||||
String lowerMessage = message.toLowerCase();
|
||||
if (message.contains("Failed to get element") ||
|
||||
message.contains("missed input") ||
|
||||
(message.contains("minecraft:") && (message.contains("venom") ||
|
||||
message.contains("enchantments") || message.contains("enchant"))) ||
|
||||
(message.contains("Cannot invoke") && message.contains("getClass()")) ||
|
||||
(current instanceof IllegalStateException && message.contains("Failed to get element")) ||
|
||||
lowerMessage.contains("enchantment") && (lowerMessage.contains("not found") ||
|
||||
lowerMessage.contains("unknown") || lowerMessage.contains("invalid"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Check exception type
|
||||
if (current instanceof IllegalStateException) {
|
||||
String className = current.getClass().getName();
|
||||
if (className.contains("DataResult") || className.contains("serialization") ||
|
||||
className.contains("Codec") || className.contains("Decoder")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Check for NullPointerException related to enchantment deserialization
|
||||
if (current instanceof NullPointerException) {
|
||||
StackTraceElement[] stack = current.getStackTrace();
|
||||
for (StackTraceElement element : stack) {
|
||||
String className = element.getClassName();
|
||||
if (className.contains("enchant") || className.contains("ItemStack") ||
|
||||
className.contains("serialization") || className.contains("ConfigurationSerialization")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
current = current.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract enchantment name from error message if available
|
||||
* Helps identify which specific enchantment caused the issue
|
||||
*/
|
||||
private static String extractEnchantmentName(Throwable throwable) {
|
||||
Throwable current = throwable;
|
||||
while (current != null) {
|
||||
String message = current.getMessage();
|
||||
if (message != null) {
|
||||
// Look for patterns like "minecraft:venom" or "venom" in error messages
|
||||
if (message.contains("minecraft:")) {
|
||||
int start = message.indexOf("minecraft:");
|
||||
if (start >= 0) {
|
||||
int end = message.indexOf(" ", start);
|
||||
if (end < 0) end = message.indexOf("}", start);
|
||||
if (end < 0) end = message.indexOf("\"", start);
|
||||
if (end < 0) end = message.indexOf(",", start);
|
||||
if (end < 0) end = Math.min(start + 50, message.length());
|
||||
if (end > start) {
|
||||
String enchantment = message.substring(start, end).trim();
|
||||
// Clean up common suffixes
|
||||
enchantment = enchantment.replaceAll("[}\",\\]]", "");
|
||||
if (enchantment.length() > 10 && enchantment.length() < 100) {
|
||||
return enchantment;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Look for enchantment names in quotes or braces
|
||||
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("[\"']([a-z0-9_:-]+enchant[a-z0-9_:-]*|venom|curse|soul|telepathy)[\"']",
|
||||
java.util.regex.Pattern.CASE_INSENSITIVE);
|
||||
java.util.regex.Matcher matcher = pattern.matcher(message);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
}
|
||||
current = current.getCause();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract detailed error information for debugging
|
||||
*/
|
||||
private static String extractErrorDetails(Throwable throwable) {
|
||||
StringBuilder details = new StringBuilder();
|
||||
Throwable current = throwable;
|
||||
int depth = 0;
|
||||
while (current != null && depth < 3) {
|
||||
if (details.length() > 0) {
|
||||
details.append(" -> ");
|
||||
}
|
||||
details.append(current.getClass().getSimpleName());
|
||||
String message = current.getMessage();
|
||||
if (message != null && message.length() < 200) {
|
||||
details.append(": ").append(message.substring(0, Math.min(message.length(), 100)));
|
||||
if (message.length() > 100) {
|
||||
details.append("...");
|
||||
}
|
||||
}
|
||||
current = current.getCause();
|
||||
depth++;
|
||||
}
|
||||
return details.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics about deserialization failures
|
||||
* Useful for debugging and monitoring
|
||||
*/
|
||||
public static String getDeserializationStats() {
|
||||
int total = customEnchantmentFailures + versionCompatibilityFailures + otherDeserializationFailures;
|
||||
if (total == 0) {
|
||||
return "No deserialization failures recorded.";
|
||||
}
|
||||
return String.format("Deserialization failures: %d total (Custom Enchantments: %d, Version Issues: %d, Other: %d)",
|
||||
total, customEnchantmentFailures, versionCompatibilityFailures, otherDeserializationFailures);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset deserialization statistics
|
||||
*/
|
||||
public static void resetDeserializationStats() {
|
||||
customEnchantmentFailures = 0;
|
||||
versionCompatibilityFailures = 0;
|
||||
otherDeserializationFailures = 0;
|
||||
}
|
||||
|
||||
private static String collectCompatibilityMessage(Throwable throwable) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
Throwable current = throwable;
|
||||
boolean first = true;
|
||||
while (current != null) {
|
||||
String message = current.getMessage();
|
||||
if (message != null && !message.isEmpty()) {
|
||||
if (!first) {
|
||||
builder.append(" | cause: ");
|
||||
}
|
||||
builder.append(message);
|
||||
first = false;
|
||||
}
|
||||
current = current.getCause();
|
||||
}
|
||||
if (builder.length() == 0) {
|
||||
builder.append(throwable.getClass().getName());
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.example.playerdatasync.premium.utils;
|
||||
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents offline player inventory data that can be viewed or edited
|
||||
* without the player being online.
|
||||
*/
|
||||
public class OfflinePlayerData {
|
||||
private final UUID uuid;
|
||||
private final String lastKnownName;
|
||||
private ItemStack[] inventoryContents;
|
||||
private ItemStack[] armorContents;
|
||||
private ItemStack[] enderChestContents;
|
||||
private ItemStack offhandItem;
|
||||
private boolean existsInDatabase;
|
||||
|
||||
public OfflinePlayerData(UUID uuid, String lastKnownName) {
|
||||
this.uuid = uuid;
|
||||
this.lastKnownName = lastKnownName;
|
||||
this.inventoryContents = new ItemStack[0];
|
||||
this.armorContents = new ItemStack[0];
|
||||
this.enderChestContents = new ItemStack[0];
|
||||
this.offhandItem = null;
|
||||
this.existsInDatabase = false;
|
||||
}
|
||||
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return lastKnownName != null ? lastKnownName : (uuid != null ? uuid.toString() : "unknown");
|
||||
}
|
||||
|
||||
public ItemStack[] getInventoryContents() {
|
||||
return inventoryContents != null ? inventoryContents : new ItemStack[0];
|
||||
}
|
||||
|
||||
public void setInventoryContents(ItemStack[] inventoryContents) {
|
||||
this.inventoryContents = inventoryContents != null ? inventoryContents : new ItemStack[0];
|
||||
}
|
||||
|
||||
public ItemStack[] getArmorContents() {
|
||||
return armorContents != null ? armorContents : new ItemStack[0];
|
||||
}
|
||||
|
||||
public void setArmorContents(ItemStack[] armorContents) {
|
||||
this.armorContents = armorContents != null ? armorContents : new ItemStack[0];
|
||||
}
|
||||
|
||||
public ItemStack[] getEnderChestContents() {
|
||||
return enderChestContents != null ? enderChestContents : new ItemStack[0];
|
||||
}
|
||||
|
||||
public void setEnderChestContents(ItemStack[] enderChestContents) {
|
||||
this.enderChestContents = enderChestContents != null ? enderChestContents : new ItemStack[0];
|
||||
}
|
||||
|
||||
public ItemStack getOffhandItem() {
|
||||
return offhandItem;
|
||||
}
|
||||
|
||||
public void setOffhandItem(ItemStack offhandItem) {
|
||||
this.offhandItem = offhandItem;
|
||||
}
|
||||
|
||||
public boolean existsInDatabase() {
|
||||
return existsInDatabase;
|
||||
}
|
||||
|
||||
public void setExistsInDatabase(boolean existsInDatabase) {
|
||||
this.existsInDatabase = existsInDatabase;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package com.example.playerdatasync.premium.utils;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.example.playerdatasync.premium.core.PlayerDataSyncPremium;
|
||||
|
||||
/**
|
||||
* Advanced caching system for PlayerDataSync
|
||||
* Provides in-memory caching with TTL, LRU eviction, and performance metrics
|
||||
*/
|
||||
public class PlayerDataCache {
|
||||
// Plugin reference kept for potential future use
|
||||
@SuppressWarnings("unused")
|
||||
private final PlayerDataSyncPremium plugin;
|
||||
private final ConcurrentHashMap<UUID, CachedPlayerData> cache;
|
||||
private final AtomicLong hits = new AtomicLong(0);
|
||||
private final AtomicLong misses = new AtomicLong(0);
|
||||
private final AtomicLong evictions = new AtomicLong(0);
|
||||
|
||||
private final int maxCacheSize;
|
||||
private final long defaultTTL;
|
||||
private final boolean enableCompression;
|
||||
|
||||
public PlayerDataCache(PlayerDataSyncPremium plugin) {
|
||||
this.plugin = plugin;
|
||||
this.cache = new ConcurrentHashMap<>();
|
||||
this.maxCacheSize = plugin.getConfig().getInt("performance.cache_size", 100);
|
||||
this.defaultTTL = plugin.getConfig().getLong("performance.cache_ttl", 300000); // 5 minutes
|
||||
this.enableCompression = plugin.getConfig().getBoolean("performance.cache_compression", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache player data with TTL
|
||||
*/
|
||||
public void cachePlayerData(Player player, CachedPlayerData data) {
|
||||
if (cache.size() >= maxCacheSize) {
|
||||
evictLeastRecentlyUsed();
|
||||
}
|
||||
|
||||
data.setLastAccessed(System.currentTimeMillis());
|
||||
data.setTtl(defaultTTL);
|
||||
|
||||
if (enableCompression) {
|
||||
data.compress();
|
||||
}
|
||||
|
||||
cache.put(player.getUniqueId(), data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached player data
|
||||
*/
|
||||
public CachedPlayerData getCachedPlayerData(Player player) {
|
||||
CachedPlayerData data = cache.get(player.getUniqueId());
|
||||
|
||||
if (data == null) {
|
||||
misses.incrementAndGet();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if data has expired
|
||||
if (data.isExpired()) {
|
||||
cache.remove(player.getUniqueId());
|
||||
misses.incrementAndGet();
|
||||
return null;
|
||||
}
|
||||
|
||||
data.setLastAccessed(System.currentTimeMillis());
|
||||
hits.incrementAndGet();
|
||||
|
||||
if (enableCompression && data.isCompressed()) {
|
||||
data.decompress();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove player data from cache
|
||||
*/
|
||||
public void removePlayerData(Player player) {
|
||||
cache.remove(player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached data
|
||||
*/
|
||||
public void clearCache() {
|
||||
cache.clear();
|
||||
hits.set(0);
|
||||
misses.set(0);
|
||||
evictions.set(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict least recently used entries
|
||||
*/
|
||||
private void evictLeastRecentlyUsed() {
|
||||
if (cache.isEmpty()) return;
|
||||
|
||||
UUID oldestKey = null;
|
||||
long oldestTime = Long.MAX_VALUE;
|
||||
|
||||
for (Map.Entry<UUID, CachedPlayerData> entry : cache.entrySet()) {
|
||||
if (entry.getValue().getLastAccessed() < oldestTime) {
|
||||
oldestTime = entry.getValue().getLastAccessed();
|
||||
oldestKey = entry.getKey();
|
||||
}
|
||||
}
|
||||
|
||||
if (oldestKey != null) {
|
||||
cache.remove(oldestKey);
|
||||
evictions.incrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean expired entries
|
||||
*/
|
||||
public void cleanExpiredEntries() {
|
||||
cache.entrySet().removeIf(entry -> entry.getValue().isExpired());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
public CacheStats getStats() {
|
||||
long totalRequests = hits.get() + misses.get();
|
||||
double hitRate = totalRequests > 0 ? (double) hits.get() / totalRequests * 100 : 0;
|
||||
|
||||
return new CacheStats(
|
||||
cache.size(),
|
||||
maxCacheSize,
|
||||
hits.get(),
|
||||
misses.get(),
|
||||
evictions.get(),
|
||||
hitRate
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached player data container
|
||||
*/
|
||||
public static class CachedPlayerData {
|
||||
private String inventoryData;
|
||||
private String enderChestData;
|
||||
private String armorData;
|
||||
private String offhandData;
|
||||
private String effectsData;
|
||||
private String statisticsData;
|
||||
private String attributesData;
|
||||
private String advancementsData;
|
||||
private long lastAccessed;
|
||||
private long ttl;
|
||||
private boolean compressed = false;
|
||||
|
||||
public CachedPlayerData() {
|
||||
this.lastAccessed = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
// Getters and setters
|
||||
public String getInventoryData() { return inventoryData; }
|
||||
public void setInventoryData(String inventoryData) { this.inventoryData = inventoryData; }
|
||||
|
||||
public String getEnderChestData() { return enderChestData; }
|
||||
public void setEnderChestData(String enderChestData) { this.enderChestData = enderChestData; }
|
||||
|
||||
public String getArmorData() { return armorData; }
|
||||
public void setArmorData(String armorData) { this.armorData = armorData; }
|
||||
|
||||
public String getOffhandData() { return offhandData; }
|
||||
public void setOffhandData(String offhandData) { this.offhandData = offhandData; }
|
||||
|
||||
public String getEffectsData() { return effectsData; }
|
||||
public void setEffectsData(String effectsData) { this.effectsData = effectsData; }
|
||||
|
||||
public String getStatisticsData() { return statisticsData; }
|
||||
public void setStatisticsData(String statisticsData) { this.statisticsData = statisticsData; }
|
||||
|
||||
public String getAttributesData() { return attributesData; }
|
||||
public void setAttributesData(String attributesData) { this.attributesData = attributesData; }
|
||||
|
||||
public String getAdvancementsData() { return advancementsData; }
|
||||
public void setAdvancementsData(String advancementsData) { this.advancementsData = advancementsData; }
|
||||
|
||||
public long getLastAccessed() { return lastAccessed; }
|
||||
public void setLastAccessed(long lastAccessed) { this.lastAccessed = lastAccessed; }
|
||||
|
||||
public long getTtl() { return ttl; }
|
||||
public void setTtl(long ttl) { this.ttl = ttl; }
|
||||
|
||||
public boolean isExpired() {
|
||||
return System.currentTimeMillis() - lastAccessed > ttl;
|
||||
}
|
||||
|
||||
public boolean isCompressed() { return compressed; }
|
||||
public void setCompressed(boolean compressed) { this.compressed = compressed; }
|
||||
|
||||
/**
|
||||
* Compress data (placeholder for future compression implementation)
|
||||
*/
|
||||
public void compress() {
|
||||
// TODO: Implement actual compression
|
||||
this.compressed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompress data (placeholder for future compression implementation)
|
||||
*/
|
||||
public void decompress() {
|
||||
// TODO: Implement actual decompression
|
||||
this.compressed = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache statistics container
|
||||
*/
|
||||
public static class CacheStats {
|
||||
private final int currentSize;
|
||||
private final int maxSize;
|
||||
private final long hits;
|
||||
private final long misses;
|
||||
private final long evictions;
|
||||
private final double hitRate;
|
||||
|
||||
public CacheStats(int currentSize, int maxSize, long hits, long misses, long evictions, double hitRate) {
|
||||
this.currentSize = currentSize;
|
||||
this.maxSize = maxSize;
|
||||
this.hits = hits;
|
||||
this.misses = misses;
|
||||
this.evictions = evictions;
|
||||
this.hitRate = hitRate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Cache: %d/%d entries, Hit Rate: %.1f%%, Hits: %d, Misses: %d, Evictions: %d",
|
||||
currentSize, maxSize, hitRate, hits, misses, evictions);
|
||||
}
|
||||
|
||||
// Getters
|
||||
public int getCurrentSize() { return currentSize; }
|
||||
public int getMaxSize() { return maxSize; }
|
||||
public long getHits() { return hits; }
|
||||
public long getMisses() { return misses; }
|
||||
public long getEvictions() { return evictions; }
|
||||
public double getHitRate() { return hitRate; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package com.example.playerdatasync.premium.utils;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Utility class for Folia compatibility
|
||||
* Automatically detects Folia and uses appropriate schedulers
|
||||
*
|
||||
* Based on official Folia API from PaperMC:
|
||||
* - GlobalRegionScheduler: For global tasks
|
||||
* - RegionScheduler: For region-specific tasks (player/location-based)
|
||||
* - AsyncScheduler: For async tasks
|
||||
*
|
||||
* @see <a href="https://papermc.io/downloads/folia">Folia Downloads</a>
|
||||
* @see <a href="https://docs.papermc.io/paper/dev/folia-support">Folia Support Documentation</a>
|
||||
*/
|
||||
public class SchedulerUtils {
|
||||
private static final boolean IS_FOLIA;
|
||||
|
||||
static {
|
||||
boolean folia = false;
|
||||
try {
|
||||
Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
|
||||
folia = true;
|
||||
} catch (ClassNotFoundException e) {
|
||||
// Not Folia
|
||||
}
|
||||
IS_FOLIA = folia;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the server is running Folia
|
||||
*/
|
||||
public static boolean isFolia() {
|
||||
return IS_FOLIA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a task synchronously on the main thread (or region for Folia)
|
||||
*/
|
||||
public static BukkitTask runTask(Plugin plugin, Runnable task) {
|
||||
if (IS_FOLIA) {
|
||||
try {
|
||||
// Use GlobalRegionScheduler for global tasks (Folia API)
|
||||
Object server = Bukkit.getServer();
|
||||
Object globalScheduler = server.getClass()
|
||||
.getMethod("getGlobalRegionScheduler")
|
||||
.invoke(server);
|
||||
|
||||
// GlobalRegionScheduler.run(Plugin, Consumer)
|
||||
return (BukkitTask) globalScheduler.getClass()
|
||||
.getMethod("run", Plugin.class, Consumer.class)
|
||||
.invoke(globalScheduler, plugin, (Consumer<Object>) (t) -> task.run());
|
||||
} catch (Exception e) {
|
||||
// Fallback to regular scheduler if reflection fails
|
||||
plugin.getLogger().warning("Failed to use Folia GlobalRegionScheduler, falling back to Bukkit scheduler: " + e.getMessage());
|
||||
return Bukkit.getScheduler().runTask(plugin, task);
|
||||
}
|
||||
}
|
||||
return Bukkit.getScheduler().runTask(plugin, task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a task synchronously for a specific player (uses region scheduler on Folia)
|
||||
*/
|
||||
public static BukkitTask runTask(Plugin plugin, Player player, Runnable task) {
|
||||
if (IS_FOLIA) {
|
||||
try {
|
||||
Location loc = player.getLocation();
|
||||
Object server = Bukkit.getServer();
|
||||
Object regionScheduler = server.getClass()
|
||||
.getMethod("getRegionScheduler")
|
||||
.invoke(server);
|
||||
|
||||
// RegionScheduler.run(Plugin, Location, Consumer)
|
||||
return (BukkitTask) regionScheduler.getClass()
|
||||
.getMethod("run", Plugin.class, Location.class, Consumer.class)
|
||||
.invoke(regionScheduler, plugin, loc, (Consumer<Object>) (t) -> task.run());
|
||||
} catch (Exception e) {
|
||||
// Fallback to regular scheduler if reflection fails
|
||||
plugin.getLogger().warning("Failed to use Folia RegionScheduler, falling back to Bukkit scheduler: " + e.getMessage());
|
||||
return Bukkit.getScheduler().runTask(plugin, task);
|
||||
}
|
||||
}
|
||||
return Bukkit.getScheduler().runTask(plugin, task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a task synchronously for a specific location (uses region scheduler on Folia)
|
||||
*/
|
||||
public static BukkitTask runTask(Plugin plugin, Location location, Runnable task) {
|
||||
if (IS_FOLIA) {
|
||||
try {
|
||||
Object server = Bukkit.getServer();
|
||||
Object regionScheduler = server.getClass()
|
||||
.getMethod("getRegionScheduler")
|
||||
.invoke(server);
|
||||
|
||||
// RegionScheduler.run(Plugin, Location, Consumer)
|
||||
return (BukkitTask) regionScheduler.getClass()
|
||||
.getMethod("run", Plugin.class, Location.class, Consumer.class)
|
||||
.invoke(regionScheduler, plugin, location, (Consumer<Object>) (t) -> task.run());
|
||||
} catch (Exception e) {
|
||||
// Fallback to regular scheduler if reflection fails
|
||||
plugin.getLogger().warning("Failed to use Folia RegionScheduler, falling back to Bukkit scheduler: " + e.getMessage());
|
||||
return Bukkit.getScheduler().runTask(plugin, task);
|
||||
}
|
||||
}
|
||||
return Bukkit.getScheduler().runTask(plugin, task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a task asynchronously
|
||||
*/
|
||||
public static BukkitTask runTaskAsync(Plugin plugin, Runnable task) {
|
||||
if (IS_FOLIA) {
|
||||
try {
|
||||
Object server = Bukkit.getServer();
|
||||
Object asyncScheduler = server.getClass()
|
||||
.getMethod("getAsyncScheduler")
|
||||
.invoke(server);
|
||||
|
||||
// AsyncScheduler.runNow(Plugin, Consumer)
|
||||
return (BukkitTask) asyncScheduler.getClass()
|
||||
.getMethod("runNow", Plugin.class, Consumer.class)
|
||||
.invoke(asyncScheduler, plugin, (Consumer<Object>) (t) -> task.run());
|
||||
} catch (Exception e) {
|
||||
// Fallback to regular scheduler if reflection fails
|
||||
plugin.getLogger().warning("Failed to use Folia AsyncScheduler, falling back to Bukkit scheduler: " + e.getMessage());
|
||||
return Bukkit.getScheduler().runTaskAsynchronously(plugin, task);
|
||||
}
|
||||
}
|
||||
return Bukkit.getScheduler().runTaskAsynchronously(plugin, task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a task later synchronously
|
||||
*/
|
||||
public static BukkitTask runTaskLater(Plugin plugin, Runnable task, long delay) {
|
||||
if (IS_FOLIA) {
|
||||
try {
|
||||
Object server = Bukkit.getServer();
|
||||
Object globalScheduler = server.getClass()
|
||||
.getMethod("getGlobalRegionScheduler")
|
||||
.invoke(server);
|
||||
|
||||
// GlobalRegionScheduler.runDelayed(Plugin, Consumer, long)
|
||||
return (BukkitTask) globalScheduler.getClass()
|
||||
.getMethod("runDelayed", Plugin.class, Consumer.class, long.class)
|
||||
.invoke(globalScheduler, plugin, (Consumer<Object>) (t) -> task.run(), delay);
|
||||
} catch (Exception e) {
|
||||
// Fallback to regular scheduler if reflection fails
|
||||
plugin.getLogger().warning("Failed to use Folia GlobalRegionScheduler, falling back to Bukkit scheduler: " + e.getMessage());
|
||||
return Bukkit.getScheduler().runTaskLater(plugin, task, delay);
|
||||
}
|
||||
}
|
||||
return Bukkit.getScheduler().runTaskLater(plugin, task, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a task later synchronously for a specific player
|
||||
*/
|
||||
public static BukkitTask runTaskLater(Plugin plugin, Player player, Runnable task, long delay) {
|
||||
if (IS_FOLIA) {
|
||||
try {
|
||||
Location loc = player.getLocation();
|
||||
Object server = Bukkit.getServer();
|
||||
Object regionScheduler = server.getClass()
|
||||
.getMethod("getRegionScheduler")
|
||||
.invoke(server);
|
||||
|
||||
// RegionScheduler.runDelayed(Plugin, Location, Consumer, long)
|
||||
return (BukkitTask) regionScheduler.getClass()
|
||||
.getMethod("runDelayed", Plugin.class, Location.class, Consumer.class, long.class)
|
||||
.invoke(regionScheduler, plugin, loc, (Consumer<Object>) (t) -> task.run(), delay);
|
||||
} catch (Exception e) {
|
||||
// Fallback to regular scheduler if reflection fails
|
||||
plugin.getLogger().warning("Failed to use Folia RegionScheduler, falling back to Bukkit scheduler: " + e.getMessage());
|
||||
return Bukkit.getScheduler().runTaskLater(plugin, task, delay);
|
||||
}
|
||||
}
|
||||
return Bukkit.getScheduler().runTaskLater(plugin, task, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a task later asynchronously
|
||||
*/
|
||||
public static BukkitTask runTaskLaterAsync(Plugin plugin, Runnable task, long delay) {
|
||||
if (IS_FOLIA) {
|
||||
try {
|
||||
Object server = Bukkit.getServer();
|
||||
Object asyncScheduler = server.getClass()
|
||||
.getMethod("getAsyncScheduler")
|
||||
.invoke(server);
|
||||
|
||||
// Folia uses milliseconds for async scheduler
|
||||
long delayMs = delay * 50; // Convert ticks to milliseconds
|
||||
// AsyncScheduler.runDelayed(Plugin, Consumer, long, TimeUnit)
|
||||
return (BukkitTask) asyncScheduler.getClass()
|
||||
.getMethod("runDelayed", Plugin.class, Consumer.class, long.class, TimeUnit.class)
|
||||
.invoke(asyncScheduler, plugin, (Consumer<Object>) (t) -> task.run(), delayMs, TimeUnit.MILLISECONDS);
|
||||
} catch (Exception e) {
|
||||
// Fallback to regular scheduler if reflection fails
|
||||
plugin.getLogger().warning("Failed to use Folia AsyncScheduler, falling back to Bukkit scheduler: " + e.getMessage());
|
||||
return Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, task, delay);
|
||||
}
|
||||
}
|
||||
return Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, task, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a repeating task synchronously
|
||||
*/
|
||||
public static BukkitTask runTaskTimer(Plugin plugin, Runnable task, long delay, long period) {
|
||||
if (IS_FOLIA) {
|
||||
try {
|
||||
Object server = Bukkit.getServer();
|
||||
Object globalScheduler = server.getClass()
|
||||
.getMethod("getGlobalRegionScheduler")
|
||||
.invoke(server);
|
||||
|
||||
// GlobalRegionScheduler.runAtFixedRate(Plugin, Consumer, long, long)
|
||||
return (BukkitTask) globalScheduler.getClass()
|
||||
.getMethod("runAtFixedRate", Plugin.class, Consumer.class, long.class, long.class)
|
||||
.invoke(globalScheduler, plugin, (Consumer<Object>) (t) -> task.run(), delay, period);
|
||||
} catch (Exception e) {
|
||||
// Fallback to regular scheduler if reflection fails
|
||||
plugin.getLogger().warning("Failed to use Folia GlobalRegionScheduler, falling back to Bukkit scheduler: " + e.getMessage());
|
||||
return Bukkit.getScheduler().runTaskTimer(plugin, task, delay, period);
|
||||
}
|
||||
}
|
||||
return Bukkit.getScheduler().runTaskTimer(plugin, task, delay, period);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a repeating task asynchronously
|
||||
*/
|
||||
public static BukkitTask runTaskTimerAsync(Plugin plugin, Runnable task, long delay, long period) {
|
||||
if (IS_FOLIA) {
|
||||
try {
|
||||
Object server = Bukkit.getServer();
|
||||
Object asyncScheduler = server.getClass()
|
||||
.getMethod("getAsyncScheduler")
|
||||
.invoke(server);
|
||||
|
||||
// Folia uses milliseconds for async scheduler
|
||||
long delayMs = delay * 50; // Convert ticks to milliseconds
|
||||
long periodMs = period * 50; // Convert ticks to milliseconds
|
||||
// AsyncScheduler.runAtFixedRate(Plugin, Consumer, long, long, TimeUnit)
|
||||
return (BukkitTask) asyncScheduler.getClass()
|
||||
.getMethod("runAtFixedRate", Plugin.class, Consumer.class, long.class, long.class, TimeUnit.class)
|
||||
.invoke(asyncScheduler, plugin, (Consumer<Object>) (t) -> task.run(), delayMs, periodMs, TimeUnit.MILLISECONDS);
|
||||
} catch (Exception e) {
|
||||
// Fallback to regular scheduler if reflection fails
|
||||
plugin.getLogger().warning("Failed to use Folia AsyncScheduler, falling back to Bukkit scheduler: " + e.getMessage());
|
||||
return Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, task, delay, period);
|
||||
}
|
||||
}
|
||||
return Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, task, delay, period);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current thread is the main thread (or region thread for Folia)
|
||||
*/
|
||||
public static boolean isPrimaryThread() {
|
||||
if (IS_FOLIA) {
|
||||
try {
|
||||
// On Folia, check if we're on a region thread
|
||||
return Bukkit.isPrimaryThread();
|
||||
} catch (Exception e) {
|
||||
return Bukkit.isPrimaryThread();
|
||||
}
|
||||
}
|
||||
return Bukkit.isPrimaryThread();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a method synchronously (for Folia compatibility)
|
||||
*/
|
||||
public static <T> T callSyncMethod(Plugin plugin, java.util.concurrent.Callable<T> callable) throws Exception {
|
||||
if (IS_FOLIA) {
|
||||
// On Folia, we need to use a different approach
|
||||
// For now, we'll use a CompletableFuture
|
||||
java.util.concurrent.CompletableFuture<T> future = new java.util.concurrent.CompletableFuture<>();
|
||||
runTask(plugin, () -> {
|
||||
try {
|
||||
future.complete(callable.call());
|
||||
} catch (Exception e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return future.get();
|
||||
}
|
||||
return Bukkit.getScheduler().callSyncMethod(plugin, callable).get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.example.playerdatasync.premium.utils;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
/**
|
||||
* Utility class to check Minecraft version compatibility and feature availability
|
||||
*/
|
||||
public class VersionCompatibility {
|
||||
private static String serverVersion;
|
||||
private static int majorVersion;
|
||||
private static int minorVersion;
|
||||
private static int patchVersion;
|
||||
|
||||
static {
|
||||
try {
|
||||
serverVersion = Bukkit.getServer().getBukkitVersion();
|
||||
parseVersion(serverVersion);
|
||||
} catch (Exception e) {
|
||||
serverVersion = "unknown";
|
||||
majorVersion = 0;
|
||||
minorVersion = 0;
|
||||
patchVersion = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseVersion(String version) {
|
||||
try {
|
||||
// Format: "1.8.8-R0.1-SNAPSHOT" or "1.21.1-R0.1-SNAPSHOT"
|
||||
String[] parts = version.split("-")[0].split("\\.");
|
||||
majorVersion = Integer.parseInt(parts[0]);
|
||||
minorVersion = parts.length > 1 ? Integer.parseInt(parts[1]) : 0;
|
||||
patchVersion = parts.length > 2 ? Integer.parseInt(parts[2]) : 0;
|
||||
} catch (Exception e) {
|
||||
majorVersion = 0;
|
||||
minorVersion = 0;
|
||||
patchVersion = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the server version is at least the specified version
|
||||
*/
|
||||
public static boolean isAtLeast(int major, int minor, int patch) {
|
||||
if (majorVersion > major) return true;
|
||||
if (majorVersion < major) return false;
|
||||
if (minorVersion > minor) return true;
|
||||
if (minorVersion < minor) return false;
|
||||
return patchVersion >= patch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the server version is between two versions (inclusive)
|
||||
*/
|
||||
public static boolean isBetween(int majorMin, int minorMin, int patchMin,
|
||||
int majorMax, int minorMax, int patchMax) {
|
||||
return isAtLeast(majorMin, minorMin, patchMin) &&
|
||||
!isAtLeast(majorMax, minorMax, patchMax + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the server version string
|
||||
*/
|
||||
public static String getServerVersion() {
|
||||
return serverVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if offhand is supported (1.9+)
|
||||
*/
|
||||
public static boolean isOffhandSupported() {
|
||||
return isAtLeast(1, 9, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if attributes are supported (1.9+)
|
||||
*/
|
||||
public static boolean isAttributesSupported() {
|
||||
return isAtLeast(1, 9, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if advancements are supported (1.12+)
|
||||
*/
|
||||
public static boolean isAdvancementsSupported() {
|
||||
return isAtLeast(1, 12, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if NamespacedKey is supported (1.13+)
|
||||
*/
|
||||
public static boolean isNamespacedKeySupported() {
|
||||
return isAtLeast(1, 13, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the version is 1.8
|
||||
*/
|
||||
public static boolean isVersion1_8() {
|
||||
return majorVersion == 1 && minorVersion == 8;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the version is 1.9-1.11
|
||||
*/
|
||||
public static boolean isVersion1_9_to_1_11() {
|
||||
return majorVersion == 1 && minorVersion >= 9 && minorVersion <= 11;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the version is 1.12
|
||||
*/
|
||||
public static boolean isVersion1_12() {
|
||||
return majorVersion == 1 && minorVersion == 12;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the version is 1.13-1.16
|
||||
*/
|
||||
public static boolean isVersion1_13_to_1_16() {
|
||||
return majorVersion == 1 && minorVersion >= 13 && minorVersion <= 16;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the version is 1.17
|
||||
*/
|
||||
public static boolean isVersion1_17() {
|
||||
return majorVersion == 1 && minorVersion == 17;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the version is 1.18-1.20
|
||||
*/
|
||||
public static boolean isVersion1_18_to_1_20() {
|
||||
return majorVersion == 1 && minorVersion >= 18 && minorVersion <= 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the version is 1.21+
|
||||
*/
|
||||
public static boolean isVersion1_21_Plus() {
|
||||
return majorVersion == 1 && minorVersion >= 21;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable version string
|
||||
*/
|
||||
public static String getVersionString() {
|
||||
return majorVersion + "." + minorVersion + "." + patchVersion;
|
||||
}
|
||||
}
|
||||
|
||||
175
PlayerDataSync-Premium/premium/src/main/resources/config.yml
Normal file
175
PlayerDataSync-Premium/premium/src/main/resources/config.yml
Normal file
@@ -0,0 +1,175 @@
|
||||
# =====================================
|
||||
# PlayerDataSync Premium Configuration
|
||||
# Compatible with Minecraft 1.8 - 1.21.11
|
||||
# =====================================
|
||||
|
||||
# =====================================
|
||||
# LICENSE CONFIGURATION (REQUIRED)
|
||||
# =====================================
|
||||
# IMPORTANT: You must enter your license key to use PlayerDataSync Premium
|
||||
# Get your license key from: https://craftingstudiopro.de
|
||||
license:
|
||||
key: YOUR-LICENSE-KEY-HERE # Enter your license key from CraftingStudio Pro
|
||||
# The license key will be validated against the CraftingStudio Pro API
|
||||
# API Documentation: https://www.craftingstudiopro.de/docs/api
|
||||
|
||||
# =====================================
|
||||
# UPDATE CHECKER CONFIGURATION
|
||||
# =====================================
|
||||
update_checker:
|
||||
enabled: true # Enable automatic update checking
|
||||
notify_ops: true # Notify operators when updates are available
|
||||
auto_download: false # Automatically download updates (not implemented yet)
|
||||
timeout: 10000 # Timeout in milliseconds for update check requests
|
||||
|
||||
# =====================================
|
||||
# PREMIUM FEATURES CONFIGURATION
|
||||
# =====================================
|
||||
premium:
|
||||
# License revalidation interval (in hours)
|
||||
# License will be revalidated periodically to ensure it's still valid
|
||||
revalidation_interval_hours: 24
|
||||
|
||||
# Cache license validation results to reduce API calls
|
||||
# License validation is cached for 30 minutes by default
|
||||
cache_validation: true
|
||||
|
||||
# Enable premium-specific features
|
||||
enable_premium_features: true
|
||||
|
||||
# =====================================
|
||||
# SERVER CONFIGURATION
|
||||
# =====================================
|
||||
server:
|
||||
id: default # Unique identifier for this server instance
|
||||
|
||||
# =====================================
|
||||
# DATABASE CONFIGURATION
|
||||
# =====================================
|
||||
database:
|
||||
type: mysql # Available options: mysql, sqlite
|
||||
table_prefix: player_data_premium # Prefix used for all plugin tables
|
||||
|
||||
# MySQL Database Configuration
|
||||
mysql:
|
||||
host: localhost
|
||||
port: 3306
|
||||
database: minecraft
|
||||
user: root
|
||||
password: password
|
||||
ssl: false
|
||||
connection_timeout: 5000 # milliseconds
|
||||
max_connections: 10
|
||||
|
||||
# SQLite Database Configuration
|
||||
sqlite:
|
||||
file: plugins/PlayerDataSync-Premium/playerdata.db
|
||||
|
||||
# =====================================
|
||||
# PLAYER DATA SYNCHRONIZATION SETTINGS
|
||||
# =====================================
|
||||
sync:
|
||||
# Basic Player Data
|
||||
coordinates: true # Player's current coordinates
|
||||
position: true # Player's position (world, x, y, z, yaw, pitch)
|
||||
xp: true # Experience points and levels
|
||||
gamemode: true # Current gamemode
|
||||
|
||||
# Inventory and Storage
|
||||
inventory: true # Main inventory contents
|
||||
enderchest: true # Ender chest contents
|
||||
armor: true # Equipped armor pieces
|
||||
offhand: true # Offhand item
|
||||
|
||||
# Player Status
|
||||
health: true # Current health
|
||||
hunger: true # Hunger and saturation
|
||||
effects: true # Active potion effects
|
||||
|
||||
# Progress and Achievements
|
||||
achievements: true # Player advancements/achievements (WARNING: May cause lag with 1000+ achievements)
|
||||
statistics: true # Player statistics (blocks broken, distance traveled, etc.)
|
||||
|
||||
# Advanced Features
|
||||
attributes: true # Player attributes (max health, speed, etc.)
|
||||
permissions: false # Sync permissions (requires LuckPerms integration)
|
||||
economy: true # Sync economy balance (requires Vault)
|
||||
|
||||
# =====================================
|
||||
# AUTOMATIC SAVE CONFIGURATION
|
||||
# =====================================
|
||||
autosave:
|
||||
enabled: true
|
||||
interval: 1 # seconds between automatic saves, 0 to disable
|
||||
on_world_change: true # save when player changes world
|
||||
on_death: true # save when player dies
|
||||
on_server_switch: true # save when player switches servers (BungeeCord/Velocity)
|
||||
on_kick: true # save when player is kicked (might be server switch)
|
||||
async: true # perform saves asynchronously
|
||||
|
||||
# =====================================
|
||||
# INTEGRATION SETTINGS
|
||||
# =====================================
|
||||
integrations:
|
||||
bungeecord: false # enable BungeeCord/Velocity support
|
||||
invsee: true # enable InvSee integration
|
||||
openinv: true # enable OpenInv integration
|
||||
|
||||
# =====================================
|
||||
# LOGGING CONFIGURATION
|
||||
# =====================================
|
||||
logging:
|
||||
level: INFO # Logging level: ALL, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST
|
||||
log_database: false # Log database queries (for debugging)
|
||||
log_performance: false # Log performance metrics
|
||||
debug_mode: false # Enable debug mode (more verbose logging)
|
||||
|
||||
# =====================================
|
||||
# METRICS CONFIGURATION
|
||||
# =====================================
|
||||
metrics:
|
||||
bstats: true # Enable bStats metrics
|
||||
custom_metrics: true # Enable custom metrics
|
||||
|
||||
# =====================================
|
||||
# MESSAGES CONFIGURATION
|
||||
# =====================================
|
||||
messages:
|
||||
enabled: true
|
||||
language: en # Available: en, de
|
||||
prefix: "&8[&6PDS Premium&8]"
|
||||
colors: true
|
||||
|
||||
# =====================================
|
||||
# PERFORMANCE CONFIGURATION
|
||||
# =====================================
|
||||
performance:
|
||||
batch_size: 50
|
||||
cache_size: 100
|
||||
cache_ttl: 300000
|
||||
cache_compression: true
|
||||
connection_pooling: true
|
||||
async_loading: true
|
||||
disable_achievement_sync_on_large_amounts: true
|
||||
achievement_batch_size: 50
|
||||
achievement_timeout_ms: 5000
|
||||
max_achievements_per_player: 2000
|
||||
|
||||
# =====================================
|
||||
# COMPATIBILITY CONFIGURATION
|
||||
# =====================================
|
||||
compatibility:
|
||||
safe_attribute_sync: true
|
||||
disable_attributes_on_error: false
|
||||
version_check: true
|
||||
legacy_1_20_support: true
|
||||
modern_1_21_support: true
|
||||
disable_achievements_on_critical_error: true
|
||||
|
||||
# =====================================
|
||||
# SECURITY CONFIGURATION
|
||||
# =====================================
|
||||
security:
|
||||
encrypt_data: false
|
||||
hash_uuids: false
|
||||
audit_log: true
|
||||
@@ -0,0 +1,182 @@
|
||||
# =====================================
|
||||
# PlayerDataSync Deutsche Nachrichten
|
||||
# =====================================
|
||||
|
||||
# Allgemein
|
||||
prefix: "&8[&bPlayerDataSync&8]"
|
||||
no_permission: "&cDu hast keine Berechtigung für diesen Befehl."
|
||||
player_not_found: "&cSpieler '{player}' nicht gefunden."
|
||||
invalid_syntax: "&cUngültige Syntax. Verwende: {usage}"
|
||||
feature_disabled: "&cDiese Funktion ist derzeit deaktiviert."
|
||||
|
||||
# Datensynchronisation Nachrichten
|
||||
loading: "&aDaten werden synchronisiert..."
|
||||
loaded: "&aSpielerdaten erfolgreich geladen."
|
||||
load_failed: "&cFehler beim Laden der Spielerdaten."
|
||||
saving: "&eSpeichere Spielerdaten..."
|
||||
sync_complete: "&aDatensynchronisation erfolgreich abgeschlossen."
|
||||
sync_failed: "&cFehler bei der Datensynchronisation: {error}"
|
||||
manual_save_success: "&aSpielerdaten erfolgreich gespeichert."
|
||||
manual_save_failed: "&cFehler beim Speichern der Spielerdaten: {error}"
|
||||
|
||||
# Konfigurationsnachrichten
|
||||
reloaded: "&aKonfiguration erfolgreich neu geladen."
|
||||
reload_failed: "&cFehler beim Neuladen der Konfiguration: {error}"
|
||||
config_updated: "&aKonfigurationssetting '{setting}' auf '{value}' aktualisiert."
|
||||
|
||||
# Sync-Option Nachrichten
|
||||
sync_enabled: "&aSync für '{option}' wurde aktiviert."
|
||||
sync_disabled: "&cSync für '{option}' wurde deaktiviert."
|
||||
sync_status: "&7{option}: {status}"
|
||||
sync_status_enabled: "&aAktiviert"
|
||||
sync_status_disabled: "&cDeaktiviert"
|
||||
|
||||
# Status-Nachrichten
|
||||
status_header: "&8&m----------&r &bPlayerDataSync Status &8&m----------"
|
||||
status_footer: "&8&m----------------------------------------"
|
||||
status_version: "&7Version: &f{version}"
|
||||
status_database: "&7Datenbank: &f{type} &8(&7{status}&8)"
|
||||
status_connected_players: "&7Verbundene Spieler: &f{count}"
|
||||
status_total_records: "&7Gesamte Datensätze: &f{count}"
|
||||
status_last_backup: "&7Letztes Backup: &f{time}"
|
||||
status_autosave: "&7Autosave: &f{status} &8(&7{interval}m&8)"
|
||||
|
||||
# Datenbank-Nachrichten
|
||||
database_connected: "&aErfolgreich mit {type} Datenbank verbunden."
|
||||
database_disconnected: "&eVon Datenbank getrennt."
|
||||
database_error: "&cDatenbankfehler: {error}"
|
||||
database_reconnected: "&aMit Datenbank wieder verbunden."
|
||||
database_migration: "&eDatenbankschema wird aktualisiert..."
|
||||
database_migration_complete: "&aDatenbankmigration abgeschlossen."
|
||||
|
||||
# Backup-Nachrichten
|
||||
backup_created: "&aBackup erfolgreich erstellt: {id}"
|
||||
backup_restored: "&aDaten aus Backup wiederhergestellt: {id}"
|
||||
backup_failed: "&cBackup-Operation fehlgeschlagen: {error}"
|
||||
backup_not_found: "&cBackup '{id}' nicht gefunden."
|
||||
backup_list_header: "&8&m----------&r &bBackups für {player} &8&m----------"
|
||||
backup_list_entry: "&7{id} &8- &f{date} &8(&7{size}&8)"
|
||||
backup_list_empty: "&eKeine Backups für {player} gefunden."
|
||||
|
||||
# Import/Export Nachrichten
|
||||
import_started: "&aImport aus {format} gestartet..."
|
||||
import_complete: "&aImport erfolgreich abgeschlossen. {count} Datensätze verarbeitet."
|
||||
import_failed: "&cImport fehlgeschlagen: {error}"
|
||||
export_started: "&aExport nach {format} gestartet..."
|
||||
export_complete: "&aExport erfolgreich abgeschlossen. Datei: {file}"
|
||||
export_failed: "&cExport fehlgeschlagen: {error}"
|
||||
|
||||
# Fehlermeldungen
|
||||
error_player_offline: "&cSpieler muss online sein für diese Operation."
|
||||
error_no_data: "&cKeine Daten für Spieler '{player}' gefunden."
|
||||
error_invalid_backup_id: "&cUngültiges Backup-ID Format."
|
||||
error_permission_denied: "&cZugriff verweigert."
|
||||
error_command_disabled: "&cDieser Befehl ist derzeit deaktiviert."
|
||||
error_database_offline: "&cDatenbank ist derzeit offline."
|
||||
error_validation_failed: "&cDatenvalidierung fehlgeschlagen: {reason}"
|
||||
|
||||
# Performance-Nachrichten
|
||||
performance_warning: "&eWarnung: Hohe Datenbanklatenz erkannt ({ms}ms)"
|
||||
performance_lag: "&cDatenbankoperationen verursachen Server-Lag. Optimierung erforderlich."
|
||||
cache_cleared: "&aSpielerdaten-Cache geleert."
|
||||
cache_stats: "&7Cache-Statistiken: {hits} Treffer, {misses} Fehler, {size} Einträge"
|
||||
|
||||
# Sicherheitsnachrichten
|
||||
security_audit_log: "&7[AUDIT] {action} von {player}: {details}"
|
||||
security_encryption_enabled: "&aDatenverschlüsselung ist aktiviert."
|
||||
security_encryption_disabled: "&eDatenverschlüsselung ist deaktiviert."
|
||||
|
||||
# Integrationsnachrichten
|
||||
integration_vault_enabled: "&aVault-Integration aktiviert."
|
||||
integration_vault_disabled: "&eVault-Integration deaktiviert."
|
||||
integration_luckperms_enabled: "&aLuckPerms-Integration aktiviert."
|
||||
integration_luckperms_disabled: "&eLuckPerms-Integration deaktiviert."
|
||||
integration_bungeecord_enabled: "&aBungeeCord-Modus aktiviert."
|
||||
integration_placeholderapi_enabled: "&aPlaceholderAPI-Integration aktiviert."
|
||||
|
||||
inventory_view_usage_inventory: "&cVerwendung: /invsee <Spieler>"
|
||||
inventory_view_usage_ender: "&cVerwendung: /endersee <Spieler>"
|
||||
inventory_view_loading: "&7Lade gespeicherte Daten für &b{player}&7..."
|
||||
inventory_view_no_data: "&eKeine gespeicherten Daten für &b{player}&e gefunden. Es wird ein leeres Inventar angezeigt."
|
||||
inventory_view_open_failed: "&cGespeicherte Daten für &b{player}&c konnten nicht geladen werden."
|
||||
inventory_view_save_failed: "&cÄnderungen für &b{player}&c konnten nicht gespeichert werden. Siehe Konsole für Details."
|
||||
inventory_view_title_inventory: "&8Inventar » &b{player}"
|
||||
inventory_view_title_ender: "&8Endertruhe » &b{player}"
|
||||
|
||||
# Editor-Integrationsnachrichten
|
||||
editor_disabled: "&cDie Editor-Integration ist nicht konfiguriert. Setze PDS_EDITOR_API_KEY oder die System-Property pds.editor.apiKey."
|
||||
editor_player_required: "&cBitte gib einen Spieler an, wenn du den Befehl von der Konsole nutzt."
|
||||
editor_token_generating: "&7Fordere Editor-Zugang für &b{player}&7 an..."
|
||||
editor_token_success: "&aEditor-Link:&r {url}"
|
||||
editor_token_value: "&7Token:&r {token} &8({expires})"
|
||||
editor_token_expires: "&7läuft in {seconds}s ab"
|
||||
editor_token_expires_unknown: "&7Ablauf unbekannt"
|
||||
editor_token_failed: "&cEditor-Token konnte nicht erstellt werden: {error}"
|
||||
editor_snapshot_start: "&7Übertrage Server-Snapshot..."
|
||||
editor_snapshot_success: "&aServer-Snapshot erfolgreich gesendet."
|
||||
editor_snapshot_failed: "&cServer-Snapshot konnte nicht gesendet werden: {error}"
|
||||
editor_heartbeat_usage: "&cVerwendung: /sync editor heartbeat <online|offline>"
|
||||
editor_heartbeat_success: "&aHeartbeat gesendet. Server ist nun als {status} markiert."
|
||||
editor_heartbeat_failed: "&cHeartbeat konnte nicht gesendet werden: {error}"
|
||||
editor_usage: "&cVerwendung: /sync editor <token|snapshot|heartbeat>"
|
||||
editor_status_online: "&aonline"
|
||||
editor_status_offline: "&coffline"
|
||||
|
||||
# Update-Nachrichten
|
||||
update_available: "&aEine neue Version ist verfügbar: {version}"
|
||||
update_current: "&aDu verwendest die neueste Version."
|
||||
update_check_failed: "&cFehler bei der Update-Prüfung: {error}"
|
||||
update_check_disabled: "&eUpdate-Prüfung ist deaktiviert."
|
||||
update_check_timeout: "&eUpdate-Prüfung ist abgelaufen."
|
||||
update_check_no_internet: "&eKeine Internetverbindung für Update-Prüfung."
|
||||
update_download_url: "&7Download unter: {url}"
|
||||
|
||||
# Server-Wechsel Nachrichten
|
||||
server_switch_save: "&eSpeichere Daten vor Server-Wechsel..."
|
||||
server_switch_saved: "&aDaten vor Server-Wechsel gespeichert."
|
||||
server_switch_load: "&aLade Daten nach Server-Wechsel..."
|
||||
server_switch_loaded: "&aDaten nach Server-Wechsel geladen."
|
||||
|
||||
# Hilfe-Nachrichten
|
||||
help_header: "&8&m----------&r &bPlayerDataSync Hilfe &8&m----------"
|
||||
help_footer: "&8&m----------------------------------------"
|
||||
help_sync: "&b/sync &8- &7Sync-Optionen anzeigen oder ändern"
|
||||
help_sync_option: "&b/sync <option> <true|false> &8- &7Sync-Option umschalten"
|
||||
help_sync_reload: "&b/sync reload &8- &7Konfiguration neu laden"
|
||||
help_sync_save: "&b/sync save [spieler] &8- &7Spielerdaten manuell speichern"
|
||||
help_status: "&b/pdsstatus [spieler] &8- &7Sync-Status prüfen"
|
||||
help_backup: "&b/pdsbackup <aktion> &8- &7Backups verwalten"
|
||||
help_import: "&b/pdsimport <format> &8- &7Spielerdaten importieren"
|
||||
help_export: "&b/pdsexport <format> &8- &7Spielerdaten exportieren"
|
||||
|
||||
# Befehlsergänzung
|
||||
tab_complete_options: ["koordinaten", "position", "xp", "spielmodus", "inventar", "enderchest", "rüstung", "offhand", "gesundheit", "hunger", "effekte", "erfolge", "statistiken", "attribute", "berechtigungen", "wirtschaft"]
|
||||
tab_complete_boolean: ["true", "false", "wahr", "falsch"]
|
||||
tab_complete_backup_actions: ["erstellen", "wiederherstellen", "liste", "löschen"]
|
||||
tab_complete_formats: ["json", "yaml", "csv", "sql"]
|
||||
|
||||
# Validierungsnachrichten
|
||||
validation_invalid_world: "&cWelt '{world}' existiert nicht."
|
||||
validation_invalid_gamemode: "&cUngültiger Spielmodus: {gamemode}"
|
||||
validation_invalid_coordinates: "&cUngültige Koordinaten: {coordinates}"
|
||||
validation_data_corrupted: "&cSpielerdaten scheinen beschädigt zu sein."
|
||||
validation_version_mismatch: "&eDatensatz-Versionsmismatch. Versuche Migration..."
|
||||
|
||||
# Migrationsnachrichten
|
||||
migration_started: "&eDateenmigration gestartet..."
|
||||
migration_progress: "&7Migrationsfortschritt: {current}/{total} ({percent}%)"
|
||||
migration_complete: "&aMigration erfolgreich abgeschlossen."
|
||||
migration_failed: "&cMigration fehlgeschlagen: {error}"
|
||||
|
||||
# Aufräumnachrichten
|
||||
cleanup_started: "&eAufräumen inaktiver Spielerdaten gestartet..."
|
||||
cleanup_complete: "&aAufräumen abgeschlossen. {count} inaktive Datensätze entfernt."
|
||||
cleanup_failed: "&cAufräumen fehlgeschlagen: {error}"
|
||||
|
||||
# Statistik-Nachrichten
|
||||
stats_blocks_broken: "&7Blöcke abgebaut: &f{count}"
|
||||
stats_blocks_placed: "&7Blöcke platziert: &f{count}"
|
||||
stats_distance_traveled: "&7Zurückgelegte Strecke: &f{distance}m"
|
||||
stats_time_played: "&7Spielzeit: &f{time}"
|
||||
stats_deaths: "&7Tode: &f{count}"
|
||||
stats_kills: "&7Kills: &f{count}"
|
||||
@@ -0,0 +1,181 @@
|
||||
# =====================================
|
||||
# PlayerDataSync English Messages
|
||||
# =====================================
|
||||
|
||||
# General
|
||||
prefix: "&8[&bPlayerDataSync&8]"
|
||||
no_permission: "&cYou don't have permission to execute this command."
|
||||
player_not_found: "&cPlayer '{player}' not found."
|
||||
invalid_syntax: "&cInvalid syntax. Use: {usage}"
|
||||
feature_disabled: "&cThis feature is currently disabled."
|
||||
|
||||
# Data Synchronization Messages
|
||||
loading: "&aData is being synchronized..."
|
||||
loaded: "&aPlayer data loaded successfully."
|
||||
load_failed: "&cFailed to load player data."
|
||||
saving: "&eSaving player data..."
|
||||
sync_complete: "&aData synchronization completed successfully."
|
||||
sync_failed: "&cFailed to synchronize data: {error}"
|
||||
manual_save_success: "&aPlayer data saved successfully."
|
||||
manual_save_failed: "&cFailed to save player data: {error}"
|
||||
|
||||
# Configuration Messages
|
||||
reloaded: "&aConfiguration reloaded successfully."
|
||||
reload_failed: "&cFailed to reload configuration: {error}"
|
||||
config_updated: "&aConfiguration setting '{setting}' updated to '{value}'."
|
||||
|
||||
# Sync Option Messages
|
||||
sync_enabled: "&aSync for '{option}' has been enabled."
|
||||
sync_disabled: "&cSync for '{option}' has been disabled."
|
||||
sync_status: "&7{option}: {status}"
|
||||
sync_status_enabled: "&aEnabled"
|
||||
sync_status_disabled: "&cDisabled"
|
||||
|
||||
# Status Messages
|
||||
status_header: "&8&m----------&r &bPlayerDataSync Status &8&m----------"
|
||||
status_footer: "&8&m----------------------------------------"
|
||||
status_version: "&7Version: &f{version}"
|
||||
status_database: "&7Database: &f{type} &8(&7{status}&8)"
|
||||
status_connected_players: "&7Connected Players: &f{count}"
|
||||
status_total_records: "&7Total Records: &f{count}"
|
||||
status_last_backup: "&7Last Backup: &f{time}"
|
||||
status_autosave: "&7Autosave: &f{status} &8(&7{interval}m&8)"
|
||||
|
||||
# Database Messages
|
||||
database_connected: "&aSuccessfully connected to {type} database."
|
||||
database_disconnected: "&eDisconnected from database."
|
||||
database_error: "&cDatabase error: {error}"
|
||||
database_reconnected: "&aReconnected to database."
|
||||
database_migration: "&eUpdating database schema..."
|
||||
database_migration_complete: "&aDatabase migration completed."
|
||||
|
||||
# Backup Messages
|
||||
backup_created: "&aBackup created successfully: {id}"
|
||||
backup_restored: "&aData restored from backup: {id}"
|
||||
backup_failed: "&cBackup operation failed: {error}"
|
||||
backup_not_found: "&cBackup '{id}' not found."
|
||||
backup_list_header: "&8&m----------&r &bBackups for {player} &8&m----------"
|
||||
backup_list_entry: "&7{id} &8- &f{date} &8(&7{size}&8)"
|
||||
backup_list_empty: "&eNo backups found for {player}."
|
||||
|
||||
# Import/Export Messages
|
||||
import_started: "&aStarting import from {format}..."
|
||||
import_complete: "&aImport completed successfully. {count} records processed."
|
||||
import_failed: "&cImport failed: {error}"
|
||||
export_started: "&aStarting export to {format}..."
|
||||
export_complete: "&aExport completed successfully. File: {file}"
|
||||
export_failed: "&cExport failed: {error}"
|
||||
|
||||
# Error Messages
|
||||
error_player_offline: "&cPlayer must be online for this operation."
|
||||
error_no_data: "&cNo data found for player '{player}'."
|
||||
error_invalid_backup_id: "&cInvalid backup ID format."
|
||||
error_permission_denied: "&cPermission denied."
|
||||
error_command_disabled: "&cThis command is currently disabled."
|
||||
error_database_offline: "&cDatabase is currently offline."
|
||||
error_validation_failed: "&cData validation failed: {reason}"
|
||||
|
||||
# Performance Messages
|
||||
performance_warning: "&eWarning: High database latency detected ({ms}ms)"
|
||||
performance_lag: "&cDatabase operations are causing server lag. Consider optimizing."
|
||||
cache_cleared: "&aPlayer data cache cleared."
|
||||
cache_stats: "&7Cache Statistics: {hits} hits, {misses} misses, {size} entries"
|
||||
|
||||
# Security Messages
|
||||
security_audit_log: "&7[AUDIT] {action} by {player}: {details}"
|
||||
security_encryption_enabled: "&aData encryption is enabled."
|
||||
security_encryption_disabled: "&eData encryption is disabled."
|
||||
|
||||
# Integration Messages
|
||||
integration_placeholderapi_enabled: "&aPlaceholderAPI integration enabled."
|
||||
integration_vault_enabled: "&aVault integration enabled."
|
||||
integration_vault_disabled: "&eVault integration disabled."
|
||||
integration_luckperms_enabled: "&aLuckPerms integration enabled."
|
||||
integration_luckperms_disabled: "&eLuckPerms integration disabled."
|
||||
integration_bungeecord_enabled: "&aBungeeCord mode enabled."
|
||||
inventory_view_usage_inventory: "&cUsage: /invsee <player>"
|
||||
inventory_view_usage_ender: "&cUsage: /endersee <player>"
|
||||
inventory_view_loading: "&7Loading stored data for &b{player}&7..."
|
||||
inventory_view_no_data: "&eNo stored data found for &b{player}&e. Showing empty inventory."
|
||||
inventory_view_open_failed: "&cUnable to load stored data for &b{player}&c."
|
||||
inventory_view_save_failed: "&cFailed to save changes for &b{player}&c. Check console for details."
|
||||
inventory_view_title_inventory: "&8Inventory » &b{player}"
|
||||
inventory_view_title_ender: "&8Ender Chest » &b{player}"
|
||||
|
||||
# Editor Integration Messages
|
||||
editor_disabled: "&cThe web editor integration is not configured. Set PDS_EDITOR_API_KEY or the system property pds.editor.apiKey."
|
||||
editor_player_required: "&cYou must provide a player when running this command from console."
|
||||
editor_token_generating: "&7Requesting editor access for &b{player}&7..."
|
||||
editor_token_success: "&aEditor link:&r {url}"
|
||||
editor_token_value: "&7Token:&r {token} &8({expires})"
|
||||
editor_token_expires: "&7expires in {seconds}s"
|
||||
editor_token_expires_unknown: "&7expiration unknown"
|
||||
editor_token_failed: "&cFailed to generate editor token: {error}"
|
||||
editor_snapshot_start: "&7Uploading server snapshot..."
|
||||
editor_snapshot_success: "&aServer snapshot sent successfully."
|
||||
editor_snapshot_failed: "&cFailed to send server snapshot: {error}"
|
||||
editor_heartbeat_usage: "&cUsage: /sync editor heartbeat <online|offline>"
|
||||
editor_heartbeat_success: "&aHeartbeat sent. Server marked as {status}."
|
||||
editor_heartbeat_failed: "&cFailed to send heartbeat: {error}"
|
||||
editor_usage: "&cUsage: /sync editor <token|snapshot|heartbeat>"
|
||||
editor_status_online: "&aonline"
|
||||
editor_status_offline: "&coffline"
|
||||
|
||||
# Update Messages
|
||||
update_available: "&aA new version is available: {version}"
|
||||
update_current: "&aYou are running the latest version."
|
||||
update_check_failed: "&cFailed to check for updates: {error}"
|
||||
update_check_disabled: "&eUpdate checking is disabled."
|
||||
update_check_timeout: "&eUpdate check timed out."
|
||||
update_check_no_internet: "&eNo internet connection for update check."
|
||||
update_download_url: "&7Download at: {url}"
|
||||
|
||||
# Server Switch Messages
|
||||
server_switch_save: "&eSaving data before server switch..."
|
||||
server_switch_saved: "&aData saved before server switch."
|
||||
server_switch_load: "&aLoading data after server switch..."
|
||||
server_switch_loaded: "&aData loaded after server switch."
|
||||
|
||||
# Help Messages
|
||||
help_header: "&8&m----------&r &bPlayerDataSync Help &8&m----------"
|
||||
help_footer: "&8&m----------------------------------------"
|
||||
help_sync: "&b/sync &8- &7View or change sync options"
|
||||
help_sync_option: "&b/sync <option> <true|false> &8- &7Toggle sync option"
|
||||
help_sync_reload: "&b/sync reload &8- &7Reload configuration"
|
||||
help_sync_save: "&b/sync save [player] &8- &7Manually save player data"
|
||||
help_status: "&b/pdsstatus [player] &8- &7Check sync status"
|
||||
help_backup: "&b/pdsbackup <action> &8- &7Manage backups"
|
||||
help_import: "&b/pdsimport <format> &8- &7Import player data"
|
||||
help_export: "&b/pdsexport <format> &8- &7Export player data"
|
||||
|
||||
# Command Completion Messages
|
||||
tab_complete_options: ["coordinates", "position", "xp", "gamemode", "inventory", "enderchest", "armor", "offhand", "health", "hunger", "effects", "achievements", "statistics", "attributes", "permissions", "economy"]
|
||||
tab_complete_boolean: ["true", "false"]
|
||||
tab_complete_backup_actions: ["create", "restore", "list", "delete"]
|
||||
tab_complete_formats: ["json", "yaml", "csv", "sql"]
|
||||
|
||||
# Validation Messages
|
||||
validation_invalid_world: "&cWorld '{world}' does not exist."
|
||||
validation_invalid_gamemode: "&cInvalid gamemode: {gamemode}"
|
||||
validation_invalid_coordinates: "&cInvalid coordinates: {coordinates}"
|
||||
validation_data_corrupted: "&cPlayer data appears to be corrupted."
|
||||
validation_version_mismatch: "&eData version mismatch. Attempting migration..."
|
||||
|
||||
# Migration Messages
|
||||
migration_started: "&eStarting data migration..."
|
||||
migration_progress: "&7Migration progress: {current}/{total} ({percent}%)"
|
||||
migration_complete: "&aMigration completed successfully."
|
||||
migration_failed: "&cMigration failed: {error}"
|
||||
|
||||
# Cleanup Messages
|
||||
cleanup_started: "&eStarting cleanup of inactive player data..."
|
||||
cleanup_complete: "&aCleanup completed. Removed {count} inactive records."
|
||||
cleanup_failed: "&cCleanup failed: {error}"
|
||||
|
||||
# Statistics Messages
|
||||
stats_blocks_broken: "&7Blocks Broken: &f{count}"
|
||||
stats_blocks_placed: "&7Blocks Placed: &f{count}"
|
||||
stats_distance_traveled: "&7Distance Traveled: &f{distance}m"
|
||||
stats_time_played: "&7Time Played: &f{time}"
|
||||
stats_deaths: "&7Deaths: &f{count}"
|
||||
stats_kills: "&7Kills: &f{count}"
|
||||
201
PlayerDataSync-Premium/premium/src/main/resources/plugin.yml
Normal file
201
PlayerDataSync-Premium/premium/src/main/resources/plugin.yml
Normal file
@@ -0,0 +1,201 @@
|
||||
name: PlayerDataSync-Premium
|
||||
main: com.example.playerdatasync.premium.core.PlayerDataSyncPremium
|
||||
version: ${project.version}
|
||||
api-version: "1.13"
|
||||
load: STARTUP
|
||||
authors: [DerGamer09]
|
||||
description: Premium version of PlayerDataSync with license validation and enhanced features
|
||||
website: https://craftingstudiopro.de/plugins/playerdatasync-premium
|
||||
softdepend: [Vault, LuckPerms, PlaceholderAPI]
|
||||
|
||||
commands:
|
||||
sync:
|
||||
description: View or change sync options
|
||||
usage: /<command> [<option> <true|false>] | /<command> reload | /<command> status | /<command> save [player] | /<command> license [validate|info] | /<command> update [check]
|
||||
permission: playerdatasync.premium.admin
|
||||
aliases: [pds, playerdatasync, pdspremium]
|
||||
|
||||
pdsstatus:
|
||||
description: Check PlayerDataSync Premium status and statistics
|
||||
usage: /<command> [player]
|
||||
permission: playerdatasync.premium.status
|
||||
aliases: [pdsstats]
|
||||
|
||||
pdsbackup:
|
||||
description: Manage player data backups
|
||||
usage: /<command> create | /<command> restore <player> [backup_id] | /<command> list [player]
|
||||
permission: playerdatasync.premium.backup
|
||||
|
||||
pdsimport:
|
||||
description: Import player data from other plugins or formats
|
||||
usage: /<command> <format> [options]
|
||||
permission: playerdatasync.premium.import
|
||||
|
||||
pdsexport:
|
||||
description: Export player data to various formats
|
||||
usage: /<command> <format> [player] [options]
|
||||
permission: playerdatasync.premium.export
|
||||
|
||||
permissions:
|
||||
# Premium Administrative permissions
|
||||
playerdatasync.premium.admin.*:
|
||||
description: Allows all PlayerDataSync Premium admin commands and features
|
||||
default: op
|
||||
children:
|
||||
playerdatasync.premium.admin.coordinates: true
|
||||
playerdatasync.premium.admin.position: true
|
||||
playerdatasync.premium.admin.xp: true
|
||||
playerdatasync.premium.admin.gamemode: true
|
||||
playerdatasync.premium.admin.enderchest: true
|
||||
playerdatasync.premium.admin.inventory: true
|
||||
playerdatasync.premium.admin.armor: true
|
||||
playerdatasync.premium.admin.offhand: true
|
||||
playerdatasync.premium.admin.health: true
|
||||
playerdatasync.premium.admin.hunger: true
|
||||
playerdatasync.premium.admin.effects: true
|
||||
playerdatasync.premium.admin.achievements: true
|
||||
playerdatasync.premium.admin.statistics: true
|
||||
playerdatasync.premium.admin.attributes: true
|
||||
playerdatasync.premium.admin.permissions: true
|
||||
playerdatasync.premium.admin.economy: true
|
||||
playerdatasync.premium.admin.reload: true
|
||||
playerdatasync.premium.admin.save: true
|
||||
playerdatasync.premium.admin.license: true
|
||||
playerdatasync.premium.admin.update: true
|
||||
playerdatasync.premium.integration.invsee: true
|
||||
playerdatasync.premium.integration.enderchest: true
|
||||
playerdatasync.premium.backup: true
|
||||
playerdatasync.premium.import: true
|
||||
playerdatasync.premium.export: true
|
||||
playerdatasync.premium.status: true
|
||||
|
||||
# Individual sync feature permissions
|
||||
playerdatasync.premium.admin.coordinates:
|
||||
description: Allows toggling coordinate synchronization
|
||||
default: op
|
||||
playerdatasync.premium.admin.position:
|
||||
description: Allows toggling position synchronization
|
||||
default: op
|
||||
playerdatasync.premium.admin.xp:
|
||||
description: Allows toggling experience synchronization
|
||||
default: op
|
||||
playerdatasync.premium.admin.gamemode:
|
||||
description: Allows toggling gamemode synchronization
|
||||
default: op
|
||||
playerdatasync.premium.admin.enderchest:
|
||||
description: Allows toggling enderchest synchronization
|
||||
default: op
|
||||
playerdatasync.premium.admin.inventory:
|
||||
description: Allows toggling inventory synchronization
|
||||
default: op
|
||||
playerdatasync.premium.admin.armor:
|
||||
description: Allows toggling armor synchronization
|
||||
default: op
|
||||
playerdatasync.premium.admin.offhand:
|
||||
description: Allows toggling offhand synchronization
|
||||
default: op
|
||||
playerdatasync.premium.admin.health:
|
||||
description: Allows toggling health synchronization
|
||||
default: op
|
||||
playerdatasync.premium.admin.hunger:
|
||||
description: Allows toggling hunger synchronization
|
||||
default: op
|
||||
playerdatasync.premium.admin.effects:
|
||||
description: Allows toggling potion effects synchronization
|
||||
default: op
|
||||
playerdatasync.premium.admin.achievements:
|
||||
description: Allows toggling achievements synchronization
|
||||
default: op
|
||||
playerdatasync.premium.admin.statistics:
|
||||
description: Allows toggling statistics synchronization
|
||||
default: op
|
||||
playerdatasync.premium.admin.attributes:
|
||||
description: Allows toggling attributes synchronization
|
||||
default: op
|
||||
playerdatasync.premium.admin.permissions:
|
||||
description: Allows toggling permissions synchronization
|
||||
default: op
|
||||
playerdatasync.premium.admin.economy:
|
||||
description: Allows toggling economy synchronization
|
||||
default: op
|
||||
playerdatasync.premium.admin.reload:
|
||||
description: Allows reloading the plugin configuration
|
||||
default: op
|
||||
playerdatasync.premium.admin.save:
|
||||
description: Allows manually saving player data
|
||||
default: op
|
||||
playerdatasync.premium.admin.license:
|
||||
description: Allows managing license validation
|
||||
default: op
|
||||
playerdatasync.premium.admin.update:
|
||||
description: Allows checking for updates
|
||||
default: op
|
||||
|
||||
# Status and monitoring permissions
|
||||
playerdatasync.premium.status:
|
||||
description: Allows checking plugin status and statistics
|
||||
default: op
|
||||
playerdatasync.premium.status.others:
|
||||
description: Allows checking other players' sync status
|
||||
default: op
|
||||
|
||||
# Backup management permissions
|
||||
playerdatasync.premium.backup:
|
||||
description: Allows managing player data backups
|
||||
default: op
|
||||
playerdatasync.premium.backup.create:
|
||||
description: Allows creating backups
|
||||
default: op
|
||||
playerdatasync.premium.backup.restore:
|
||||
description: Allows restoring from backups
|
||||
default: op
|
||||
playerdatasync.premium.backup.others:
|
||||
description: Allows managing other players' backups
|
||||
default: op
|
||||
|
||||
# Import/Export permissions
|
||||
playerdatasync.premium.import:
|
||||
description: Allows importing player data
|
||||
default: op
|
||||
playerdatasync.premium.export:
|
||||
description: Allows exporting player data
|
||||
default: op
|
||||
|
||||
# Message permissions
|
||||
playerdatasync.premium.message.show.loading:
|
||||
description: Allows player to see loading messages
|
||||
default: true
|
||||
playerdatasync.premium.message.show.saving:
|
||||
description: Allows player to see saving messages
|
||||
default: true
|
||||
playerdatasync.premium.message.show.errors:
|
||||
description: Allows player to see error messages
|
||||
default: true
|
||||
playerdatasync.premium.message.show.sync:
|
||||
description: Allows player to see sync notifications
|
||||
default: false
|
||||
|
||||
# Bypass permissions
|
||||
playerdatasync.premium.bypass.sync:
|
||||
description: Bypass automatic data synchronization
|
||||
default: false
|
||||
playerdatasync.premium.bypass.autosave:
|
||||
description: Bypass automatic saves
|
||||
default: false
|
||||
playerdatasync.premium.bypass.validation:
|
||||
description: Bypass data validation checks
|
||||
default: false
|
||||
|
||||
# Integration permissions
|
||||
playerdatasync.premium.integration.vault:
|
||||
description: Allow Vault economy integration
|
||||
default: false
|
||||
playerdatasync.premium.integration.luckperms:
|
||||
description: Allow LuckPerms integration
|
||||
default: false
|
||||
playerdatasync.premium.integration.invsee:
|
||||
description: Allows viewing and editing stored player inventories through InvSee/OpenInv integration
|
||||
default: op
|
||||
playerdatasync.premium.integration.enderchest:
|
||||
description: Allows viewing and editing stored player ender chests through InvSee/OpenInv integration
|
||||
default: op
|
||||
Reference in New Issue
Block a user