This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
123
.gitignore
vendored
Normal file
123
.gitignore
vendored
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# Maven
|
||||||
|
target/
|
||||||
|
pom.xml.tag
|
||||||
|
pom.xml.releaseBackup
|
||||||
|
pom.xml.versionsBackup
|
||||||
|
pom.xml.next
|
||||||
|
release.properties
|
||||||
|
dependency-reduced-pom.xml
|
||||||
|
buildNumber.properties
|
||||||
|
.mvn/timing.properties
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
|
||||||
|
# Compiled class files
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Package Files
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.nar
|
||||||
|
*.ear
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
|
||||||
|
# IDE - IntelliJ IDEA
|
||||||
|
.idea/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# IDE - Eclipse
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
bin/
|
||||||
|
.metadata
|
||||||
|
|
||||||
|
# IDE - NetBeans
|
||||||
|
nbproject/private/
|
||||||
|
build/
|
||||||
|
nbbuild/
|
||||||
|
dist/
|
||||||
|
nbdist/
|
||||||
|
.nb-gradle/
|
||||||
|
|
||||||
|
# IDE - VS Code
|
||||||
|
.vscode/
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.desktop.ini
|
||||||
|
|
||||||
|
# Plugin specific
|
||||||
|
# Database files (SQLite)
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Configuration files with sensitive data
|
||||||
|
# Uncomment if you want to ignore config files with passwords
|
||||||
|
# config.yml
|
||||||
|
# database.yml
|
||||||
|
|
||||||
|
# Logs directory
|
||||||
|
logs/
|
||||||
|
server.log
|
||||||
|
latest.log
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
|
||||||
|
# Test output
|
||||||
|
test-output/
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Java
|
||||||
|
*.class
|
||||||
|
hs_err_pid*
|
||||||
|
replay_pid*
|
||||||
|
|
||||||
|
# Minecraft server files (if testing locally)
|
||||||
|
server/
|
||||||
|
world/
|
||||||
|
world_nether/
|
||||||
|
world_the_end/
|
||||||
|
plugins/
|
||||||
|
crash-reports/
|
||||||
|
world-datapack/
|
||||||
|
usercache.json
|
||||||
|
banned-players.json
|
||||||
|
banned-ips.json
|
||||||
|
ops.json
|
||||||
|
whitelist.json
|
||||||
|
eula.txt
|
||||||
|
server.properties
|
||||||
|
bukkit.yml
|
||||||
|
spigot.yml
|
||||||
|
paper.yml
|
||||||
|
|
||||||
|
# Plugin development
|
||||||
|
# Keep config.yml and plugin.yml as examples, but ignore local config changes
|
||||||
|
# Uncomment if you want to ignore all config changes:
|
||||||
|
# src/main/resources/config.yml
|
||||||
|
|
||||||
922
CHANGELOG.md
Normal file
922
CHANGELOG.md
Normal file
@@ -0,0 +1,922 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to PlayerDataSync will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.2.9-RELEASE] - 2026-01-25
|
||||||
|
|
||||||
|
### 🎯 Custom-Enchantment-Support & Database Upgrade / Custom-Enchantment-Support & Datenbank-Upgrade
|
||||||
|
|
||||||
|
### 🔧 Fixed
|
||||||
|
|
||||||
|
- **Database Truncation Error (Critical Fix) / Datenbank-Truncation-Fehler (Critical Fix)**:
|
||||||
|
- **EN:** Fixes "Data too long for column" errors with large inventories
|
||||||
|
- **DE:** Behebt "Data too long for column" Fehler bei großen Inventaren
|
||||||
|
- ✅ **EN:** Automatic upgrade from `TEXT` to `LONGTEXT` for `inventory`, `enderchest`, `armor`, and `offhand` columns
|
||||||
|
- ✅ **DE:** Automatisches Upgrade von `TEXT` zu `LONGTEXT` für `inventory`, `enderchest`, `armor` und `offhand` Spalten
|
||||||
|
- ✅ **EN:** Now supports inventories with many custom enchantments (e.g., ExcellentEnchants)
|
||||||
|
- ✅ **DE:** Unterstützt jetzt Inventare mit vielen Custom-Enchantments (z.B. ExcellentEnchants)
|
||||||
|
- ✅ **EN:** Upgrade is performed automatically on server start
|
||||||
|
- ✅ **DE:** Upgrade wird automatisch beim Server-Start durchgeführt
|
||||||
|
- ✅ **EN:** Runtime upgrade attempt on truncation errors
|
||||||
|
- ✅ **DE:** Runtime-Upgrade-Versuch bei Truncation-Fehlern
|
||||||
|
- ✅ **EN:** Improved error messages with solution suggestions
|
||||||
|
- ✅ **DE:** Verbesserte Fehlermeldungen mit Lösungsvorschlägen
|
||||||
|
- 🔧 **EN:** Fixes issues with large inventories and custom enchantments
|
||||||
|
- 🔧 **DE:** Behebt Probleme mit großen Inventaren und Custom-Enchantments
|
||||||
|
|
||||||
|
- **Custom-Enchantment Deserialization / Custom-Enchantment-Deserialisierung**:
|
||||||
|
- **EN:** Robust error handling for custom enchantments
|
||||||
|
- **DE:** Robuste Fehlerbehandlung für Custom-Enchantments
|
||||||
|
- ✅ **EN:** Improved detection of custom enchantment errors (e.g., `minecraft:venom`)
|
||||||
|
- ✅ **DE:** Verbesserte Erkennung von Custom-Enchantment-Fehlern (z.B. `minecraft:venom`)
|
||||||
|
- ✅ **EN:** Items are skipped instead of causing plugin crashes
|
||||||
|
- ✅ **DE:** Items werden übersprungen statt Plugin-Absturz zu verursachen
|
||||||
|
- ✅ **EN:** Data remains preserved in the database
|
||||||
|
- ✅ **DE:** Daten bleiben in der Datenbank erhalten
|
||||||
|
- ✅ **EN:** Detailed logging with enchantment names
|
||||||
|
- ✅ **DE:** Detailliertes Logging mit Enchantment-Namen
|
||||||
|
- ✅ **EN:** Support for ExcellentEnchants and similar plugins
|
||||||
|
- ✅ **DE:** Unterstützung für ExcellentEnchants und ähnliche Plugins
|
||||||
|
- 🔧 **EN:** Prevents crashes with unrecognized custom enchantments
|
||||||
|
- 🔧 **DE:** Verhindert Abstürze bei nicht erkannten Custom-Enchantments
|
||||||
|
|
||||||
|
- **Stale Player Data / Veraltete Spielerdaten**:
|
||||||
|
- **EN:** Fixes issue with outdated player data
|
||||||
|
- **DE:** Behebt Problem mit nicht aktualisierten Spielerdaten
|
||||||
|
- ✅ **EN:** Database upgrade enables successful saves
|
||||||
|
- ✅ **DE:** Datenbank-Upgrade ermöglicht erfolgreiche Speicherungen
|
||||||
|
- ✅ **EN:** Improved error handling prevents data loss
|
||||||
|
- ✅ **DE:** Verbesserte Fehlerbehandlung verhindert Datenverlust
|
||||||
|
- ✅ **EN:** Automatic recovery after database upgrade
|
||||||
|
- ✅ **DE:** Automatische Wiederherstellung nach Datenbank-Upgrade
|
||||||
|
|
||||||
|
### ✨ Added
|
||||||
|
|
||||||
|
- **Custom-Enchantment Synchronization / Custom-Enchantment-Synchronisation**:
|
||||||
|
- **EN:** Full support for custom enchantments
|
||||||
|
- **DE:** Vollständige Unterstützung für Custom-Enchantments
|
||||||
|
- ✅ **EN:** Preservation of all NBT data including custom enchantments during serialization
|
||||||
|
- ✅ **DE:** Erhaltung aller NBT-Daten inklusive Custom-Enchantments beim Serialisieren
|
||||||
|
- ✅ **EN:** Refresh mechanism after loading inventories (2-tick delay)
|
||||||
|
- ✅ **DE:** Refresh-Mechanismus nach dem Laden von Inventaren (2-Tick-Delay)
|
||||||
|
- ✅ **EN:** Explicit re-setting of items so plugins can process enchantments
|
||||||
|
- ✅ **DE:** Explizites Neusetzen von Items, damit Plugins Enchantments verarbeiten können
|
||||||
|
- ✅ **EN:** Works for main inventory, armor, offhand, and enderchest
|
||||||
|
- ✅ **DE:** Funktioniert für Hauptinventar, Rüstung, Offhand und Enderchest
|
||||||
|
- 📝 **EN:** Supports plugins like ExcellentEnchants that use custom enchantments
|
||||||
|
- 📝 **DE:** Unterstützt Plugins wie ExcellentEnchants, die Custom-Enchantments verwenden
|
||||||
|
|
||||||
|
- **Deserialization Statistics & Monitoring / Deserialisierungs-Statistiken & Monitoring**:
|
||||||
|
- **EN:** Comprehensive monitoring system
|
||||||
|
- **DE:** Umfassendes Monitoring-System
|
||||||
|
- ✅ **EN:** Counters for custom enchantment errors, version compatibility errors, and other errors
|
||||||
|
- ✅ **DE:** Zähler für Custom-Enchantment-Fehler, Versionskompatibilitäts-Fehler und andere Fehler
|
||||||
|
- ✅ **EN:** `getDeserializationStats()` method for statistics
|
||||||
|
- ✅ **DE:** `getDeserializationStats()` Methode für Statistiken
|
||||||
|
- ✅ **EN:** `resetDeserializationStats()` method to reset statistics
|
||||||
|
- ✅ **DE:** `resetDeserializationStats()` Methode zum Zurücksetzen
|
||||||
|
- ✅ **EN:** Integration into `/sync cache` command
|
||||||
|
- ✅ **DE:** Integration in `/sync cache` Befehl
|
||||||
|
- ✅ **EN:** Detailed error logging with enchantment names
|
||||||
|
- ✅ **DE:** Detaillierte Fehlerprotokollierung mit Enchantment-Namen
|
||||||
|
- 📝 **EN:** Admins can now easily monitor custom enchantment issues
|
||||||
|
- 📝 **DE:** Admins können jetzt Probleme mit Custom-Enchantments einfach überwachen
|
||||||
|
|
||||||
|
- **Improved Error Handling / Verbesserte Fehlerbehandlung**:
|
||||||
|
- **EN:** Extended error detection and handling
|
||||||
|
- **DE:** Erweiterte Fehlererkennung und -behandlung
|
||||||
|
- ✅ **EN:** Automatic extraction of enchantment names from error messages
|
||||||
|
- ✅ **DE:** Automatische Extraktion von Enchantment-Namen aus Fehlermeldungen
|
||||||
|
- ✅ **EN:** Detailed error chain analysis (up to 3 levels)
|
||||||
|
- ✅ **DE:** Detaillierte Fehlerketten-Analyse (bis zu 3 Ebenen)
|
||||||
|
- ✅ **EN:** Contextual error messages with solution suggestions
|
||||||
|
- ✅ **DE:** Kontextuelle Fehlermeldungen mit Lösungsvorschlägen
|
||||||
|
- ✅ **EN:** Better detection of various error types (IllegalStateException, NullPointerException, etc.)
|
||||||
|
- ✅ **DE:** Bessere Erkennung verschiedener Fehlertypen (IllegalStateException, NullPointerException, etc.)
|
||||||
|
- ✅ **EN:** Pattern-based detection of custom enchantment errors
|
||||||
|
- ✅ **DE:** Pattern-basierte Erkennung von Custom-Enchantment-Fehlern
|
||||||
|
|
||||||
|
### 🔄 Changed
|
||||||
|
|
||||||
|
- **Database Schema / Datenbank-Schema**:
|
||||||
|
- **EN:** Automatic upgrade for existing installations
|
||||||
|
- **DE:** Automatisches Upgrade für bestehende Installationen
|
||||||
|
- ✅ **EN:** `inventory`: TEXT → LONGTEXT (max. ~4GB instead of ~65KB)
|
||||||
|
- ✅ **DE:** `inventory`: TEXT → LONGTEXT (max. ~4GB statt ~65KB)
|
||||||
|
- ✅ **EN:** `enderchest`: TEXT → LONGTEXT
|
||||||
|
- ✅ **DE:** `enderchest`: TEXT → LONGTEXT
|
||||||
|
- ✅ **EN:** `armor`: TEXT → LONGTEXT
|
||||||
|
- ✅ **DE:** `armor`: TEXT → LONGTEXT
|
||||||
|
- ✅ **EN:** `offhand`: TEXT → LONGTEXT
|
||||||
|
- ✅ **DE:** `offhand`: TEXT → LONGTEXT
|
||||||
|
- ✅ **EN:** Upgrade is performed automatically on server start
|
||||||
|
- ✅ **DE:** Upgrade wird beim Server-Start automatisch durchgeführt
|
||||||
|
- 📝 **EN:** Existing data is preserved, no data migration needed
|
||||||
|
- 📝 **DE:** Bestehende Daten bleiben erhalten, keine Datenmigration nötig
|
||||||
|
|
||||||
|
- **EditorIntegration Removed / EditorIntegration entfernt**:
|
||||||
|
- **EN:** Preparation for website update
|
||||||
|
- **DE:** Vorbereitung für Website-Update
|
||||||
|
- ✅ **EN:** EditorIntegrationManager completely removed
|
||||||
|
- ✅ **DE:** EditorIntegrationManager komplett entfernt
|
||||||
|
- ✅ **EN:** All editor-related commands removed
|
||||||
|
- ✅ **DE:** Alle Editor-bezogenen Befehle entfernt
|
||||||
|
- ✅ **EN:** Code cleanup for future editor integration
|
||||||
|
- ✅ **DE:** Code-Bereinigung für zukünftige Editor-Integration
|
||||||
|
- 📝 **EN:** New editor integration will be added in a future version
|
||||||
|
- 📝 **DE:** Neue Editor-Integration wird in zukünftiger Version hinzugefügt
|
||||||
|
|
||||||
|
### 📊 Technical Details
|
||||||
|
|
||||||
|
#### Database Upgrade Process / Datenbank-Upgrade-Prozess
|
||||||
|
|
||||||
|
**EN:** The plugin automatically performs an upgrade of database columns on startup:
|
||||||
|
|
||||||
|
**DE:** Das Plugin führt beim Start automatisch ein Upgrade der Datenbank-Spalten durch:
|
||||||
|
|
||||||
|
1. **EN:** **Check**: Verifies the current data type of each column
|
||||||
|
**DE:** **Prüfung**: Überprüft den aktuellen Datentyp jeder Spalte
|
||||||
|
2. **EN:** **Upgrade**: Converts `TEXT` to `LONGTEXT` if necessary
|
||||||
|
**DE:** **Upgrade**: Konvertiert `TEXT` zu `LONGTEXT` wenn nötig
|
||||||
|
3. **EN:** **Logging**: Logs all upgrades for transparency
|
||||||
|
**DE:** **Logging**: Protokolliert alle Upgrades für Transparenz
|
||||||
|
4. **EN:** **Runtime Upgrade**: Also attempts to upgrade during runtime if an error occurs
|
||||||
|
**DE:** **Runtime-Upgrade**: Versucht auch während des Betriebs zu upgraden, wenn ein Fehler auftritt
|
||||||
|
|
||||||
|
**EN:** **Why LONGTEXT?**
|
||||||
|
**DE:** **Warum LONGTEXT?**
|
||||||
|
- `TEXT`: Max. ~65KB (65,535 bytes)
|
||||||
|
- `LONGTEXT`: Max. ~4GB (4,294,967,295 bytes)
|
||||||
|
- **EN:** Custom enchantments with extensive NBT data can become very large
|
||||||
|
- **DE:** Custom-Enchantments mit vielen NBT-Daten können sehr groß werden
|
||||||
|
- **EN:** Large inventories with many items and enchantments require more space
|
||||||
|
- **DE:** Große Inventare mit vielen Items und Enchantments benötigen mehr Platz
|
||||||
|
|
||||||
|
#### Custom-Enchantment Error Handling / Custom-Enchantment-Fehlerbehandlung
|
||||||
|
|
||||||
|
**EN:** The improved error handling recognizes various error types:
|
||||||
|
|
||||||
|
**DE:** Die verbesserte Fehlerbehandlung erkennt verschiedene Fehlertypen:
|
||||||
|
|
||||||
|
- **IllegalStateException** with DataResult/Codec/Decoder
|
||||||
|
- **NullPointerException** in enchantment-related classes
|
||||||
|
- **EN:** Error messages with "enchantment not found/unknown/invalid"
|
||||||
|
- **DE:** Fehlermeldungen mit "enchantment not found/unknown/invalid"
|
||||||
|
- **EN:** Pattern-based detection of custom enchantment names
|
||||||
|
- **DE:** Pattern-basierte Erkennung von Custom-Enchantment-Namen
|
||||||
|
|
||||||
|
**EN:** **Error Handling Flow:**
|
||||||
|
**DE:** **Fehlerbehandlung-Flow:**
|
||||||
|
1. **EN:** Attempt normal deserialization
|
||||||
|
**DE:** Versuch der normalen Deserialisierung
|
||||||
|
2. **EN:** On error: Check if it's a custom enchantment problem
|
||||||
|
**DE:** Bei Fehler: Prüfung ob es ein Custom-Enchantment-Problem ist
|
||||||
|
3. **EN:** Extract enchantment name from error message
|
||||||
|
**DE:** Extraktion des Enchantment-Namens aus der Fehlermeldung
|
||||||
|
4. **EN:** Detailed logging with context
|
||||||
|
**DE:** Detailliertes Logging mit Kontext
|
||||||
|
5. **EN:** Item is skipped (null), but data remains in database
|
||||||
|
**DE:** Item wird übersprungen (null), aber Daten bleiben in DB
|
||||||
|
6. **EN:** Statistics are updated
|
||||||
|
**DE:** Statistiken werden aktualisiert
|
||||||
|
|
||||||
|
#### Refresh Mechanism / Refresh-Mechanismus
|
||||||
|
|
||||||
|
**EN:** After loading inventories, a refresh mechanism is executed:
|
||||||
|
|
||||||
|
**DE:** Nach dem Laden von Inventaren wird ein Refresh-Mechanismus ausgeführt:
|
||||||
|
|
||||||
|
1. **EN:** **Initial Load**: ItemStacks are loaded from database
|
||||||
|
**DE:** **Initiales Laden**: ItemStacks werden aus der Datenbank geladen
|
||||||
|
2. **EN:** **2-Tick Delay**: Waits 2 ticks to give plugins time to initialize
|
||||||
|
**DE:** **2-Tick-Delay**: Wartet 2 Ticks, damit Plugins Zeit haben zu initialisieren
|
||||||
|
3. **EN:** **Refresh**: Explicitly re-sets items to trigger plugin processing
|
||||||
|
**DE:** **Refresh**: Setzt Items explizit neu, um Plugin-Verarbeitung zu triggern
|
||||||
|
4. **EN:** **Update**: Calls `updateInventory()` for client synchronization
|
||||||
|
**DE:** **Update**: Ruft `updateInventory()` auf für Client-Synchronisation
|
||||||
|
|
||||||
|
**EN:** **Why 2 Ticks?**
|
||||||
|
**DE:** **Warum 2 Ticks?**
|
||||||
|
- **EN:** Gives custom enchantment plugins time to register their enchantments
|
||||||
|
- **DE:** Gibt Custom-Enchantment-Plugins Zeit, ihre Enchantments zu registrieren
|
||||||
|
- **EN:** Enables plugin event handlers to react to item changes
|
||||||
|
- **DE:** Ermöglicht Plugin-Event-Handler, auf Item-Änderungen zu reagieren
|
||||||
|
- **EN:** Prevents race conditions between plugin loading and item loading
|
||||||
|
- **DE:** Verhindert Race-Conditions zwischen Plugin-Loading und Item-Loading
|
||||||
|
|
||||||
|
#### Statistics System / Statistiken-System
|
||||||
|
|
||||||
|
**EN:** The new statistics system collects information about deserialization errors:
|
||||||
|
|
||||||
|
**DE:** Das neue Statistiken-System sammelt Informationen über Deserialisierungs-Fehler:
|
||||||
|
|
||||||
|
- **EN:** **Custom Enchantment Errors**: Counts items skipped due to unrecognized custom enchantments
|
||||||
|
- **DE:** **Custom-Enchantment-Fehler**: Zählt Items, die wegen nicht erkannter Custom-Enchantments übersprungen wurden
|
||||||
|
- **EN:** **Version Compatibility Errors**: Counts items with version compatibility issues
|
||||||
|
- **DE:** **Versionskompatibilitäts-Fehler**: Zählt Items mit Versionskompatibilitätsproblemen
|
||||||
|
- **EN:** **Other Errors**: Counts all other deserialization errors
|
||||||
|
- **DE:** **Andere Fehler**: Zählt alle anderen Deserialisierungs-Fehler
|
||||||
|
|
||||||
|
**EN:** **Usage:**
|
||||||
|
**DE:** **Verwendung:**
|
||||||
|
```bash
|
||||||
|
/sync cache # EN: Shows all statistics / DE: Zeigt alle Statistiken
|
||||||
|
/sync cache clear # EN: Resets statistics / DE: Setzt Statistiken zurück
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔍 Monitoring & Debugging
|
||||||
|
|
||||||
|
**EN:** Admins can now easily monitor custom enchantment issues:
|
||||||
|
|
||||||
|
**DE:** Admins können jetzt einfach Probleme mit Custom-Enchantments überwachen:
|
||||||
|
|
||||||
|
1. **EN:** **View Statistics**: `/sync cache` shows deserialization statistics
|
||||||
|
**DE:** **Statistiken anzeigen**: `/sync cache` zeigt Deserialisierungs-Statistiken
|
||||||
|
2. **EN:** **Analyze Errors**: Detailed logs show exactly which enchantments cause problems
|
||||||
|
**DE:** **Fehler analysieren**: Detaillierte Logs zeigen genau, welche Enchantments Probleme verursachen
|
||||||
|
3. **EN:** **Fix Issues**: Clear error messages with solution suggestions
|
||||||
|
**DE:** **Probleme beheben**: Klare Fehlermeldungen mit Lösungsvorschlägen
|
||||||
|
|
||||||
|
**EN:** **Example Output:**
|
||||||
|
**DE:** **Beispiel-Output:**
|
||||||
|
```
|
||||||
|
Deserialization Stats: Deserialization failures: 5 total
|
||||||
|
(Custom Enchantments: 3, Version Issues: 1, Other: 1)
|
||||||
|
⚠ If you see custom enchantment failures, ensure enchantment plugins
|
||||||
|
(e.g., ExcellentEnchants) are loaded and all enchantments are registered.
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚠️ Important Notes / Wichtige Hinweise
|
||||||
|
|
||||||
|
- **EN:** **Database Upgrade**: On first start after update, columns are automatically upgraded
|
||||||
|
**DE:** **Datenbank-Upgrade**: Beim ersten Start nach dem Update werden die Spalten automatisch geupgradet
|
||||||
|
- **EN:** **Custom Enchantments**: Ensure enchantment plugins (e.g., ExcellentEnchants) are installed and active on both servers
|
||||||
|
**DE:** **Custom-Enchantments**: Stellen Sie sicher, dass Enchantment-Plugins (z.B. ExcellentEnchants) auf beiden Servern installiert und aktiv sind
|
||||||
|
- **EN:** **Plugin Load Order**: Enchantment plugins should load before PlayerDataSync (check `plugin.yml`)
|
||||||
|
**DE:** **Plugin-Load-Reihenfolge**: Enchantment-Plugins sollten vor PlayerDataSync geladen werden (in `plugin.yml` prüfen)
|
||||||
|
- **EN:** **EditorIntegration**: EditorIntegration has been removed and will be re-implemented in a future version
|
||||||
|
**DE:** **EditorIntegration**: Die EditorIntegration wurde entfernt und wird in einer zukünftigen Version neu implementiert
|
||||||
|
|
||||||
|
### 📝 Migration Guide
|
||||||
|
|
||||||
|
**EN:** **For Existing Installations:**
|
||||||
|
|
||||||
|
**DE:** **Für bestehende Installationen:**
|
||||||
|
|
||||||
|
1. **EN:** **Automatic Upgrade**: No manual action needed - plugin performs upgrade automatically
|
||||||
|
**DE:** **Automatisches Upgrade**: Keine manuelle Aktion nötig - das Plugin führt das Upgrade automatisch durch
|
||||||
|
2. **EN:** **Restart Server**: Restart server after update to perform database upgrade
|
||||||
|
**DE:** **Server neu starten**: Nach dem Update den Server neu starten, damit das Datenbank-Upgrade durchgeführt wird
|
||||||
|
3. **EN:** **Check Logs**: Verify logs for upgrade messages:
|
||||||
|
**DE:** **Logs prüfen**: Überprüfen Sie die Logs auf Upgrade-Meldungen:
|
||||||
|
```
|
||||||
|
[INFO] Upgraded inventory column from TEXT to LONGTEXT to support large inventories
|
||||||
|
[INFO] Upgraded enderchest column from TEXT to LONGTEXT to support large inventories
|
||||||
|
[INFO] Upgraded armor column from TEXT to LONGTEXT to support large inventories
|
||||||
|
[INFO] Upgraded offhand column from TEXT to LONGTEXT to support large inventories
|
||||||
|
```
|
||||||
|
4. **EN:** **Check Custom Enchantments**: Ensure all enchantment plugins are loaded correctly
|
||||||
|
**DE:** **Custom-Enchantments prüfen**: Stellen Sie sicher, dass alle Enchantment-Plugins korrekt geladen sind
|
||||||
|
|
||||||
|
**EN:** **Troubleshooting:**
|
||||||
|
|
||||||
|
**DE:** **Bei Problemen:**
|
||||||
|
|
||||||
|
- **EN:** Check `/sync cache` for deserialization statistics
|
||||||
|
**DE:** Prüfen Sie `/sync cache` für Deserialisierungs-Statistiken
|
||||||
|
- **EN:** Review logs for custom enchantment errors
|
||||||
|
**DE:** Überprüfen Sie die Logs auf Custom-Enchantment-Fehler
|
||||||
|
- **EN:** Ensure enchantment plugins are installed on both servers
|
||||||
|
**DE:** Stellen Sie sicher, dass Enchantment-Plugins auf beiden Servern installiert sind
|
||||||
|
- **EN:** Check plugin load order in `plugin.yml`
|
||||||
|
**DE:** Prüfen Sie die Plugin-Load-Reihenfolge in `plugin.yml`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.2.7-RELEASE] - 2025-12-29
|
||||||
|
|
||||||
|
### 🔧 Critical Fixes & New Features
|
||||||
|
|
||||||
|
This release includes critical bug fixes for XP synchronization and Vault economy, plus a new Respawn to Lobby feature.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Issue #45 - XP & Level Synchronization (Critical Fix)**: Complete rewrite of experience synchronization
|
||||||
|
- ✅ Replaced unreliable `setTotalExperience()` with `giveExp()` as primary method
|
||||||
|
- ✅ `giveExp()` is more reliable across all Minecraft versions (1.8-1.21.11)
|
||||||
|
- ✅ Better error handling and verification with detailed logging
|
||||||
|
- ✅ Automatic correction if experience doesn't match expected value
|
||||||
|
- ✅ Prevents XP sync failures on all supported versions
|
||||||
|
- ✅ Improved level calculation and synchronization
|
||||||
|
- 🔧 Fixes Issue #43, #45 and XP sync problems across version range
|
||||||
|
- 📝 Detailed logging for debugging XP sync issues
|
||||||
|
- **Issue #46 - Vault Balance de-sync on server shutdown**: Fixed economy balance not being saved during shutdown
|
||||||
|
- ✅ Enhanced shutdown save process to ensure Vault economy is available
|
||||||
|
- ✅ Reconfigure economy integration before shutdown save
|
||||||
|
- ✅ Added delay to ensure Vault is fully initialized before saving
|
||||||
|
- ✅ Force balance refresh before save to get latest balance
|
||||||
|
- ✅ Better error handling and logging during shutdown
|
||||||
|
- ✅ Prevents economy balance loss on server restart
|
||||||
|
- 🔧 Fixes Issue #46: Vault Balance de-sync on server shutdown
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Respawn to Lobby Feature**: New feature to send players to lobby server after death/respawn
|
||||||
|
- ✅ Automatically transfers players to lobby server after respawn
|
||||||
|
- ✅ Uses existing BungeeCord integration and shared database
|
||||||
|
- ✅ Configurable lobby server name
|
||||||
|
- ✅ Saves player data before transfer to ensure data consistency
|
||||||
|
- ✅ Smart detection to prevent transfers if already on lobby server
|
||||||
|
- ✅ Requires BungeeCord integration to be enabled
|
||||||
|
- 📝 Configuration: `respawn_to_lobby.enabled` and `respawn_to_lobby.server` in config.yml
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
- **XP Sync Method Change**: Switched from `setTotalExperience()` to `giveExp()` for better compatibility
|
||||||
|
- **Why**: `setTotalExperience()` has version-specific bugs, `giveExp()` works reliably everywhere
|
||||||
|
- **Verification**: Added automatic verification and correction mechanism
|
||||||
|
- **Logging**: Enhanced logging with before/after values for debugging
|
||||||
|
- **Shutdown Process**: Improved economy save process with Vault reconfiguration and balance refresh
|
||||||
|
- **Respawn Handler**: New `PlayerRespawnEvent` handler for lobby transfer functionality
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.2.8-BETA] - 2025-12-29
|
||||||
|
|
||||||
|
### 🎉 Big Update - Major Improvements & API Migration
|
||||||
|
|
||||||
|
This release includes significant improvements, API migrations, and enhanced compatibility features.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **XP Synchronization (Critical Fix)**: Complete rewrite of experience synchronization
|
||||||
|
- ✅ Replaced unreliable `setTotalExperience()` with `giveExp()` as primary method
|
||||||
|
- ✅ `giveExp()` is more reliable across all Minecraft versions (1.8-1.21.11)
|
||||||
|
- ✅ Better error handling and verification with detailed logging
|
||||||
|
- ✅ Automatic correction if experience doesn't match expected value
|
||||||
|
- ✅ Prevents XP sync failures on all supported versions
|
||||||
|
- 🔧 Fixes Issue #43, #45 and XP sync problems across version range
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Update Checker**: Complete migration to CraftingStudio Pro API
|
||||||
|
- ✅ Migrated from SpigotMC Legacy API to CraftingStudio Pro API
|
||||||
|
- ✅ New API endpoint: `https://craftingstudiopro.de/api/plugins/playerdatasync/latest`
|
||||||
|
- ✅ Uses plugin slug (`playerdatasync`) instead of resource ID
|
||||||
|
- ✅ Improved JSON response parsing using Gson library
|
||||||
|
- ✅ Better error handling for HTTP status codes (429 Rate Limit, etc.)
|
||||||
|
- ✅ Enhanced update information with download URLs from API response
|
||||||
|
- 📖 API Documentation: https://www.craftingstudiopro.de/docs/api
|
||||||
|
- **Plugin API Version**: Updated to 1.13 for better modern API support
|
||||||
|
- Minimum required Minecraft version: 1.13
|
||||||
|
- Still supports versions 1.8-1.21.11 with automatic compatibility handling
|
||||||
|
- Improved NamespacedKey support and modern Material API usage
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Version Compatibility**: Fixed critical GRAY_STAINED_GLASS_PANE compatibility issue
|
||||||
|
- ✅ Prevents fatal error on Minecraft 1.8-1.12 servers
|
||||||
|
- ✅ Automatic version detection and Material selection
|
||||||
|
- ✅ Uses `STAINED_GLASS_PANE` with durability value 7 for older versions (1.8-1.12)
|
||||||
|
- ✅ Uses `GRAY_STAINED_GLASS_PANE` for modern versions (1.13+)
|
||||||
|
- ✅ Filler item in inventory viewer now works correctly across all supported versions
|
||||||
|
- **Inventory Synchronization**: Enhanced inventory sync reliability
|
||||||
|
- ✅ Added `updateInventory()` calls after loading inventory, armor, and offhand
|
||||||
|
- ✅ Improved client synchronization for all inventory types
|
||||||
|
- ✅ Better inventory size validation (normalized to 36 slots for main inventory)
|
||||||
|
- ✅ Improved enderchest size validation (normalized to 27 slots)
|
||||||
|
- ✅ Better armor array normalization (ensures exactly 4 slots)
|
||||||
|
- **ItemStack Validation**: Enhanced ItemStack sanitization and validation
|
||||||
|
- ✅ Improved validation of item amounts (checks against max stack size)
|
||||||
|
- ✅ Better handling of invalid stack sizes (clamps to max instead of removing)
|
||||||
|
- ✅ Improved AIR item filtering
|
||||||
|
- ✅ More robust error handling for corrupted items
|
||||||
|
- **Logging System**: Complete logging overhaul
|
||||||
|
- ✅ Replaced all `System.err.println()` calls with proper Bukkit logger
|
||||||
|
- ✅ Replaced all `printStackTrace()` calls with proper `logger.log()` calls
|
||||||
|
- ✅ Better log levels (WARNING/SEVERE instead of stderr for compatibility issues)
|
||||||
|
- ✅ More consistent error messages across the codebase
|
||||||
|
- ✅ Stack traces now properly logged through plugin logger
|
||||||
|
- ✅ Improved logging for version compatibility issues
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- **Code Quality**: Significant improvements to error handling and resource management
|
||||||
|
- ✅ Comprehensive exception handling with proper stack trace logging
|
||||||
|
- ✅ Better debug logging throughout inventory operations
|
||||||
|
- ✅ Improved client synchronization after inventory changes
|
||||||
|
- ✅ Better resource management and cleanup
|
||||||
|
- ✅ Enhanced error diagnostics throughout the codebase
|
||||||
|
- **Performance**: Optimizations for inventory operations
|
||||||
|
- ✅ Better item validation prevents unnecessary operations
|
||||||
|
- ✅ Improved error recovery mechanisms
|
||||||
|
- ✅ Enhanced memory management
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
- **API Migration**: Complete rewrite of UpdateChecker class
|
||||||
|
- Old: SpigotMC Legacy API (plain text response)
|
||||||
|
- New: CraftingStudio Pro API (JSON response with Gson parsing)
|
||||||
|
- Improved error handling for network issues and rate limits
|
||||||
|
- **Compatibility**: Maintained support for Minecraft 1.8-1.21.11
|
||||||
|
- Version-based feature detection and automatic disabling
|
||||||
|
- Graceful degradation for unsupported features on older versions
|
||||||
|
- Comprehensive version compatibility checking at startup
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
⚠️ **Plugin API Version**: Changed from `1.8` to `1.13`
|
||||||
|
- Plugins compiled with this version require at least Minecraft 1.13
|
||||||
|
- Server administrators using 1.8-1.12 should continue using previous versions
|
||||||
|
- Automatic legacy conversion may still work, but not guaranteed
|
||||||
|
|
||||||
|
### Migration Guide
|
||||||
|
If upgrading from 1.2.6-RELEASE or earlier:
|
||||||
|
1. No configuration changes required
|
||||||
|
2. Update checker will now use CraftingStudio Pro API
|
||||||
|
3. All existing data is compatible
|
||||||
|
4. Recommended to test on a staging server first
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.2.6-RELEASE] - 2025-12-29
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Update Checker**: Migrated to CraftingStudio Pro API
|
||||||
|
- Updated from SpigotMC API to CraftingStudio Pro API (https://craftingstudiopro.de/api)
|
||||||
|
- Now uses plugin slug instead of resource ID
|
||||||
|
- Improved JSON response parsing for better update information
|
||||||
|
- Better error handling for rate limits (429 responses)
|
||||||
|
- API endpoint: `/api/plugins/playerdatasync/latest`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Version Compatibility**: Fixed GRAY_STAINED_GLASS_PANE compatibility issue for Minecraft 1.8-1.12
|
||||||
|
- Added version check to use STAINED_GLASS_PANE with durability value 7 for older versions
|
||||||
|
- Prevents fatal error when loading plugin on 1.8-1.12 servers
|
||||||
|
- Filler item in inventory viewer now works correctly across all supported versions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.2.7-ALPHA] - 2025-12-29
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Update Checker**: Migrated to CraftingStudio Pro API
|
||||||
|
- Updated from SpigotMC API to CraftingStudio Pro API (https://craftingstudiopro.de/api)
|
||||||
|
- Now uses plugin slug instead of resource ID
|
||||||
|
- Improved JSON response parsing for better update information
|
||||||
|
- Better error handling for rate limits (429 responses)
|
||||||
|
- API endpoint: `/api/plugins/playerdatasync/latest`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Version Compatibility**: Fixed GRAY_STAINED_GLASS_PANE compatibility issue for Minecraft 1.8-1.12
|
||||||
|
- Added version check to use STAINED_GLASS_PANE with durability value 7 for older versions
|
||||||
|
- Prevents fatal error when loading plugin on 1.8-1.12 servers
|
||||||
|
- Filler item in inventory viewer now works correctly across all supported versions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.2.6-ALPHA] - 2025-12-29
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- **Inventory Synchronization**: Significantly improved inventory sync reliability
|
||||||
|
- Added `updateInventory()` call after loading inventory, armor, and offhand to ensure client synchronization
|
||||||
|
- Improved inventory size validation (normalized to 36 slots for main inventory)
|
||||||
|
- Improved enderchest size validation (normalized to 27 slots)
|
||||||
|
- Better armor array normalization (ensures exactly 4 slots)
|
||||||
|
- Enhanced error handling with detailed logging and stack traces
|
||||||
|
- Added debug logging for successful inventory loads
|
||||||
|
- **ItemStack Validation**: Enhanced ItemStack sanitization
|
||||||
|
- Improved validation of item amounts (checks against max stack size)
|
||||||
|
- Better handling of invalid stack sizes (clamps to max stack size instead of removing)
|
||||||
|
- Improved AIR item filtering
|
||||||
|
- More robust error handling for corrupted items
|
||||||
|
- **Logging System**: Improved logging consistency
|
||||||
|
- Replaced all `System.err.println()` calls with proper Bukkit logger
|
||||||
|
- Replaced all `printStackTrace()` calls with proper logger.log() calls
|
||||||
|
- Better log levels (WARNING instead of stderr for compatibility issues)
|
||||||
|
- More consistent error messages across the codebase
|
||||||
|
- Improved logging for version compatibility issues
|
||||||
|
- Stack traces now properly logged through plugin logger instead of printStackTrace()
|
||||||
|
- **Code Quality**: Further improvements to error handling
|
||||||
|
- Added comprehensive exception handling with stack traces
|
||||||
|
- Better debug logging throughout inventory operations
|
||||||
|
- Improved client synchronization after inventory changes
|
||||||
|
- Better resource management and cleanup
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Inventory Sync Issues**: Fixed cases where inventory changes weren't synchronized with client
|
||||||
|
- **ItemStack Validation**: Fixed potential issues with invalid item amounts and stack sizes
|
||||||
|
- **Logging**: Fixed inconsistent logging using System.err instead of proper logger
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.2.5-SNAPSHOT] - 2025-12-29
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Issue #45 - XP Sync Not Working**: Fixed experience synchronization not working
|
||||||
|
- Improved `applyExperience()` method with proper reset before setting experience
|
||||||
|
- Added verification to ensure experience is set correctly
|
||||||
|
- Added fallback mechanism using `giveExp()` if `setTotalExperience()` doesn't work
|
||||||
|
- Better error handling with detailed logging and stack traces
|
||||||
|
- Now works reliably across all Minecraft versions (1.8-1.21.11)
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- **Code Quality**: Fixed deprecated method usage and improved compatibility
|
||||||
|
- Replaced deprecated `URL(String)` constructor with `URI.toURL()` for better Java 20+ compatibility
|
||||||
|
- Replaced deprecated `PotionEffectType.getName()` with `getKey().getKey()` for better compatibility
|
||||||
|
- Improved `PotionEffectType.getByName()` usage with NamespacedKey fallback
|
||||||
|
- Replaced deprecated `getMaxHealth()` with Attribute system where available
|
||||||
|
- Improved `getOfflinePlayer(String)` usage with better error handling
|
||||||
|
- Added `@SuppressWarnings` annotations for necessary deprecated method usage
|
||||||
|
- Cleaned up unused imports and improved code organization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.2.4-SNAPSHOT] - 2025-12-29
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Extended Version Support**: Full compatibility with Minecraft 1.8 to 1.21.11
|
||||||
|
- Comprehensive version detection and compatibility checking
|
||||||
|
- Maven build profiles for all major Minecraft versions (1.8, 1.9-1.16, 1.17, 1.18-1.20, 1.21+)
|
||||||
|
- Automatic feature detection and disabling based on server version
|
||||||
|
- VersionCompatibility utility class for runtime version checks
|
||||||
|
- **Project Structure Reorganization**: Complete package restructuring
|
||||||
|
- Organized code into logical packages: `core`, `database`, `integration`, `listeners`, `managers`, `utils`, `commands`, `api`
|
||||||
|
- Improved code maintainability and organization
|
||||||
|
- All imports and package declarations updated accordingly
|
||||||
|
- **Version-Based Feature Management**: Automatic feature disabling
|
||||||
|
- Offhand sync automatically disabled on 1.8 (requires 1.9+)
|
||||||
|
- Attribute sync automatically disabled on 1.8 (requires 1.9+)
|
||||||
|
- Advancement sync automatically disabled on 1.8-1.11 (requires 1.12+)
|
||||||
|
- Features are checked and disabled during plugin initialization
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Issue #43 - Experience Synchronization Error**: Fixed experience synchronization issues
|
||||||
|
- Initial fix for experience synchronization problems
|
||||||
|
- Added validation for negative experience values
|
||||||
|
- **Issue #42 - Vault Reset on Server Restart**: Fixed economy balance not being restored on server restart
|
||||||
|
- Economy integration is now reconfigured during shutdown to ensure availability
|
||||||
|
- Balance restoration with 5-tick delay and retry mechanism
|
||||||
|
- Improved Vault provider availability checking
|
||||||
|
- **Issue #41 - Potion Effect on Death**: Fixed potion effects being restored after death
|
||||||
|
- Effects are only restored if player is not dead or respawning
|
||||||
|
- Added death/respawn state checking before effect restoration
|
||||||
|
- Effects are properly cleared on death as expected
|
||||||
|
- **Issue #40 - Heartbeat HTTP 500**: Improved error handling for HTTP 500 errors
|
||||||
|
- Enhanced error handling with detailed logging
|
||||||
|
- Specific error messages for different HTTP status codes (400, 401, 404, 500+)
|
||||||
|
- Connection timeout and socket timeout handling
|
||||||
|
- Better debugging information for API issues
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Minecraft Version Support**: Extended from 1.20.4-1.21.9 to 1.8-1.21.11
|
||||||
|
- Default Java version set to 8 for maximum compatibility
|
||||||
|
- Maven profiles for different Java versions (8, 16, 17, 21)
|
||||||
|
- Plugin API version set to 1.8 (lowest supported version)
|
||||||
|
- **Build System**: Enhanced Maven configuration
|
||||||
|
- Compiler plugin now uses variables for source/target versions
|
||||||
|
- Multiple build profiles for different Minecraft versions
|
||||||
|
- Proper Java version handling per Minecraft version
|
||||||
|
- **Code Organization**: Complete package restructure
|
||||||
|
- `core/`: Main plugin class (PlayerDataSync)
|
||||||
|
- `database/`: Database management (DatabaseManager, ConnectionPool)
|
||||||
|
- `integration/`: Plugin integrations (EditorIntegrationManager, InventoryViewerIntegrationManager)
|
||||||
|
- `listeners/`: Event listeners (PlayerDataListener, ServerSwitchListener)
|
||||||
|
- `managers/`: Manager classes (AdvancementSyncManager, BackupManager, ConfigManager, MessageManager)
|
||||||
|
- `utils/`: Utility classes (InventoryUtils, OfflinePlayerData, PlayerDataCache, VersionCompatibility)
|
||||||
|
- `commands/`: Command handlers (SyncCommand)
|
||||||
|
- `api/`: API and update checker (UpdateChecker)
|
||||||
|
- **Version Compatibility Checking**: Enhanced startup version validation
|
||||||
|
- Detects Minecraft version range (1.8 to 1.21.11)
|
||||||
|
- Logs feature availability based on version
|
||||||
|
- Provides warnings for unsupported versions
|
||||||
|
- Tests critical API methods with version checks
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
- **Build Profiles**:
|
||||||
|
- `mvn package -Pmc-1.8` for Minecraft 1.8 (Java 8)
|
||||||
|
- `mvn package -Pmc-1.9` through `-Pmc-1.16` for 1.9-1.16 (Java 8)
|
||||||
|
- `mvn package -Pmc-1.17` for Minecraft 1.17 (Java 16)
|
||||||
|
- `mvn package -Pmc-1.18` through `-Pmc-1.20` for 1.18-1.20 (Java 17)
|
||||||
|
- `mvn package -Pmc-1.21` for Minecraft 1.21+ (Java 21) - Default
|
||||||
|
- **Code Quality**: Improved error handling and version compatibility
|
||||||
|
- **Resource Management**: Better cleanup and memory management
|
||||||
|
- **Exception Handling**: More specific error messages and recovery mechanisms
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
- **Minecraft 1.8**: Full support (some features disabled)
|
||||||
|
- **Minecraft 1.9-1.11**: Full support (advancements disabled)
|
||||||
|
- **Minecraft 1.12-1.16**: Full support
|
||||||
|
- **Minecraft 1.17**: Full support
|
||||||
|
- **Minecraft 1.18-1.20**: Full support
|
||||||
|
- **Minecraft 1.21-1.21.11**: Full support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [Unreleased] - 2025-01-14
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **API Server ID**: Fixed "Missing server_id" error in heartbeat and API requests
|
||||||
|
- Changed JSON field name from `"serverId"` (camelCase) to `"server_id"` (snake_case) in all API payloads
|
||||||
|
- Improved server_id resolution from config file with better fallback handling
|
||||||
|
- Added detailed logging to track which server_id source is being used
|
||||||
|
- Fixed server_id not being read correctly from `server.id` config option
|
||||||
|
- All API endpoints (heartbeat, token, snapshot) now correctly send server_id
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Message Configuration**: New option to disable sync messages
|
||||||
|
- Added `messages.show_sync_messages` config option (default: `true`)
|
||||||
|
- When set to `false`, all sync-related messages (loading, saving, server switch) are disabled
|
||||||
|
- Prevents empty messages from being sent when message strings are empty
|
||||||
|
- Works in conjunction with existing permission system
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **API Integration**: All JSON payloads now use snake_case for `server_id` field
|
||||||
|
- `buildHeartbeatPayload()`: Uses `"server_id"` instead of `"serverId"`
|
||||||
|
- `buildTokenPayload()`: Uses `"server_id"` instead of `"serverId"`
|
||||||
|
- `buildSnapshotPayload()`: Uses `"server_id"` instead of `"serverId"`
|
||||||
|
- **Server ID Resolution**: Enhanced resolution logic with better error handling
|
||||||
|
- Checks environment variables first, then system properties, then ConfigManager, then direct config
|
||||||
|
- Always ensures a valid server_id is returned (never null or empty)
|
||||||
|
- Added comprehensive logging for debugging server_id resolution
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- **New Settings**:
|
||||||
|
- `messages.show_sync_messages`: Control whether sync messages are shown to players (default: `true`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.1.7-SNAPSHOT] - 2025-01-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Extended Version Support**: Full compatibility with Minecraft 1.20.4 to 1.21.9
|
||||||
|
- Comprehensive version detection and compatibility checking
|
||||||
|
- Maven build profiles for different Minecraft versions
|
||||||
|
- Enhanced startup version validation
|
||||||
|
- **Enhanced Message System**: Improved localization support
|
||||||
|
- Added parameter support to MessageManager (`get(String key, String... params)`)
|
||||||
|
- Support for both indexed (`{0}`, `{1}`) and named (`{version}`, `{error}`, `{url}`) placeholders
|
||||||
|
- Dynamic message content with variable substitution
|
||||||
|
- **Version Compatibility Fixes**: Robust ItemStack deserialization
|
||||||
|
- Safe deserialization methods (`safeItemStackArrayFromBase64`, `safeItemStackFromBase64`)
|
||||||
|
- Graceful handling of version-incompatible ItemStack data
|
||||||
|
- Individual item error handling to prevent complete deserialization failures
|
||||||
|
- Fallback mechanisms for corrupted or incompatible data
|
||||||
|
- **Enhanced Update Checker**: Improved console messaging
|
||||||
|
- Localized update checker messages in German and English
|
||||||
|
- Better error handling with specific error messages
|
||||||
|
- Configurable update checking with proper console feedback
|
||||||
|
- Dynamic content in update notifications (version numbers, URLs)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Java Version**: Upgraded from Java 17 to Java 21 for optimal performance
|
||||||
|
- **Minecraft Version**: Updated default target to Minecraft 1.21
|
||||||
|
- **Plugin Metadata**: Enhanced plugin.yml with proper `authors` array and `load: STARTUP`
|
||||||
|
- **Version Compatibility**: Comprehensive support for 1.20.4 through 1.21.9
|
||||||
|
- **Message Handling**: All hardcoded messages replaced with localized MessageManager calls
|
||||||
|
- **Error Recovery**: Better handling of version compatibility issues
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Critical Version Compatibility**: Fixed "Newer version! Server downgrades are not supported!" errors
|
||||||
|
- ItemStack deserialization now handles version mismatches gracefully
|
||||||
|
- Individual items that can't be deserialized are skipped instead of crashing
|
||||||
|
- Empty arrays returned as fallback for completely failed deserialization
|
||||||
|
- **MessageManager Compilation**: Fixed "Method get cannot be applied to given types" errors
|
||||||
|
- Added overloaded `get` method with parameter support
|
||||||
|
- Proper parameter replacement for dynamic content
|
||||||
|
- Backward compatibility with existing `get(String key)` method
|
||||||
|
- **Update Checker Messages**: Fixed missing console messages
|
||||||
|
- All update checker events now display proper localized messages
|
||||||
|
- Dynamic content (version numbers, URLs) properly integrated
|
||||||
|
- Better error reporting for different failure scenarios
|
||||||
|
- **Database Loading**: Enhanced error handling for corrupted inventory data
|
||||||
|
- Safe deserialization prevents server crashes from version issues
|
||||||
|
- Partial data recovery when some items are incompatible
|
||||||
|
- Better logging for version compatibility issues
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- **Data Validation**: Enhanced ItemStack validation and sanitization
|
||||||
|
- **Error Handling**: Graceful degradation for corrupted or incompatible data
|
||||||
|
- **Version Safety**: Protection against version-related crashes
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **Memory Efficiency**: Better handling of large ItemStack arrays
|
||||||
|
- **Error Recovery**: Faster recovery from deserialization failures
|
||||||
|
- **Resource Management**: Improved cleanup of failed operations
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
- **Minecraft 1.20.4**: Full support confirmed
|
||||||
|
- **Minecraft 1.20.5**: Full support confirmed
|
||||||
|
- **Minecraft 1.20.6**: Full support confirmed
|
||||||
|
- **Minecraft 1.21.0**: Full support confirmed
|
||||||
|
- **Minecraft 1.21.1**: Full support confirmed
|
||||||
|
- **Minecraft 1.21.2**: Full support confirmed
|
||||||
|
- **Minecraft 1.21.3**: Full support confirmed
|
||||||
|
- **Minecraft 1.21.4**: Full support confirmed
|
||||||
|
- **Minecraft 1.21.5**: Full support confirmed
|
||||||
|
- **Minecraft 1.21.6**: Full support confirmed
|
||||||
|
- **Minecraft 1.21.7**: Full support confirmed
|
||||||
|
- **Minecraft 1.21.8**: Full support confirmed
|
||||||
|
- **Minecraft 1.21.9**: Full support confirmed
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- **New Messages**:
|
||||||
|
- `loaded`: "Player data loaded successfully" / "Spielerdaten erfolgreich geladen"
|
||||||
|
- `load_failed`: "Failed to load player data" / "Fehler beim Laden der Spielerdaten"
|
||||||
|
- `update_check_disabled`: "Update checking is disabled" / "Update-Prüfung ist deaktiviert"
|
||||||
|
- `update_check_timeout`: "Update check timed out" / "Update-Prüfung ist abgelaufen"
|
||||||
|
- `update_check_no_internet`: "No internet connection for update check" / "Keine Internetverbindung für Update-Prüfung"
|
||||||
|
- `update_download_url`: "Download at: {url}" / "Download unter: {url}"
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
- **Enhanced Commands**:
|
||||||
|
- All commands now use localized messages with parameter support
|
||||||
|
- Better error reporting with dynamic content
|
||||||
|
- Improved user feedback for all operations
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
- **Build System**: Maven profiles for different Minecraft versions
|
||||||
|
- `mvn package -Pmc-1.20.4` for Minecraft 1.20.4 (Java 17)
|
||||||
|
- `mvn package -Pmc-1.21` for Minecraft 1.21 (Java 21) - Default
|
||||||
|
- `mvn package -Pmc-1.21.1` for Minecraft 1.21.1 (Java 21)
|
||||||
|
- **Code Quality**: Enhanced error handling and parameter validation
|
||||||
|
- **Resource Management**: Better cleanup and memory management
|
||||||
|
- **Exception Handling**: More specific error messages and recovery mechanisms
|
||||||
|
- **Debugging**: Enhanced diagnostic information and version compatibility logging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Backup System**: Complete backup and restore functionality
|
||||||
|
- Automatic daily backups with configurable retention
|
||||||
|
- Manual backup creation via `/sync backup create`
|
||||||
|
- Backup restoration via `/sync restore <player> [backup_id]`
|
||||||
|
- SQL dump generation for database backups
|
||||||
|
- ZIP compression for file backups
|
||||||
|
- Backup listing and management commands
|
||||||
|
- **Performance Caching**: In-memory player data cache
|
||||||
|
- Configurable cache size and TTL (Time-To-Live)
|
||||||
|
- LRU (Least Recently Used) eviction policy
|
||||||
|
- Optional compression for memory optimization
|
||||||
|
- Cache statistics and management via `/sync cache`
|
||||||
|
- **Enhanced Performance Monitoring**
|
||||||
|
- Detailed save/load time tracking
|
||||||
|
- Connection pool statistics
|
||||||
|
- Performance metrics logging
|
||||||
|
- Slow operation detection and warnings
|
||||||
|
- **Improved Update Checker**
|
||||||
|
- Configurable timeout settings
|
||||||
|
- Better error handling for network issues
|
||||||
|
- Download link in update notifications
|
||||||
|
- User-Agent header for API requests
|
||||||
|
- **Emergency Configuration System**
|
||||||
|
- Automatic config file creation if missing
|
||||||
|
- Fallback configuration generation
|
||||||
|
- Debug information for configuration issues
|
||||||
|
- Multi-layer configuration loading approach
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Achievement Sync Limits**: Increased limits for Minecraft's 1000+ achievements
|
||||||
|
- `MAX_COUNT_ATTEMPTS`: 1000 → 2000
|
||||||
|
- `MAX_ACHIEVEMENTS`: 1000 → 2000
|
||||||
|
- `MAX_PROCESSED`: 2000 → 3000
|
||||||
|
- Large amount warning threshold: 500 → 1500
|
||||||
|
- **Database Performance**: Enhanced connection pooling
|
||||||
|
- Exponential backoff for connection acquisition
|
||||||
|
- Increased total timeout to 10 seconds
|
||||||
|
- Better connection pool exhaustion handling
|
||||||
|
- **Inventory Utilities**: Improved ItemStack handling
|
||||||
|
- Integrated sanitization and validation
|
||||||
|
- Better corruption data handling
|
||||||
|
- Enhanced Base64 serialization/deserialization
|
||||||
|
- **Configuration Management**: Robust config loading
|
||||||
|
- Multiple fallback mechanisms
|
||||||
|
- Emergency configuration creation
|
||||||
|
- Better error diagnostics
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Critical Achievement Bug**: Fixed infinite loop in achievement counting
|
||||||
|
- Added hard limits to prevent server freezes
|
||||||
|
- Implemented timeout-based processing
|
||||||
|
- Better error handling and logging
|
||||||
|
- **Database Parameter Error**: Fixed "No value specified for parameter 20" error
|
||||||
|
- Corrected SQL REPLACE INTO statement
|
||||||
|
- Added missing `advancements` and `server_id` parameters
|
||||||
|
- Fixed PreparedStatement parameter setting
|
||||||
|
- **Slow Save Detection**: Optimized achievement serialization
|
||||||
|
- Reduced processing time for large achievement counts
|
||||||
|
- Added performance monitoring
|
||||||
|
- Implemented batch processing with timeouts
|
||||||
|
- **Configuration Loading**: Fixed empty config.yml issue
|
||||||
|
- Added multiple fallback mechanisms
|
||||||
|
- Emergency configuration generation
|
||||||
|
- Better resource loading handling
|
||||||
|
- **Compilation Errors**: Fixed missing imports
|
||||||
|
- Added `java.io.File` and `java.io.FileWriter` imports
|
||||||
|
- Resolved Date ambiguity in BackupManager
|
||||||
|
- Fixed try-catch block nesting issues
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- **Enhanced Data Validation**: Improved ItemStack sanitization
|
||||||
|
- **Audit Logging**: Better security event tracking
|
||||||
|
- **Error Recovery**: Graceful handling of corrupted data
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **Memory Optimization**: Cache compression and TTL management
|
||||||
|
- **Database Efficiency**: Connection pooling and batch processing
|
||||||
|
- **Async Operations**: Non-blocking backup and save operations
|
||||||
|
- **Resource Management**: Better memory and connection cleanup
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
- **Minecraft 1.21.5**: Full support for Paper 1.21.5
|
||||||
|
- **Legacy Support**: Maintained compatibility with 1.20.x
|
||||||
|
- **API Compatibility**: Proper handling of version differences
|
||||||
|
- **Database Compatibility**: Support for MySQL, SQLite, and PostgreSQL
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- **New Settings**:
|
||||||
|
- `performance.cache_ttl`: Cache time-to-live in milliseconds
|
||||||
|
- `performance.cache_compression`: Enable cache compression
|
||||||
|
- `update_checker.timeout`: Connection timeout for update checks
|
||||||
|
- `performance.max_achievements_per_player`: Increased to 2000
|
||||||
|
- `server.id`: Unique server identifier for multi-server setups
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
- **New Commands**:
|
||||||
|
- `/sync backup create`: Create manual backup
|
||||||
|
- `/sync restore <player> [backup_id]`: Restore from backup
|
||||||
|
- `/sync cache clear`: Clear performance statistics
|
||||||
|
- **Enhanced Commands**:
|
||||||
|
- `/sync cache`: Now shows performance and connection pool stats
|
||||||
|
- `/sync status`: Improved status reporting
|
||||||
|
- `/sync reload`: Better configuration reload handling
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
- **Code Quality**: Improved error handling and logging
|
||||||
|
- **Resource Management**: Better cleanup and memory management
|
||||||
|
- **Exception Handling**: More specific error messages and recovery
|
||||||
|
- **Debugging**: Enhanced diagnostic information and logging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.1.4] - 2024-12-XX
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release with basic player data synchronization
|
||||||
|
- MySQL and SQLite database support
|
||||||
|
- Basic configuration system
|
||||||
|
- Player event handling (join, quit, world change, death)
|
||||||
|
- Sync command with basic functionality
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Coordinate synchronization
|
||||||
|
- Experience (XP) synchronization
|
||||||
|
- Gamemode synchronization
|
||||||
|
- Inventory synchronization
|
||||||
|
- Enderchest synchronization
|
||||||
|
- Armor synchronization
|
||||||
|
- Offhand synchronization
|
||||||
|
- Health and hunger synchronization
|
||||||
|
- Potion effects synchronization
|
||||||
|
- Achievements synchronization
|
||||||
|
- Statistics synchronization
|
||||||
|
- Attributes synchronization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.1.3] - 2024-12-XX
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Various bug fixes and stability improvements
|
||||||
|
- Database connection handling improvements
|
||||||
|
- Better error messages and logging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.1.2] - 2024-12-XX
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Configuration loading issues
|
||||||
|
- Database parameter errors
|
||||||
|
- Achievement sync problems
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.1.1] - 2024-12-XX
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Initial bug fixes and improvements
|
||||||
|
- Better error handling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.1.0] - 2024-12-XX
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial stable release
|
||||||
|
- Core synchronization features
|
||||||
|
- Basic configuration system
|
||||||
|
- Database support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.0.0] - 2024-12-XX
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial development release
|
||||||
|
- Basic plugin structure
|
||||||
|
- Core functionality implementation
|
||||||
30
LICENSE
30
LICENSE
@@ -1,18 +1,22 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 DerGamer09
|
Copyright (c) 2025 DerGamer009
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
in the Software without restriction, including without limitation the rights
|
||||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
following conditions:
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
The above copyright notice and this permission notice shall be included in all
|
||||||
portions of the Software.
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
||||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
||||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
||||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|||||||
239
PREMIUM_INTEGRATION.md
Normal file
239
PREMIUM_INTEGRATION.md
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
# PlayerDataSync Premium - Integration Guide
|
||||||
|
|
||||||
|
## Übersicht / Overview
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt, wie die Premium-Komponenten (Lizenz-Validierung und Update-Check) in PlayerDataSync Premium integriert werden.
|
||||||
|
|
||||||
|
This document describes how to integrate the premium components (license validation and update check) into PlayerDataSync Premium.
|
||||||
|
|
||||||
|
## Komponenten / Components
|
||||||
|
|
||||||
|
### 1. LicenseValidator
|
||||||
|
**Pfad / Path:** `com.example.playerdatasync.premium.api.LicenseValidator`
|
||||||
|
|
||||||
|
**Funktion / Function:**
|
||||||
|
- Validiert Lizenzschlüssel gegen die CraftingStudio Pro API
|
||||||
|
- Validates license keys against the CraftingStudio Pro API
|
||||||
|
- Caching von Validierungsergebnissen (30 Minuten)
|
||||||
|
- Caches validation results (30 minutes)
|
||||||
|
|
||||||
|
**API Endpoint:**
|
||||||
|
```
|
||||||
|
POST https://craftingstudiopro.de/api/license/validate
|
||||||
|
Body: { "licenseKey": "YOUR-KEY", "pluginId": "playerdatasync-premium" }
|
||||||
|
Response: { "valid": boolean, "message": string, "purchase": {...} }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. PremiumUpdateChecker
|
||||||
|
**Pfad / Path:** `com.example.playerdatasync.premium.api.PremiumUpdateChecker`
|
||||||
|
|
||||||
|
**Funktion / Function:**
|
||||||
|
- Prüft auf Updates über die CraftingStudio Pro API
|
||||||
|
- Checks for updates via CraftingStudio Pro API
|
||||||
|
- Benachrichtigt OPs über verfügbare Updates
|
||||||
|
- Notifies OPs about available updates
|
||||||
|
|
||||||
|
**API Endpoint:**
|
||||||
|
```
|
||||||
|
GET https://craftingstudiopro.de/api/plugins/playerdatasync-premium/latest
|
||||||
|
Response: { "version": string, "downloadUrl": string, ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. LicenseManager
|
||||||
|
**Pfad / Path:** `com.example.playerdatasync.premium.managers.LicenseManager`
|
||||||
|
|
||||||
|
**Funktion / Function:**
|
||||||
|
- Verwaltet Lizenz-Validierung und Caching
|
||||||
|
- Manages license validation and caching
|
||||||
|
- Periodische Re-Validierung (alle 24 Stunden)
|
||||||
|
- Periodic re-validation (every 24 hours)
|
||||||
|
- Automatische Plugin-Deaktivierung bei ungültiger Lizenz
|
||||||
|
- Automatic plugin disabling on invalid license
|
||||||
|
|
||||||
|
### 4. PremiumIntegration
|
||||||
|
**Pfad / Path:** `com.example.playerdatasync.premium.PremiumIntegration`
|
||||||
|
|
||||||
|
**Funktion / Function:**
|
||||||
|
- Wrapper-Klasse für einfache Integration
|
||||||
|
- Wrapper class for easy integration
|
||||||
|
- Kombiniert LicenseManager und PremiumUpdateChecker
|
||||||
|
- Combines LicenseManager and PremiumUpdateChecker
|
||||||
|
|
||||||
|
## Integration in Hauptklasse / Integration in Main Class
|
||||||
|
|
||||||
|
### Beispiel / Example:
|
||||||
|
|
||||||
|
```java
|
||||||
|
package com.example.playerdatasync.premium.core;
|
||||||
|
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
import com.example.playerdatasync.premium.PremiumIntegration;
|
||||||
|
|
||||||
|
public class PlayerDataSyncPremium extends JavaPlugin {
|
||||||
|
private PremiumIntegration premiumIntegration;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
getLogger().info("Enabling PlayerDataSync Premium...");
|
||||||
|
|
||||||
|
// Save default config
|
||||||
|
saveDefaultConfig();
|
||||||
|
|
||||||
|
// Initialize premium features (license validation)
|
||||||
|
premiumIntegration = new PremiumIntegration(this);
|
||||||
|
if (!premiumIntegration.initialize()) {
|
||||||
|
// License validation failed, plugin will be disabled
|
||||||
|
getLogger().severe("License validation failed. Plugin disabled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only continue if license is valid
|
||||||
|
if (!premiumIntegration.isLicenseValid()) {
|
||||||
|
getLogger().severe("Invalid license. Plugin disabled.");
|
||||||
|
getServer().getPluginManager().disablePlugin(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... rest of your plugin initialization ...
|
||||||
|
|
||||||
|
getLogger().info("PlayerDataSync Premium enabled successfully!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
getLogger().info("Disabling PlayerDataSync Premium...");
|
||||||
|
|
||||||
|
if (premiumIntegration != null) {
|
||||||
|
premiumIntegration.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... rest of shutdown code ...
|
||||||
|
|
||||||
|
getLogger().info("PlayerDataSync Premium disabled successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
public PremiumIntegration getPremiumIntegration() {
|
||||||
|
return premiumIntegration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konfiguration / Configuration
|
||||||
|
|
||||||
|
### config.yml Beispiel / Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# License Configuration
|
||||||
|
license:
|
||||||
|
key: YOUR-LICENSE-KEY-HERE # Your license key from CraftingStudio Pro
|
||||||
|
|
||||||
|
# Update Checker
|
||||||
|
update_checker:
|
||||||
|
enabled: true
|
||||||
|
notify_ops: true
|
||||||
|
timeout: 10000
|
||||||
|
|
||||||
|
# Premium Features
|
||||||
|
premium:
|
||||||
|
revalidation_interval_hours: 24
|
||||||
|
cache_validation: true
|
||||||
|
enable_premium_features: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Verwendung / API Usage
|
||||||
|
|
||||||
|
### Lizenz validieren / Validate License:
|
||||||
|
|
||||||
|
```java
|
||||||
|
LicenseValidator validator = new LicenseValidator(plugin);
|
||||||
|
|
||||||
|
// Asynchron
|
||||||
|
CompletableFuture<LicenseValidationResult> future =
|
||||||
|
validator.validateLicenseAsync("YOUR-LICENSE-KEY");
|
||||||
|
|
||||||
|
future.thenAccept(result -> {
|
||||||
|
if (result.isValid()) {
|
||||||
|
// License is valid
|
||||||
|
PurchaseInfo purchase = result.getPurchase();
|
||||||
|
// Use purchase information
|
||||||
|
} else {
|
||||||
|
// License is invalid
|
||||||
|
String message = result.getMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Synchron (blockiert Thread)
|
||||||
|
LicenseValidationResult result = validator.validateLicense("YOUR-LICENSE-KEY");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update prüfen / Check for Updates:
|
||||||
|
|
||||||
|
```java
|
||||||
|
PremiumUpdateChecker updateChecker = new PremiumUpdateChecker(plugin);
|
||||||
|
updateChecker.check(); // Asynchron
|
||||||
|
```
|
||||||
|
|
||||||
|
### LicenseManager verwenden / Use LicenseManager:
|
||||||
|
|
||||||
|
```java
|
||||||
|
LicenseManager licenseManager = new LicenseManager(plugin);
|
||||||
|
licenseManager.initialize(); // Validiert Lizenz beim Start
|
||||||
|
|
||||||
|
// Prüfen ob Lizenz gültig ist
|
||||||
|
if (licenseManager.isLicenseValid()) {
|
||||||
|
// Plugin kann verwendet werden
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lizenz neu validieren
|
||||||
|
licenseManager.revalidateLicense();
|
||||||
|
|
||||||
|
// Neue Lizenz setzen
|
||||||
|
licenseManager.setLicenseKey("NEW-LICENSE-KEY");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fehlerbehandlung / Error Handling
|
||||||
|
|
||||||
|
### Rate Limiting:
|
||||||
|
Die API hat ein Rate Limit von 100 Requests pro Stunde pro IP.
|
||||||
|
The API has a rate limit of 100 requests per hour per IP.
|
||||||
|
|
||||||
|
Bei Überschreitung wird ein `429 Too Many Requests` Status Code zurückgegeben.
|
||||||
|
On exceeding the limit, a `429 Too Many Requests` status code is returned.
|
||||||
|
|
||||||
|
### Netzwerk-Fehler / Network Errors:
|
||||||
|
- `UnknownHostException`: Keine Internetverbindung / No internet connection
|
||||||
|
- `SocketTimeoutException`: Timeout beim Verbindungsaufbau / Connection timeout
|
||||||
|
- Alle Fehler werden geloggt / All errors are logged
|
||||||
|
|
||||||
|
## Sicherheit / Security
|
||||||
|
|
||||||
|
- **Lizenzschlüssel maskieren**: In Logs werden nur die ersten und letzten 4 Zeichen angezeigt
|
||||||
|
- **License key masking**: Only first and last 4 characters shown in logs
|
||||||
|
- **Caching**: Validierungsergebnisse werden 30 Minuten gecacht
|
||||||
|
- **Caching**: Validation results are cached for 30 minutes
|
||||||
|
- **Re-Validierung**: Automatische Re-Validierung alle 24 Stunden
|
||||||
|
- **Re-validation**: Automatic re-validation every 24 hours
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Lizenz wird nicht akzeptiert / License not accepted:
|
||||||
|
1. Prüfen Sie den Lizenzschlüssel in `config.yml`
|
||||||
|
2. Check license key in `config.yml`
|
||||||
|
3. Stellen Sie sicher, dass die Lizenz für "playerdatasync-premium" gültig ist
|
||||||
|
4. Ensure license is valid for "playerdatasync-premium"
|
||||||
|
5. Prüfen Sie die Logs auf Fehlermeldungen
|
||||||
|
6. Check logs for error messages
|
||||||
|
|
||||||
|
### Update-Check funktioniert nicht / Update check not working:
|
||||||
|
1. Prüfen Sie die Internetverbindung
|
||||||
|
2. Check internet connection
|
||||||
|
3. Prüfen Sie, ob `update_checker.enabled: true` in der Config ist
|
||||||
|
4. Check if `update_checker.enabled: true` in config
|
||||||
|
5. Prüfen Sie die Logs auf Rate-Limit-Fehler
|
||||||
|
6. Check logs for rate limit errors
|
||||||
|
|
||||||
|
## API Dokumentation / API Documentation
|
||||||
|
|
||||||
|
Vollständige API-Dokumentation: https://www.craftingstudiopro.de/docs/api
|
||||||
|
|
||||||
|
Complete API documentation: https://www.craftingstudiopro.de/docs/api
|
||||||
265
PREMIUM_README.md
Normal file
265
PREMIUM_README.md
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
# PlayerDataSync Premium
|
||||||
|
|
||||||
|
## Übersicht / Overview
|
||||||
|
|
||||||
|
**EN:** PlayerDataSync Premium is the premium version of PlayerDataSync with license validation and additional features.
|
||||||
|
**DE:** PlayerDataSync Premium ist die Premium-Version von PlayerDataSync mit Lizenz-Validierung und zusätzlichen Features.
|
||||||
|
|
||||||
|
## 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 / Installation
|
||||||
|
|
||||||
|
### Requirements / Anforderungen
|
||||||
|
|
||||||
|
- **EN:** Minecraft Server 1.8 - 1.21.11
|
||||||
|
- **DE:** Minecraft Server 1.8 - 1.21.11
|
||||||
|
- **EN:** Valid license key from CraftingStudio Pro
|
||||||
|
- **DE:** Gültiger Lizenzschlüssel von CraftingStudio Pro
|
||||||
|
- **EN:** Internet connection for license validation
|
||||||
|
- **DE:** Internetverbindung für Lizenz-Validierung
|
||||||
|
|
||||||
|
### Setup / Einrichtung
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
## Configuration / Konfiguration
|
||||||
|
|
||||||
|
### License Configuration / Lizenz-Konfiguration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
license:
|
||||||
|
key: YOUR-LICENSE-KEY-HERE # Your license key from CraftingStudio Pro
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Checker Configuration / Update-Checker-Konfiguration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
update_checker:
|
||||||
|
enabled: true # Enable automatic update checking
|
||||||
|
notify_ops: true # Notify operators when updates are available
|
||||||
|
timeout: 10000 # Timeout in milliseconds
|
||||||
|
```
|
||||||
|
|
||||||
|
### Premium Features Configuration / Premium-Features-Konfiguration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
premium:
|
||||||
|
revalidation_interval_hours: 24 # Revalidate license every 24 hours
|
||||||
|
cache_validation: true # Cache validation results
|
||||||
|
enable_premium_features: true # Enable premium-specific features
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Integration / API-Integration
|
||||||
|
|
||||||
|
### License Validation / Lizenz-Validierung
|
||||||
|
|
||||||
|
**Endpoint:**
|
||||||
|
```
|
||||||
|
POST https://craftingstudiopro.de/api/license/validate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```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 / Update-Prüfung
|
||||||
|
|
||||||
|
**Endpoint:**
|
||||||
|
```
|
||||||
|
GET https://craftingstudiopro.de/api/plugins/playerdatasync-premium/latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.2.9-RELEASE",
|
||||||
|
"downloadUrl": "https://...",
|
||||||
|
"createdAt": "2025-01-01T00:00:00Z",
|
||||||
|
"title": "Release 1.2.9",
|
||||||
|
"releaseType": "release",
|
||||||
|
"pluginTitle": "PlayerDataSync Premium",
|
||||||
|
"pluginSlug": "playerdatasync-premium"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limits / Rate-Limits
|
||||||
|
|
||||||
|
**EN:** The API has a rate limit of 100 requests per hour per IP address.
|
||||||
|
**DE:** Die API hat ein Rate Limit von 100 Anfragen pro Stunde pro IP-Adresse.
|
||||||
|
|
||||||
|
**EN:** If you exceed the limit, you will receive a `429 Too Many Requests` status code.
|
||||||
|
**DE:** Bei Überschreitung erhalten Sie einen `429 Too Many Requests` Status Code.
|
||||||
|
|
||||||
|
**EN:** The plugin uses caching to minimize API calls:
|
||||||
|
- License validation: Cached for 30 minutes
|
||||||
|
- Update checks: Performed on server start and can be triggered manually
|
||||||
|
|
||||||
|
**DE:** Das Plugin verwendet Caching zur Minimierung von API-Aufrufen:
|
||||||
|
- Lizenz-Validierung: 30 Minuten gecacht
|
||||||
|
- Update-Prüfungen: Beim Server-Start und manuell auslösbar
|
||||||
|
|
||||||
|
## Troubleshooting / Fehlerbehebung
|
||||||
|
|
||||||
|
### License Validation Failed / Lizenz-Validierung fehlgeschlagen
|
||||||
|
|
||||||
|
**EN:** **Problem:** License validation fails on startup
|
||||||
|
**DE:** **Problem:** Lizenz-Validierung schlägt beim Start fehl
|
||||||
|
|
||||||
|
**EN:** **Solutions:**
|
||||||
|
1. Check your license key in `config.yml`
|
||||||
|
2. Ensure the license is valid for "playerdatasync-premium"
|
||||||
|
3. Check your internet connection
|
||||||
|
4. Verify the license hasn't expired
|
||||||
|
5. Check server logs for detailed error messages
|
||||||
|
|
||||||
|
**DE:** **Lösungen:**
|
||||||
|
1. Prüfe deinen Lizenzschlüssel in `config.yml`
|
||||||
|
2. Stelle sicher, dass die Lizenz für "playerdatasync-premium" gültig ist
|
||||||
|
3. Prüfe deine Internetverbindung
|
||||||
|
4. Verifiziere, dass die Lizenz nicht abgelaufen ist
|
||||||
|
5. Prüfe die Server-Logs für detaillierte Fehlermeldungen
|
||||||
|
|
||||||
|
### Update Check Not Working / Update-Prüfung funktioniert nicht
|
||||||
|
|
||||||
|
**EN:** **Problem:** Update checker doesn't find updates
|
||||||
|
**DE:** **Problem:** Update-Checker findet keine Updates
|
||||||
|
|
||||||
|
**EN:** **Solutions:**
|
||||||
|
1. Check `update_checker.enabled: true` in config
|
||||||
|
2. Verify internet connection
|
||||||
|
3. Check logs for rate limit errors
|
||||||
|
4. Manually trigger update check: `/sync update`
|
||||||
|
|
||||||
|
**DE:** **Lösungen:**
|
||||||
|
1. Prüfe `update_checker.enabled: true` in der Config
|
||||||
|
2. Verifiziere Internetverbindung
|
||||||
|
3. Prüfe Logs auf Rate-Limit-Fehler
|
||||||
|
4. Manuell Update-Prüfung auslösen: `/sync update`
|
||||||
|
|
||||||
|
### Plugin Disables Itself / Plugin deaktiviert sich selbst
|
||||||
|
|
||||||
|
**EN:** **Problem:** Plugin disables itself after 30 seconds
|
||||||
|
**DE:** **Problem:** Plugin deaktiviert sich nach 30 Sekunden
|
||||||
|
|
||||||
|
**EN:** **Cause:** License validation failed or license is invalid
|
||||||
|
**DE:** **Ursache:** Lizenz-Validierung fehlgeschlagen oder Lizenz ist ungültig
|
||||||
|
|
||||||
|
**EN:** **Solutions:**
|
||||||
|
1. Check license key in config
|
||||||
|
2. Verify license is valid on CraftingStudio Pro
|
||||||
|
3. Check server logs for validation errors
|
||||||
|
4. Contact support if license should be valid
|
||||||
|
|
||||||
|
**DE:** **Lösungen:**
|
||||||
|
1. Prüfe Lizenzschlüssel in der Config
|
||||||
|
2. Verifiziere, dass die Lizenz auf CraftingStudio Pro gültig ist
|
||||||
|
3. Prüfe Server-Logs auf Validierungsfehler
|
||||||
|
4. Kontaktiere Support, wenn die Lizenz gültig sein sollte
|
||||||
|
|
||||||
|
## Commands / Befehle
|
||||||
|
|
||||||
|
### `/sync license validate`
|
||||||
|
**EN:** Manually validate license key
|
||||||
|
**DE:** Lizenzschlüssel manuell validieren
|
||||||
|
|
||||||
|
**Permission:** `playerdatasync.premium.admin`
|
||||||
|
|
||||||
|
### `/sync license info`
|
||||||
|
**EN:** Show license information (masked)
|
||||||
|
**DE:** Zeige Lizenzinformationen (maskiert)
|
||||||
|
|
||||||
|
**Permission:** `playerdatasync.premium.admin`
|
||||||
|
|
||||||
|
### `/sync update check`
|
||||||
|
**EN:** Manually check for updates
|
||||||
|
**DE:** Manuell auf Updates prüfen
|
||||||
|
|
||||||
|
**Permission:** `playerdatasync.premium.admin`
|
||||||
|
|
||||||
|
## Support / Support
|
||||||
|
|
||||||
|
**EN:** For support, please visit:
|
||||||
|
- Website: https://craftingstudiopro.de
|
||||||
|
- Discord: [Join our Discord](https://discord.gg/...)
|
||||||
|
- Documentation: https://www.craftingstudiopro.de/docs/api
|
||||||
|
|
||||||
|
**DE:** Für Support besuche bitte:
|
||||||
|
- Website: https://craftingstudiopro.de
|
||||||
|
- Discord: [Tritt unserem Discord bei](https://discord.gg/...)
|
||||||
|
- Dokumentation: https://www.craftingstudiopro.de/docs/api
|
||||||
|
|
||||||
|
## License / Lizenz
|
||||||
|
|
||||||
|
**EN:** PlayerDataSync Premium requires a valid license key from CraftingStudio Pro.
|
||||||
|
**DE:** PlayerDataSync Premium benötigt einen gültigen Lizenzschlüssel von CraftingStudio Pro.
|
||||||
|
|
||||||
|
**EN:** Without a valid license, the plugin will disable itself after 30 seconds.
|
||||||
|
**DE:** Ohne gültige Lizenz deaktiviert sich das Plugin nach 30 Sekunden.
|
||||||
|
|
||||||
|
## Changelog / Änderungsprotokoll
|
||||||
|
|
||||||
|
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
||||||
|
|
||||||
|
Siehe [CHANGELOG.md](CHANGELOG.md) für Versionshistorie.
|
||||||
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
|
||||||
337
README.md
337
README.md
@@ -1,2 +1,339 @@
|
|||||||
# PlayerDataSync
|
# PlayerDataSync
|
||||||
|
|
||||||
|
A comprehensive Bukkit/Spigot plugin for Minecraft **1.8 to 1.21.11** that synchronizes player data across multiple servers using MySQL, SQLite, or PostgreSQL databases. Perfect for multi-server networks with BungeeCord or Velocity.
|
||||||
|
|
||||||
|
Player inventories, experience, health, achievements, economy balance, and more are stored in a shared database whenever they leave a server and restored when they join again.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- **Full Player Data Sync**: Inventory, EnderChest, Armor, Offhand, Experience, Health, Hunger, Potion Effects
|
||||||
|
- **Multi-Server Support**: Shared database across multiple servers (BungeeCord/Velocity compatible)
|
||||||
|
- **Economy Integration**: Vault economy balance synchronization
|
||||||
|
- **Achievements & Statistics**: Sync player advancements and statistics
|
||||||
|
- **Respawn to Lobby**: Automatically send players to lobby server after death/respawn
|
||||||
|
- **Version Compatibility**: Supports Minecraft 1.8 - 1.21.11 with automatic feature detection
|
||||||
|
- **Performance Optimized**: Async operations, connection pooling, batch processing
|
||||||
|
- **Database Support**: MySQL, SQLite, PostgreSQL
|
||||||
|
|
||||||
|
## 📋 Supported Versions
|
||||||
|
|
||||||
|
This plugin supports Minecraft versions **1.8 to 1.21.11**. Some features are automatically disabled on older versions:
|
||||||
|
- **Offhand sync**: Requires 1.9+
|
||||||
|
- **Attribute sync**: Requires 1.9+
|
||||||
|
- **Advancement sync**: Requires 1.12+
|
||||||
|
|
||||||
|
The plugin automatically detects the server version and enables/disables features accordingly.
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
1. **Download** the latest release from the [releases page](https://github.com/DerGamer009/PlayerDataSync/releases)
|
||||||
|
2. **Place** the jar file in your server's `plugins/` directory
|
||||||
|
3. **Configure** the database connection in `plugins/PlayerDataSync/config.yml`
|
||||||
|
4. **Restart** your server
|
||||||
|
5. **Enable** BungeeCord/Velocity integration if using a proxy network
|
||||||
|
|
||||||
|
## ⚙️ Configuration
|
||||||
|
|
||||||
|
The `config.yml` file contains all configuration options. Here's a basic setup:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Server Configuration
|
||||||
|
server:
|
||||||
|
id: default # Unique identifier for this server instance
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
database:
|
||||||
|
type: mysql # Available options: mysql, sqlite, postgresql
|
||||||
|
mysql:
|
||||||
|
host: localhost
|
||||||
|
port: 3306
|
||||||
|
database: minecraft
|
||||||
|
user: root
|
||||||
|
password: password
|
||||||
|
ssl: false
|
||||||
|
max_connections: 10
|
||||||
|
|
||||||
|
# Player Data Synchronization Settings
|
||||||
|
sync:
|
||||||
|
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: true # Main inventory contents
|
||||||
|
enderchest: true # Ender chest contents
|
||||||
|
armor: true # Equipped armor pieces
|
||||||
|
offhand: true # Offhand item
|
||||||
|
health: true # Current health
|
||||||
|
hunger: true # Hunger and saturation
|
||||||
|
effects: true # Active potion effects
|
||||||
|
achievements: true # Player advancements/achievements
|
||||||
|
statistics: true # Player statistics
|
||||||
|
attributes: true # Player attributes (max health, speed, etc.)
|
||||||
|
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
|
||||||
|
async: true # perform saves asynchronously
|
||||||
|
|
||||||
|
# Integration Settings
|
||||||
|
integrations:
|
||||||
|
bungeecord: false # enable BungeeCord/Velocity support
|
||||||
|
vault: true # enable Vault integration for economy
|
||||||
|
placeholderapi: false # enable PlaceholderAPI support
|
||||||
|
invsee: true # enable InvSee++ style inventory viewing integration
|
||||||
|
openinv: true # enable OpenInv style inventory viewing integration
|
||||||
|
|
||||||
|
# Respawn to Lobby Feature
|
||||||
|
# Sends players to a lobby server after death/respawn
|
||||||
|
# Requires BungeeCord or Velocity integration to be enabled
|
||||||
|
respawn_to_lobby:
|
||||||
|
enabled: false # enable respawn to lobby feature
|
||||||
|
server: lobby # name of the lobby server (must match BungeeCord/Velocity server name)
|
||||||
|
|
||||||
|
# Performance Settings
|
||||||
|
performance:
|
||||||
|
connection_pooling: true # use connection pooling for better performance
|
||||||
|
async_loading: true # load player data asynchronously on join
|
||||||
|
disable_achievement_sync_on_large_amounts: true # disable achievement sync if more than 1500 achievements exist
|
||||||
|
achievement_batch_size: 50 # number of achievements to process in one batch
|
||||||
|
achievement_timeout_ms: 5000 # timeout for achievement serialization (milliseconds)
|
||||||
|
max_achievements_per_player: 2000 # hard limit to prevent infinite loops
|
||||||
|
|
||||||
|
# Message Configuration
|
||||||
|
messages:
|
||||||
|
enabled: true
|
||||||
|
show_sync_messages: true # show sync messages when loading/saving data
|
||||||
|
language: en
|
||||||
|
prefix: "&8[&bPDS&8]"
|
||||||
|
colors: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Respawn to Lobby Feature
|
||||||
|
|
||||||
|
The Respawn to Lobby feature automatically sends players to a designated lobby server after they die and respawn. This is perfect for game servers where players should return to a hub after death.
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
- BungeeCord or Velocity integration must be enabled
|
||||||
|
- The lobby server name must match the server name in your proxy configuration
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
```yaml
|
||||||
|
respawn_to_lobby:
|
||||||
|
enabled: true # Enable the feature
|
||||||
|
server: lobby # Server name from your proxy config
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔨 Building
|
||||||
|
|
||||||
|
This project uses Maven. To build the plugin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/DerGamer009/PlayerDataSync.git
|
||||||
|
cd PlayerDataSync
|
||||||
|
|
||||||
|
# Build for default version (1.21)
|
||||||
|
mvn clean package
|
||||||
|
|
||||||
|
# Build for specific Minecraft version
|
||||||
|
mvn clean package -Pmc-1.8 # Minecraft 1.8 (Java 8)
|
||||||
|
mvn clean package -Pmc-1.9 # Minecraft 1.9-1.16 (Java 8)
|
||||||
|
mvn clean package -Pmc-1.17 # Minecraft 1.17 (Java 16)
|
||||||
|
mvn clean package -Pmc-1.18 # Minecraft 1.18-1.20 (Java 17)
|
||||||
|
mvn clean package -Pmc-1.21 # Minecraft 1.21+ (Java 21)
|
||||||
|
```
|
||||||
|
|
||||||
|
The resulting jar file will be in the `target/` directory.
|
||||||
|
|
||||||
|
The build process uses Maven Shade plugin to bundle required dependencies directly into the final jar, so no additional libraries need to be installed on the server.
|
||||||
|
|
||||||
|
## 🔧 Multi-Server Setup (BungeeCord/Velocity)
|
||||||
|
|
||||||
|
To use PlayerDataSync across multiple servers:
|
||||||
|
|
||||||
|
1. **Configure the same database** on all servers
|
||||||
|
2. **Enable BungeeCord/Velocity integration** in `config.yml`:
|
||||||
|
```yaml
|
||||||
|
integrations:
|
||||||
|
bungeecord: true
|
||||||
|
```
|
||||||
|
3. **Set unique server IDs** for each server (optional, defaults to "default"):
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
id: survival # Different ID for each server
|
||||||
|
```
|
||||||
|
4. **Enable server switching autosave**:
|
||||||
|
```yaml
|
||||||
|
autosave:
|
||||||
|
on_server_switch: true
|
||||||
|
```
|
||||||
|
|
||||||
|
The plugin will automatically save player data when they switch servers and restore it when they join a new server.
|
||||||
|
|
||||||
|
## ⚠️ Performance Considerations
|
||||||
|
|
||||||
|
### Achievement Synchronization
|
||||||
|
|
||||||
|
If you experience server freezing or lag when players join, it may be caused by achievement synchronization. The plugin includes automatic protection, but you should:
|
||||||
|
|
||||||
|
1. **Set `performance.disable_achievement_sync_on_large_amounts: true`** in config.yml
|
||||||
|
2. **Consider setting `sync.achievements: false`** if problems persist
|
||||||
|
3. **Monitor server logs** for timeout warnings
|
||||||
|
|
||||||
|
For servers with many achievements (1500+), the plugin automatically:
|
||||||
|
- Disables sync if too many achievements exist
|
||||||
|
- Processes achievements in batches
|
||||||
|
- Loads asynchronously to avoid blocking the main thread
|
||||||
|
|
||||||
|
### Recommended Performance Settings
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
performance:
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disabling Achievement Sync
|
||||||
|
|
||||||
|
If you experience performance issues, you can disable achievement synchronization entirely:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
sync:
|
||||||
|
achievements: false # Disable achievement sync to prevent lag
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Known Issues & Fixes
|
||||||
|
|
||||||
|
### Fixed Issues
|
||||||
|
|
||||||
|
- ✅ **Issue #45 - XP Sync**: Fixed experience synchronization not working across versions 1.8-1.21.11
|
||||||
|
- ✅ **Issue #46 - Vault Balance de-sync**: Fixed economy balance not being saved on server shutdown
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
**Server Freezing:**
|
||||||
|
1. Set `sync.achievements: false` in config.yml
|
||||||
|
2. Enable performance settings (see above)
|
||||||
|
3. Check server logs for timeout warnings
|
||||||
|
|
||||||
|
**Economy Balance Not Syncing:**
|
||||||
|
1. Ensure Vault and an economy plugin are installed
|
||||||
|
2. Check that `sync.economy: true` in config.yml
|
||||||
|
3. Verify Vault integration is enabled: `integrations.vault: true`
|
||||||
|
|
||||||
|
**Players Not Switching Servers:**
|
||||||
|
1. Enable BungeeCord/Velocity integration: `integrations.bungeecord: true`
|
||||||
|
2. Verify server names match your proxy configuration
|
||||||
|
3. Check that `autosave.on_server_switch: true` is enabled
|
||||||
|
|
||||||
|
**Data Not Syncing:**
|
||||||
|
1. Verify database connection settings
|
||||||
|
2. Check server logs for database errors
|
||||||
|
3. Ensure all servers use the same database
|
||||||
|
4. Verify server IDs are set correctly (if using multiple servers)
|
||||||
|
|
||||||
|
## 📝 Version Compatibility
|
||||||
|
|
||||||
|
### Tested Versions
|
||||||
|
- ✅ **Minecraft 1.8 - 1.21.11**: Full compatibility with automatic feature detection
|
||||||
|
- ✅ **Paper 1.20.4 - 1.21.11**: Full compatibility
|
||||||
|
- ✅ **Spigot 1.8 - 1.21.11**: Full compatibility
|
||||||
|
|
||||||
|
### Compatibility Settings
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
compatibility:
|
||||||
|
safe_attribute_sync: true # Use reflection-based attribute syncing
|
||||||
|
disable_attributes_on_error: false # Auto-disable attributes if errors occur
|
||||||
|
version_check: true # Perform version compatibility checks on startup
|
||||||
|
```
|
||||||
|
|
||||||
|
The plugin automatically detects the server version and enables/disables features accordingly.
|
||||||
|
|
||||||
|
## 🔐 Permissions
|
||||||
|
|
||||||
|
The plugin uses the following permissions:
|
||||||
|
|
||||||
|
- `playerdatasync.*` - All permissions
|
||||||
|
- `playerdatasync.message.show.loading` - Show loading messages
|
||||||
|
- `playerdatasync.message.show.loaded` - Show loaded messages
|
||||||
|
- `playerdatasync.message.show.saving` - Show saving messages
|
||||||
|
- `playerdatasync.message.show.errors` - Show error messages
|
||||||
|
|
||||||
|
## 📊 Metrics
|
||||||
|
|
||||||
|
The plugin uses [bStats](https://bstats.org/) to collect anonymous usage statistics. You can disable this in the config:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
metrics:
|
||||||
|
bstats: false # Disable bStats metrics collection
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
- **GitHub**: https://github.com/DerGamer009/PlayerDataSync
|
||||||
|
- **Issues**: https://github.com/DerGamer009/PlayerDataSync/issues
|
||||||
|
- **Releases**: https://github.com/DerGamer009/PlayerDataSync/releases
|
||||||
|
|
||||||
|
## 📚 Changelog
|
||||||
|
|
||||||
|
See [CHANGELOG.md](CHANGELOG.md) for a detailed list of changes.
|
||||||
|
|
||||||
|
## 💡 Features in Detail
|
||||||
|
|
||||||
|
### Experience & Level Sync
|
||||||
|
- Reliable XP synchronization using `giveExp()` method
|
||||||
|
- Works across all Minecraft versions (1.8-1.21.11)
|
||||||
|
- Automatic verification and correction if XP doesn't match
|
||||||
|
|
||||||
|
### Economy Sync (Vault)
|
||||||
|
- Synchronizes economy balance across servers
|
||||||
|
- Requires Vault and an economy plugin (e.g., EssentialsX, CMI)
|
||||||
|
- Ensures balance is saved on server shutdown
|
||||||
|
|
||||||
|
### Inventory Sync
|
||||||
|
- Full inventory synchronization
|
||||||
|
- EnderChest support
|
||||||
|
- Armor and offhand items
|
||||||
|
- Client synchronization after loading
|
||||||
|
|
||||||
|
### Respawn to Lobby
|
||||||
|
- Automatically sends players to lobby server after death
|
||||||
|
- Uses BungeeCord/Velocity server switching
|
||||||
|
- Saves player data before transfer
|
||||||
|
- Smart detection to prevent unnecessary transfers
|
||||||
|
|
||||||
|
### Database Support
|
||||||
|
- **MySQL**: Full support with connection pooling
|
||||||
|
- **SQLite**: File-based database for single-server setups
|
||||||
|
- **PostgreSQL**: Experimental support
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
If you encounter any issues:
|
||||||
|
1. Check the [Issues](https://github.com/DerGamer009/PlayerDataSync/issues) page
|
||||||
|
2. Review server logs for error messages
|
||||||
|
3. Verify your configuration matches the examples
|
||||||
|
4. Create a new issue with details about your problem
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Made with ❤️ for the Minecraft community**
|
||||||
|
|||||||
381
pom.xml
Normal file
381
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</artifactId>
|
||||||
|
<version>1.2.9-RELEASE</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.libs.bstats</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
<relocation>
|
||||||
|
<pattern>com.zaxxer.hikari</pattern>
|
||||||
|
<shadedPattern>com.example.playerdatasync.libs.hikari</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
<relocation>
|
||||||
|
<pattern>com.google.gson</pattern>
|
||||||
|
<shadedPattern>com.example.playerdatasync.libs.gson</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
<relocation>
|
||||||
|
<pattern>org.apache.commons</pattern>
|
||||||
|
<shadedPattern>com.example.playerdatasync.libs.commons</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
</relocations>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
137
src/main/java/com/example/playerdatasync/api/UpdateChecker.java
Normal file
137
src/main/java/com/example/playerdatasync/api/UpdateChecker.java
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package com.example.playerdatasync.api;
|
||||||
|
|
||||||
|
import com.example.playerdatasync.utils.SchedulerUtils;
|
||||||
|
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;
|
||||||
|
|
||||||
|
import com.example.playerdatasync.managers.MessageManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update checker for PlayerDataSync using CraftingStudio Pro API
|
||||||
|
* API Documentation: https://www.craftingstudiopro.de/docs/api
|
||||||
|
*/
|
||||||
|
public class UpdateChecker {
|
||||||
|
private static final String API_BASE_URL = "https://craftingstudiopro.de/api";
|
||||||
|
private static final String PLUGIN_SLUG = "playerdatasync";
|
||||||
|
|
||||||
|
private final JavaPlugin plugin;
|
||||||
|
private final MessageManager messageManager;
|
||||||
|
|
||||||
|
public UpdateChecker(JavaPlugin plugin, MessageManager messageManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.messageManager = messageManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void check() {
|
||||||
|
// Only check for updates if enabled in config
|
||||||
|
if (!plugin.getConfig().getBoolean("update_checker.enabled", true)) {
|
||||||
|
plugin.getLogger().info(messageManager.get("update_check_disabled"));
|
||||||
|
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) {
|
||||||
|
// Fallback to deprecated constructor if URI parsing fails
|
||||||
|
@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/" + plugin.getDescription().getVersion());
|
||||||
|
connection.setRequestProperty("Accept", "application/json");
|
||||||
|
|
||||||
|
int responseCode = connection.getResponseCode();
|
||||||
|
if (responseCode == 429) {
|
||||||
|
plugin.getLogger().warning(messageManager.get("update_check_failed", "Rate limit exceeded. Please try again later."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseCode != 200) {
|
||||||
|
plugin.getLogger().warning(messageManager.get("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(messageManager.get("update_check_failed", "Empty response"));
|
||||||
|
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(messageManager.get("update_check_failed", "Invalid JSON response: " + e.getMessage()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jsonObject.has("version")) {
|
||||||
|
plugin.getLogger().warning(messageManager.get("update_check_failed", "Invalid response format: missing version field"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String latestVersion = jsonObject.get("version").getAsString();
|
||||||
|
String downloadUrl = null;
|
||||||
|
if (jsonObject.has("downloadUrl") && !jsonObject.get("downloadUrl").isJsonNull()) {
|
||||||
|
downloadUrl = jsonObject.get("downloadUrl").getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
String currentVersion = plugin.getDescription().getVersion();
|
||||||
|
if (currentVersion.equalsIgnoreCase(latestVersion)) {
|
||||||
|
if (plugin.getConfig().getBoolean("update_checker.notify_ops", true)) {
|
||||||
|
plugin.getLogger().info(messageManager.get("update_current"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
plugin.getLogger().info(messageManager.get("update_available", latestVersion));
|
||||||
|
if (downloadUrl != null && !downloadUrl.isEmpty()) {
|
||||||
|
plugin.getLogger().info(messageManager.get("update_download_url", downloadUrl));
|
||||||
|
} else {
|
||||||
|
plugin.getLogger().info(messageManager.get("update_download_url",
|
||||||
|
"https://craftingstudiopro.de/plugins/" + PLUGIN_SLUG));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (java.net.UnknownHostException e) {
|
||||||
|
plugin.getLogger().fine(messageManager.get("update_check_no_internet"));
|
||||||
|
} catch (java.net.SocketTimeoutException e) {
|
||||||
|
plugin.getLogger().warning(messageManager.get("update_check_timeout"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning(messageManager.get("update_check_failed", e.getMessage()));
|
||||||
|
plugin.getLogger().log(java.util.logging.Level.FINE, "Update check error", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,586 @@
|
|||||||
|
package com.example.playerdatasync.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.core.PlayerDataSync;
|
||||||
|
import com.example.playerdatasync.managers.AdvancementSyncManager;
|
||||||
|
import com.example.playerdatasync.managers.BackupManager;
|
||||||
|
import com.example.playerdatasync.managers.MessageManager;
|
||||||
|
import com.example.playerdatasync.utils.InventoryUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced sync command with expanded functionality
|
||||||
|
*/
|
||||||
|
public class SyncCommand implements CommandExecutor, TabCompleter {
|
||||||
|
private final PlayerDataSync plugin;
|
||||||
|
private final MessageManager messageManager;
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
);
|
||||||
|
|
||||||
|
public SyncCommand(PlayerDataSync plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.messageManager = plugin.getMessageManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
|
||||||
|
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.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.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.status")) return true;
|
||||||
|
|
||||||
|
Player target;
|
||||||
|
if (args.length > 1) {
|
||||||
|
if (!hasPermission(sender, "playerdatasync.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.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.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.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.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.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.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.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.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.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1041
src/main/java/com/example/playerdatasync/core/PlayerDataSync.java
Normal file
1041
src/main/java/com/example/playerdatasync/core/PlayerDataSync.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,180 @@
|
|||||||
|
package com.example.playerdatasync.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.core.PlayerDataSync;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple connection pool implementation for PlayerDataSync
|
||||||
|
*/
|
||||||
|
public class ConnectionPool {
|
||||||
|
private final PlayerDataSync 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(PlayerDataSync 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,407 @@
|
|||||||
|
package com.example.playerdatasync.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.core.PlayerDataSync;
|
||||||
|
import com.example.playerdatasync.database.DatabaseManager;
|
||||||
|
import com.example.playerdatasync.managers.MessageManager;
|
||||||
|
import com.example.playerdatasync.utils.OfflinePlayerData;
|
||||||
|
import com.example.playerdatasync.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 PlayerDataSync plugin;
|
||||||
|
private final DatabaseManager databaseManager;
|
||||||
|
private final MessageManager messageManager;
|
||||||
|
private boolean invSeeEnabled;
|
||||||
|
private boolean openInvEnabled;
|
||||||
|
private final ItemStack fillerItem;
|
||||||
|
|
||||||
|
public InventoryViewerIntegrationManager(PlayerDataSync 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, viewer, () -> {
|
||||||
|
if (viewer.isOnline()) {
|
||||||
|
viewer.sendMessage(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OfflinePlayerData finalData = data;
|
||||||
|
SchedulerUtils.runTask(plugin, viewer, () -> {
|
||||||
|
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) {
|
||||||
|
Player viewer = Bukkit.getPlayer(viewerId);
|
||||||
|
if (viewer != null) {
|
||||||
|
SchedulerUtils.runTask(plugin, viewer, () -> {
|
||||||
|
if (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.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.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,247 @@
|
|||||||
|
package com.example.playerdatasync.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.core.PlayerDataSync;
|
||||||
|
import com.example.playerdatasync.database.DatabaseManager;
|
||||||
|
import com.example.playerdatasync.managers.AdvancementSyncManager;
|
||||||
|
import com.example.playerdatasync.managers.MessageManager;
|
||||||
|
import com.example.playerdatasync.utils.SchedulerUtils;
|
||||||
|
|
||||||
|
public class PlayerDataListener implements Listener {
|
||||||
|
private final PlayerDataSync plugin;
|
||||||
|
private final DatabaseManager dbManager;
|
||||||
|
private final MessageManager messageManager;
|
||||||
|
|
||||||
|
public PlayerDataListener(PlayerDataSync 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,101 @@
|
|||||||
|
package com.example.playerdatasync.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.core.PlayerDataSync;
|
||||||
|
import com.example.playerdatasync.database.DatabaseManager;
|
||||||
|
import com.example.playerdatasync.managers.MessageManager;
|
||||||
|
import com.example.playerdatasync.utils.SchedulerUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 PlayerDataSync plugin;
|
||||||
|
private final DatabaseManager databaseManager;
|
||||||
|
private final MessageManager messageManager;
|
||||||
|
|
||||||
|
public ServerSwitchListener(PlayerDataSync 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,332 @@
|
|||||||
|
package com.example.playerdatasync.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.core.PlayerDataSync;
|
||||||
|
import com.example.playerdatasync.utils.SchedulerUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 PlayerDataSync 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(PlayerDataSync 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, () -> 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,346 @@
|
|||||||
|
package com.example.playerdatasync.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.core.PlayerDataSync;
|
||||||
|
import com.example.playerdatasync.utils.SchedulerUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced backup and restore system for PlayerDataSync
|
||||||
|
* Supports automatic backups, compression, and data integrity verification
|
||||||
|
*/
|
||||||
|
public class BackupManager {
|
||||||
|
private final PlayerDataSync plugin;
|
||||||
|
private BukkitTask backupTask;
|
||||||
|
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
|
||||||
|
|
||||||
|
public BackupManager(PlayerDataSync 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.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.core.PlayerDataSync;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration manager for PlayerDataSync
|
||||||
|
* Handles validation, migration, and advanced configuration features
|
||||||
|
*/
|
||||||
|
public class ConfigManager {
|
||||||
|
private final PlayerDataSync plugin;
|
||||||
|
private FileConfiguration config;
|
||||||
|
private File configFile;
|
||||||
|
|
||||||
|
// Configuration version for migration
|
||||||
|
private static final int CURRENT_CONFIG_VERSION = 6;
|
||||||
|
|
||||||
|
public ConfigManager(PlayerDataSync 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,110 @@
|
|||||||
|
package com.example.playerdatasync.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.core.PlayerDataSync;
|
||||||
|
|
||||||
|
public class MessageManager {
|
||||||
|
private final PlayerDataSync plugin;
|
||||||
|
private FileConfiguration messages;
|
||||||
|
|
||||||
|
public MessageManager(PlayerDataSync 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.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.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.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.core.PlayerDataSync;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 PlayerDataSync 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(PlayerDataSync 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,288 @@
|
|||||||
|
package com.example.playerdatasync.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)
|
||||||
|
* Uses GlobalRegionScheduler on Folia for global tasks
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
* Uses RegionScheduler on Folia for player-specific tasks
|
||||||
|
*/
|
||||||
|
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 regionScheduler = Bukkit.getServer().getClass()
|
||||||
|
.getMethod("getRegionScheduler")
|
||||||
|
.invoke(Bukkit.getServer());
|
||||||
|
|
||||||
|
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
|
||||||
|
return Bukkit.getScheduler().runTask(plugin, task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Bukkit.getScheduler().runTask(plugin, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a task asynchronously
|
||||||
|
* Uses AsyncScheduler on Folia for async tasks
|
||||||
|
*/
|
||||||
|
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 globalScheduler = Bukkit.getServer().getClass()
|
||||||
|
.getMethod("getGlobalRegionScheduler")
|
||||||
|
.invoke(Bukkit.getServer());
|
||||||
|
|
||||||
|
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
|
||||||
|
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 regionScheduler = Bukkit.getServer().getClass()
|
||||||
|
.getMethod("getRegionScheduler")
|
||||||
|
.invoke(Bukkit.getServer());
|
||||||
|
|
||||||
|
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
|
||||||
|
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 asyncScheduler = Bukkit.getServer().getClass()
|
||||||
|
.getMethod("getAsyncScheduler")
|
||||||
|
.invoke(Bukkit.getServer());
|
||||||
|
|
||||||
|
// Folia uses milliseconds for async scheduler
|
||||||
|
long delayMs = delay * 50; // Convert ticks to milliseconds
|
||||||
|
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
|
||||||
|
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 globalScheduler = Bukkit.getServer().getClass()
|
||||||
|
.getMethod("getGlobalRegionScheduler")
|
||||||
|
.invoke(Bukkit.getServer());
|
||||||
|
|
||||||
|
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
|
||||||
|
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 asyncScheduler = Bukkit.getServer().getClass()
|
||||||
|
.getMethod("getAsyncScheduler")
|
||||||
|
.invoke(Bukkit.getServer());
|
||||||
|
|
||||||
|
// Folia uses milliseconds for async scheduler
|
||||||
|
long delayMs = delay * 50; // Convert ticks to milliseconds
|
||||||
|
long periodMs = period * 50; // Convert ticks to milliseconds
|
||||||
|
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
|
||||||
|
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.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
166
src/main/resources/config.yml
Normal file
166
src/main/resources/config.yml
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# =====================================
|
||||||
|
# PlayerDataSync Configuration
|
||||||
|
# Compatible with Minecraft 1.8 - 1.21.11
|
||||||
|
# Note: Some features require newer versions:
|
||||||
|
# - Offhand sync: 1.9+
|
||||||
|
# - Attribute sync: 1.9+
|
||||||
|
# - Advancement sync: 1.12+
|
||||||
|
# =====================================
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
server:
|
||||||
|
id: default # Unique identifier for this server instance
|
||||||
|
|
||||||
|
database:
|
||||||
|
type: mysql # Available options: mysql, sqlite, postgresql
|
||||||
|
table_prefix: player_data # 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/playerdata.db
|
||||||
|
|
||||||
|
# PostgreSQL Database Configuration (experimental)
|
||||||
|
postgresql:
|
||||||
|
host: localhost
|
||||||
|
port: 5432
|
||||||
|
database: minecraft
|
||||||
|
user: postgres
|
||||||
|
password: password
|
||||||
|
ssl: false
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Data Management
|
||||||
|
data_management:
|
||||||
|
cleanup:
|
||||||
|
enabled: false # automatically clean old player data
|
||||||
|
days_inactive: 90 # remove data for players inactive for X days
|
||||||
|
backup:
|
||||||
|
enabled: true # create backups of player data
|
||||||
|
interval: 1440 # backup interval in minutes (1440 = daily)
|
||||||
|
keep_backups: 7 # number of backups to keep
|
||||||
|
validation:
|
||||||
|
enabled: true # validate data before saving/loading
|
||||||
|
strict_mode: false # strict validation (may cause issues with custom items)
|
||||||
|
|
||||||
|
# Performance Settings
|
||||||
|
performance:
|
||||||
|
batch_size: 50 # number of players to process in one batch
|
||||||
|
cache_size: 100 # number of player data entries to cache
|
||||||
|
cache_ttl: 300000 # cache time-to-live in milliseconds (5 minutes)
|
||||||
|
cache_compression: true # enable cache compression for memory optimization
|
||||||
|
connection_pooling: true # use connection pooling for better performance
|
||||||
|
async_loading: true # load player data asynchronously on join
|
||||||
|
disable_achievement_sync_on_large_amounts: true # disable achievement sync if more than 1500 achievements exist
|
||||||
|
achievement_batch_size: 50 # number of achievements to process in one batch to prevent lag
|
||||||
|
achievement_timeout_ms: 5000 # timeout for achievement serialization to prevent server freeze (milliseconds)
|
||||||
|
max_achievements_per_player: 2000 # hard limit to prevent infinite loops (increased for Minecraft's 1000+ achievements)
|
||||||
|
preload_advancements_on_startup: true # pre-cache the server's advancement registry on startup
|
||||||
|
advancement_import_batch_size: 250 # number of advancements scanned per tick while caching definitions
|
||||||
|
player_advancement_import_batch_size: 150 # number of advancements checked per tick when importing a player
|
||||||
|
automatic_player_advancement_import: true # automatically queue imports for players without cached advancement data
|
||||||
|
|
||||||
|
# Compatibility Settings
|
||||||
|
compatibility:
|
||||||
|
safe_attribute_sync: true # use reflection-based attribute syncing for better version compatibility
|
||||||
|
disable_attributes_on_error: false # automatically disable attribute sync if errors occur
|
||||||
|
version_check: true # perform version compatibility checks on startup
|
||||||
|
legacy_1_20_support: true # enable additional compatibility features for Minecraft 1.20.x
|
||||||
|
modern_1_21_support: true # enable additional compatibility features for Minecraft 1.21.x
|
||||||
|
disable_achievements_on_critical_error: true # automatically disable achievement sync on critical errors to prevent server freeze
|
||||||
|
|
||||||
|
# Security Settings
|
||||||
|
security:
|
||||||
|
encrypt_data: false # encrypt sensitive data in database
|
||||||
|
hash_uuids: false # hash player UUIDs for privacy
|
||||||
|
audit_log: true # log all data operations
|
||||||
|
|
||||||
|
# Integration Settings
|
||||||
|
integrations:
|
||||||
|
bungeecord: false # enable BungeeCord/Velocity support
|
||||||
|
luckperms: false # enable LuckPerms integration
|
||||||
|
vault: true # enable Vault integration for economy
|
||||||
|
placeholderapi: false # enable PlaceholderAPI support
|
||||||
|
invsee: true # enable InvSee++ style inventory viewing integration
|
||||||
|
openinv: true # enable OpenInv style inventory viewing integration
|
||||||
|
|
||||||
|
# Respawn to Lobby Feature
|
||||||
|
# Sends players to a lobby server after death/respawn
|
||||||
|
# Requires BungeeCord or Velocity integration to be enabled
|
||||||
|
respawn_to_lobby:
|
||||||
|
enabled: false # enable respawn to lobby feature
|
||||||
|
server: lobby # name of the lobby server (must match BungeeCord/Velocity server name)
|
||||||
|
|
||||||
|
# Message Configuration
|
||||||
|
messages:
|
||||||
|
enabled: true # enable player messages
|
||||||
|
show_sync_messages: true # show sync messages when loading/saving data (set to false to disable all sync notifications)
|
||||||
|
language: en # default language (en, de, fr, es, etc.)
|
||||||
|
prefix: "&8[&bPDS&8]" # message prefix
|
||||||
|
colors: true # enable color codes in messages
|
||||||
|
|
||||||
|
# Logging and Debugging
|
||||||
|
logging:
|
||||||
|
level: INFO # Log level: DEBUG, INFO, WARN, ERROR (also accepts WARNING/SEVERE/OFF)
|
||||||
|
log_database: false # log database operations
|
||||||
|
log_performance: false # log performance metrics
|
||||||
|
debug_mode: false # enable verbose debug messages (set to true only when troubleshooting)
|
||||||
|
|
||||||
|
# Update Checker
|
||||||
|
update_checker:
|
||||||
|
enabled: true # check for plugin updates
|
||||||
|
notify_ops: true # notify operators about updates
|
||||||
|
auto_download: false # automatically download updates (not recommended)
|
||||||
|
timeout: 10000 # connection timeout in milliseconds
|
||||||
|
|
||||||
|
# Metrics and Analytics
|
||||||
|
metrics:
|
||||||
|
bstats: true # Enable bStats metrics collection
|
||||||
|
custom_metrics: true # Enable custom plugin metrics
|
||||||
182
src/main/resources/messages_de.yml
Normal file
182
src/main/resources/messages_de.yml
Normal file
@@ -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}"
|
||||||
181
src/main/resources/messages_en.yml
Normal file
181
src/main/resources/messages_en.yml
Normal file
@@ -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}"
|
||||||
197
src/main/resources/plugin.yml
Normal file
197
src/main/resources/plugin.yml
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
name: PlayerDataSync
|
||||||
|
main: com.example.playerdatasync.core.PlayerDataSync
|
||||||
|
version: ${project.version}
|
||||||
|
api-version: "1.13"
|
||||||
|
load: STARTUP
|
||||||
|
authors: [DerGamer09]
|
||||||
|
description: Advanced player data synchronization plugin for Minecraft servers
|
||||||
|
website: https://github.com/DerGamer09/PlayerDataSync
|
||||||
|
softdepend: [Vault, LuckPerms, PlaceholderAPI]
|
||||||
|
|
||||||
|
commands:
|
||||||
|
sync:
|
||||||
|
description: View or change sync options
|
||||||
|
usage: /<command> [<option> <true|false>] | /<command> reload | /<command> status | /<command> save [player]
|
||||||
|
permission: playerdatasync.admin
|
||||||
|
aliases: [pds, playerdatasync]
|
||||||
|
|
||||||
|
pdsstatus:
|
||||||
|
description: Check PlayerDataSync status and statistics
|
||||||
|
usage: /<command> [player]
|
||||||
|
permission: playerdatasync.status
|
||||||
|
aliases: [pdsstats]
|
||||||
|
|
||||||
|
pdsbackup:
|
||||||
|
description: Manage player data backups
|
||||||
|
usage: /<command> create | /<command> restore <player> [backup_id] | /<command> list [player]
|
||||||
|
permission: playerdatasync.backup
|
||||||
|
|
||||||
|
pdsimport:
|
||||||
|
description: Import player data from other plugins or formats
|
||||||
|
usage: /<command> <format> [options]
|
||||||
|
permission: playerdatasync.import
|
||||||
|
|
||||||
|
pdsexport:
|
||||||
|
description: Export player data to various formats
|
||||||
|
usage: /<command> <format> [player] [options]
|
||||||
|
permission: playerdatasync.export
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
# Administrative permissions
|
||||||
|
playerdatasync.admin.*:
|
||||||
|
description: Allows all PlayerDataSync admin commands and features
|
||||||
|
default: op
|
||||||
|
children:
|
||||||
|
playerdatasync.admin.coordinates: true
|
||||||
|
playerdatasync.admin.position: true
|
||||||
|
playerdatasync.admin.xp: true
|
||||||
|
playerdatasync.admin.gamemode: true
|
||||||
|
playerdatasync.admin.enderchest: true
|
||||||
|
playerdatasync.admin.inventory: true
|
||||||
|
playerdatasync.admin.armor: true
|
||||||
|
playerdatasync.admin.offhand: true
|
||||||
|
playerdatasync.admin.health: true
|
||||||
|
playerdatasync.admin.hunger: true
|
||||||
|
playerdatasync.admin.effects: true
|
||||||
|
playerdatasync.admin.achievements: true
|
||||||
|
playerdatasync.admin.statistics: true
|
||||||
|
playerdatasync.admin.attributes: true
|
||||||
|
playerdatasync.admin.permissions: true
|
||||||
|
playerdatasync.admin.economy: true
|
||||||
|
playerdatasync.admin.editor: true
|
||||||
|
playerdatasync.admin.reload: true
|
||||||
|
playerdatasync.admin.save: true
|
||||||
|
playerdatasync.integration.invsee: true
|
||||||
|
playerdatasync.integration.enderchest: true
|
||||||
|
playerdatasync.backup: true
|
||||||
|
playerdatasync.import: true
|
||||||
|
playerdatasync.export: true
|
||||||
|
playerdatasync.status: true
|
||||||
|
|
||||||
|
# Individual sync feature permissions
|
||||||
|
playerdatasync.admin.coordinates:
|
||||||
|
description: Allows toggling coordinate synchronization
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.position:
|
||||||
|
description: Allows toggling position synchronization
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.xp:
|
||||||
|
description: Allows toggling experience synchronization
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.gamemode:
|
||||||
|
description: Allows toggling gamemode synchronization
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.enderchest:
|
||||||
|
description: Allows toggling enderchest synchronization
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.inventory:
|
||||||
|
description: Allows toggling inventory synchronization
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.armor:
|
||||||
|
description: Allows toggling armor synchronization
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.offhand:
|
||||||
|
description: Allows toggling offhand synchronization
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.health:
|
||||||
|
description: Allows toggling health synchronization
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.hunger:
|
||||||
|
description: Allows toggling hunger synchronization
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.effects:
|
||||||
|
description: Allows toggling potion effects synchronization
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.achievements:
|
||||||
|
description: Allows toggling achievements synchronization
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.statistics:
|
||||||
|
description: Allows toggling statistics synchronization
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.attributes:
|
||||||
|
description: Allows toggling attributes synchronization
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.permissions:
|
||||||
|
description: Allows toggling permissions synchronization
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.economy:
|
||||||
|
description: Allows toggling economy synchronization
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.editor:
|
||||||
|
description: Allows managing the web editor integration and requesting tokens
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.reload:
|
||||||
|
description: Allows reloading the plugin configuration
|
||||||
|
default: op
|
||||||
|
playerdatasync.admin.save:
|
||||||
|
description: Allows manually saving player data
|
||||||
|
default: op
|
||||||
|
|
||||||
|
# Status and monitoring permissions
|
||||||
|
playerdatasync.status:
|
||||||
|
description: Allows checking plugin status and statistics
|
||||||
|
default: op
|
||||||
|
playerdatasync.status.others:
|
||||||
|
description: Allows checking other players' sync status
|
||||||
|
default: op
|
||||||
|
|
||||||
|
# Backup management permissions
|
||||||
|
playerdatasync.backup:
|
||||||
|
description: Allows managing player data backups
|
||||||
|
default: op
|
||||||
|
playerdatasync.backup.create:
|
||||||
|
description: Allows creating backups
|
||||||
|
default: op
|
||||||
|
playerdatasync.backup.restore:
|
||||||
|
description: Allows restoring from backups
|
||||||
|
default: op
|
||||||
|
playerdatasync.backup.others:
|
||||||
|
description: Allows managing other players' backups
|
||||||
|
default: op
|
||||||
|
|
||||||
|
# Import/Export permissions
|
||||||
|
playerdatasync.import:
|
||||||
|
description: Allows importing player data
|
||||||
|
default: op
|
||||||
|
playerdatasync.export:
|
||||||
|
description: Allows exporting player data
|
||||||
|
default: op
|
||||||
|
|
||||||
|
# Message permissions
|
||||||
|
playerdatasync.message.show.loading:
|
||||||
|
description: Allows player to see loading messages
|
||||||
|
default: true
|
||||||
|
playerdatasync.message.show.saving:
|
||||||
|
description: Allows player to see saving messages
|
||||||
|
default: true
|
||||||
|
playerdatasync.message.show.errors:
|
||||||
|
description: Allows player to see error messages
|
||||||
|
default: true
|
||||||
|
playerdatasync.message.show.sync:
|
||||||
|
description: Allows player to see sync notifications
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# Bypass permissions
|
||||||
|
playerdatasync.bypass.sync:
|
||||||
|
description: Bypass automatic data synchronization
|
||||||
|
default: false
|
||||||
|
playerdatasync.bypass.autosave:
|
||||||
|
description: Bypass automatic saves
|
||||||
|
default: false
|
||||||
|
playerdatasync.bypass.validation:
|
||||||
|
description: Bypass data validation checks
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# Integration permissions
|
||||||
|
playerdatasync.integration.vault:
|
||||||
|
description: Allow Vault economy integration
|
||||||
|
default: false
|
||||||
|
playerdatasync.integration.luckperms:
|
||||||
|
description: Allow LuckPerms integration
|
||||||
|
default: false
|
||||||
|
playerdatasync.integration.invsee:
|
||||||
|
description: Allows viewing and editing stored player inventories through InvSee/OpenInv integration
|
||||||
|
default: op
|
||||||
|
playerdatasync.integration.enderchest:
|
||||||
|
description: Allows viewing and editing stored player ender chests through InvSee/OpenInv integration
|
||||||
|
default: op
|
||||||
Reference in New Issue
Block a user