Minecraft Storage Plugin for PaperMC (works like a backpack)
This commit is contained in:
commit
a8524ccc47
12 changed files with 743 additions and 0 deletions
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
*.jar
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# IntelliJ IDEA
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
out/
|
||||||
|
|
||||||
|
# Eclipse
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
bin/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# NetBeans
|
||||||
|
nbproject/
|
||||||
|
nbbuild/
|
||||||
|
nbdist/
|
||||||
|
.nb-gradle/
|
||||||
|
|
||||||
|
# Logs & databases (lokale Test-Daten)
|
||||||
|
*.log
|
||||||
|
*.db
|
||||||
|
storage.db
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
106
README.md
Normal file
106
README.md
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
# ZalmikStorage
|
||||||
|
|
||||||
|
Ein mehrseitiges, datenbankgestütztes Lagersystem (ähnlich einer erweiterten Enderchest) für Paper/Spigot-Server ab **Minecraft 1.20**.
|
||||||
|
|
||||||
|
Spieler öffnen über `/storage` ein 54-Slot-Inventar mit 45 nutzbaren Slots pro Seite und können per Pfeil-Buttons zwischen Seiten blättern. Die Anzahl der verfügbaren Seiten wird über Permissions gesteuert. Items werden serialisiert in einer Datenbank gespeichert.
|
||||||
|
|
||||||
|
- **Author:** Tronax
|
||||||
|
- **Version:** 1.0
|
||||||
|
- **API:** Paper 1.20
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Mehrseitiges Lager mit Navigation (Vorherige / Nächste Seite)
|
||||||
|
- Pro-Spieler-Speicherung anhand der UUID
|
||||||
|
- Seitenlimit konfigurierbar über Permissions
|
||||||
|
- Unterstützt **SQLite**, **MySQL/MariaDB** und **PostgreSQL** (HikariCP Connection-Pool)
|
||||||
|
- Asynchrones Laden/Speichern, um den Main-Thread zu entlasten
|
||||||
|
|
||||||
|
## Befehle
|
||||||
|
|
||||||
|
| Befehl | Beschreibung | Permission |
|
||||||
|
|------------|---------------------------|----------------------|
|
||||||
|
| `/storage` | Öffnet das eigene Lager | `zalmikstorage.use` |
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
| Permission | Beschreibung | Default |
|
||||||
|
|----------------------------------|-----------------------------------------------------|---------|
|
||||||
|
| `zalmikstorage.use` | Erlaubt das Öffnen des Lagers | `false` |
|
||||||
|
| `zalmikstorage.pages.<n>` | Setzt das Seitenlimit auf `<n>` (z. B. `.5` = 5) | – |
|
||||||
|
| `zalmikstorage.pages.unlimited` | Hebt das Seitenlimit komplett auf | – |
|
||||||
|
|
||||||
|
> Das höchste zutreffende `zalmikstorage.pages.<n>` gewinnt. Ohne eine solche Permission steht **1 Seite** zur Verfügung.
|
||||||
|
|
||||||
|
## Konfiguration (`config.yml`)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
database:
|
||||||
|
type: sqlite # sqlite | mysql | mariadb | postgresql
|
||||||
|
host: "127.0.0.1"
|
||||||
|
port: 3306
|
||||||
|
database: "minecraft"
|
||||||
|
username: "root"
|
||||||
|
password: "password"
|
||||||
|
pool_size: 10
|
||||||
|
timeout: 30000
|
||||||
|
```
|
||||||
|
|
||||||
|
Bei `type: sqlite` wird eine lokale Datei `storage.db` im Plugin-Ordner verwendet (Host/Port/Zugangsdaten werden ignoriert).
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Voraussetzungen: **JDK 21** und **Maven**.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn clean package
|
||||||
|
```
|
||||||
|
|
||||||
|
Das fertige Plugin liegt anschließend unter `target/ZalmikStorage-1.0-SNAPSHOT.jar` und kann in den `plugins/`-Ordner des Servers kopiert werden.
|
||||||
|
|
||||||
|
## Datenbank-Schema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS zalmik_storage (
|
||||||
|
uuid VARCHAR(36) NOT NULL,
|
||||||
|
page INT NOT NULL,
|
||||||
|
items TEXT,
|
||||||
|
PRIMARY KEY (uuid, page)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Items werden als Base64-kodierter `BukkitObjectStream` in der Spalte `items` abgelegt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sicherheit & Stabilität
|
||||||
|
|
||||||
|
Das Plugin wurde einer Code-Prüfung unterzogen. Die dabei gefundenen Logik- und Sicherheitsprobleme sind behoben:
|
||||||
|
|
||||||
|
### Kritisch — behoben
|
||||||
|
|
||||||
|
1. **Fehlende JDBC-Treiber im Build** — `sqlite-jdbc` und `mariadb-java-client` wurden der `pom.xml` hinzugefügt. Die Standard-Konfiguration (`type: sqlite`) funktioniert jetzt out of the box.
|
||||||
|
|
||||||
|
2. **Datenverlust beim Server-Stop** — `onDisable()` speichert alle offenen Lager nun **synchron**, bevor die Datenbankverbindung getrennt und die IO-Queues heruntergefahren werden.
|
||||||
|
|
||||||
|
3. **Race-Condition beim schnellen Öffnen/Schließen** — Laden und Speichern laufen pro Spieler über eine **serielle Single-Thread-Queue** (`ExecutorService`). Dadurch ist FIFO-Reihenfolge garantiert: Ein Ladevorgang kann eine noch ausstehende Speicherung nicht mehr überholen.
|
||||||
|
|
||||||
|
### Hoch — behoben
|
||||||
|
|
||||||
|
4. **Permission-Wert wird ignoriert** — `getMaxPages()` überspringt nun Nodes mit `getValue() == false`. Eine auf `false` gesetzte `zalmikstorage.pages.<n>` gewährt keine Seiten mehr.
|
||||||
|
|
||||||
|
5. **Stiller Fehlerzustand bei DB-Verbindungsfehler** — `connect()` wirft jetzt eine Exception statt sie nur zu loggen; `onEnable()` fängt sie ab und **deaktiviert das Plugin**, statt in einem kaputten Zustand weiterzulaufen.
|
||||||
|
|
||||||
|
### Mittel / Niedrig — behoben
|
||||||
|
|
||||||
|
6. **Unsichere Deserialisierung / DoS** — `ItemSerializer` begrenzt die aus den Daten gelesene Array-Größe (`MAX_ITEMS = 54`) und lehnt ungültige Längen ab, bevor Speicher allokiert wird.
|
||||||
|
|
||||||
|
7. **Unbekannter `database.type`** — wird jetzt mit einer klaren `IllegalArgumentException` abgewiesen, statt mit leerer JDBC-URL zu scheitern.
|
||||||
|
|
||||||
|
8. **`getCommand("storage")` ohne Null-Check** — wird vor `setExecutor` auf `null` geprüft und sauber geloggt.
|
||||||
|
|
||||||
|
### Bereits zuvor solide
|
||||||
|
|
||||||
|
- **Keine SQL-Injection:** Alle Queries nutzen `PreparedStatement`, das Tabellen-Statement ist statisch.
|
||||||
|
- **Permission-Gate** auf `/storage` (`zalmikstorage.use`, Default `false`).
|
||||||
|
- **Navigations-Slots (45–53) sind klick-geschützt** (`event.setCancelled(true)`), wodurch Item-Klau aus der GUI-Leiste verhindert wird.
|
||||||
78
pom.xml
Normal file
78
pom.xml
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>de.plugin</groupId>
|
||||||
|
<artifactId>ZalmikStorage</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>papermc</id>
|
||||||
|
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.papermc.paper</groupId>
|
||||||
|
<artifactId>paper-api</artifactId>
|
||||||
|
<version>1.20.4-R0.1-SNAPSHOT</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.zaxxer</groupId>
|
||||||
|
<artifactId>HikariCP</artifactId>
|
||||||
|
<version>5.1.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<version>42.7.3</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xerial</groupId>
|
||||||
|
<artifactId>sqlite-jdbc</artifactId>
|
||||||
|
<version>3.45.3.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mariadb.jdbc</groupId>
|
||||||
|
<artifactId>mariadb-java-client</artifactId>
|
||||||
|
<version>3.4.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.2</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<relocations>
|
||||||
|
<relocation>
|
||||||
|
<pattern>com.zaxxer.hikari</pattern>
|
||||||
|
<shadedPattern>de.plugin.zalmikstorage.libs.hikari</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
</relocations>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
129
src/main/java/de/plugin/zalmikstorage/DatabaseManager.java
Normal file
129
src/main/java/de/plugin/zalmikstorage/DatabaseManager.java
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
package de.plugin.zalmikstorage;
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
|
||||||
|
public class DatabaseManager {
|
||||||
|
|
||||||
|
private final ZalmikStorage plugin;
|
||||||
|
private HikariDataSource dataSource;
|
||||||
|
private String dbType;
|
||||||
|
|
||||||
|
public DatabaseManager(ZalmikStorage plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void connect() {
|
||||||
|
dbType = plugin.getConfig().getString("database.type", "sqlite").toLowerCase();
|
||||||
|
HikariConfig config = new HikariConfig();
|
||||||
|
|
||||||
|
if (dbType.equals("sqlite")) {
|
||||||
|
File dbFile = new File(plugin.getDataFolder(), "storage.db");
|
||||||
|
config.setJdbcUrl("jdbc:sqlite:" + dbFile.getAbsolutePath());
|
||||||
|
config.setDriverClassName("org.sqlite.JDBC");
|
||||||
|
config.setMaximumPoolSize(1);
|
||||||
|
} else if (dbType.equals("postgresql") || dbType.equals("mariadb") || dbType.equals("mysql")) {
|
||||||
|
String host = plugin.getConfig().getString("database.host");
|
||||||
|
int port = plugin.getConfig().getInt("database.port");
|
||||||
|
String db = plugin.getConfig().getString("database.database");
|
||||||
|
String user = plugin.getConfig().getString("database.username");
|
||||||
|
String pass = plugin.getConfig().getString("database.password");
|
||||||
|
|
||||||
|
if (dbType.equals("postgresql")) {
|
||||||
|
config.setJdbcUrl("jdbc:postgresql://" + host + ":" + port + "/" + db);
|
||||||
|
config.setDriverClassName("org.postgresql.Driver");
|
||||||
|
} else {
|
||||||
|
config.setJdbcUrl("jdbc:mariadb://" + host + ":" + port + "/" + db);
|
||||||
|
config.setDriverClassName("org.mariadb.jdbc.Driver");
|
||||||
|
}
|
||||||
|
|
||||||
|
config.setUsername(user);
|
||||||
|
config.setPassword(pass);
|
||||||
|
config.setMaximumPoolSize(plugin.getConfig().getInt("database.pool_size", 10));
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Unbekannter database.type: '" + dbType
|
||||||
|
+ "'. Erlaubt sind: sqlite, mysql, mariadb, postgresql.");
|
||||||
|
}
|
||||||
|
|
||||||
|
config.setConnectionTimeout(plugin.getConfig().getLong("database.timeout", 30000));
|
||||||
|
config.setPoolName("ZalmikStoragePool");
|
||||||
|
|
||||||
|
try {
|
||||||
|
dataSource = new HikariDataSource(config);
|
||||||
|
createTable();
|
||||||
|
plugin.getLogger().info("Datenbank (" + dbType + ") erfolgreich verbunden.");
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (dataSource != null) {
|
||||||
|
dataSource.close();
|
||||||
|
dataSource = null;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Fehler beim Verbinden mit der Datenbank: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTable() throws SQLException {
|
||||||
|
String query = "CREATE TABLE IF NOT EXISTS zalmik_storage (" +
|
||||||
|
"uuid VARCHAR(36) NOT NULL, " +
|
||||||
|
"page INT NOT NULL, " +
|
||||||
|
"items TEXT, " +
|
||||||
|
"PRIMARY KEY (uuid, page)" +
|
||||||
|
");";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||||||
|
stmt.execute(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void savePage(String uuid, int page, String base64Items) {
|
||||||
|
String query = "";
|
||||||
|
if (dbType.equals("sqlite")) {
|
||||||
|
query = "INSERT OR REPLACE INTO zalmik_storage (uuid, page, items) VALUES (?, ?, ?)";
|
||||||
|
} else if (dbType.equals("mariadb") || dbType.equals("mysql")) {
|
||||||
|
query = "INSERT INTO zalmik_storage (uuid, page, items) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE items = VALUES(items)";
|
||||||
|
} else if (dbType.equals("postgresql")) {
|
||||||
|
query = "INSERT INTO zalmik_storage (uuid, page, items) VALUES (?, ?, ?) ON CONFLICT (uuid, page) DO UPDATE SET items = EXCLUDED.items";
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(query)) {
|
||||||
|
pstmt.setString(1, uuid);
|
||||||
|
pstmt.setInt(2, page);
|
||||||
|
pstmt.setString(3, base64Items);
|
||||||
|
pstmt.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String loadPage(String uuid, int page) {
|
||||||
|
String query = "SELECT items FROM zalmik_storage WHERE uuid = ? AND page = ?";
|
||||||
|
try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(query)) {
|
||||||
|
pstmt.setString(1, uuid);
|
||||||
|
pstmt.setInt(2, page);
|
||||||
|
try (ResultSet rs = pstmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return rs.getString("items");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Connection getConnection() throws SQLException {
|
||||||
|
return dataSource.getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disconnect() {
|
||||||
|
if (dataSource != null && !dataSource.isClosed()) {
|
||||||
|
dataSource.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/main/java/de/plugin/zalmikstorage/ItemSerializer.java
Normal file
55
src/main/java/de/plugin/zalmikstorage/ItemSerializer.java
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
package de.plugin.zalmikstorage;
|
||||||
|
|
||||||
|
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.util.Base64;
|
||||||
|
|
||||||
|
public class ItemSerializer {
|
||||||
|
|
||||||
|
// Pro Seite werden 45 Slots gespeichert; etwas Spielraum gegen korrupte Daten.
|
||||||
|
private static final int MAX_ITEMS = 54;
|
||||||
|
|
||||||
|
public static String itemsToBase64(ItemStack[] items) {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);
|
||||||
|
|
||||||
|
dataOutput.writeInt(items.length);
|
||||||
|
for (ItemStack item : items) {
|
||||||
|
dataOutput.writeObject(item);
|
||||||
|
}
|
||||||
|
dataOutput.close();
|
||||||
|
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("Items konnten nicht serialisiert werden.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ItemStack[] itemsFromBase64(String data) {
|
||||||
|
if (data == null || data.isEmpty()) return new ItemStack[45];
|
||||||
|
|
||||||
|
try {
|
||||||
|
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data));
|
||||||
|
BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream);
|
||||||
|
|
||||||
|
int length = dataInput.readInt();
|
||||||
|
if (length < 0 || length > MAX_ITEMS) {
|
||||||
|
dataInput.close();
|
||||||
|
throw new IllegalStateException("Ungültige Item-Anzahl in gespeicherten Daten: " + length);
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack[] items = new ItemStack[length];
|
||||||
|
for (int i = 0; i < items.length; i++) {
|
||||||
|
items[i] = (ItemStack) dataInput.readObject();
|
||||||
|
}
|
||||||
|
dataInput.close();
|
||||||
|
return items;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("Items konnten nicht geladen werden.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/main/java/de/plugin/zalmikstorage/StorageCommand.java
Normal file
32
src/main/java/de/plugin/zalmikstorage/StorageCommand.java
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package de.plugin.zalmikstorage;
|
||||||
|
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public class StorageCommand implements CommandExecutor {
|
||||||
|
|
||||||
|
private final StorageManager storageManager;
|
||||||
|
|
||||||
|
public StorageCommand(StorageManager storageManager) {
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage("Dieser Befehl kann nur von Spielern ausgeführt werden.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!player.hasPermission("zalmikstorage.use")) {
|
||||||
|
player.sendMessage("§cDazu hast du keine Berechtigung.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
storageManager.openStorage(player, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/main/java/de/plugin/zalmikstorage/StorageHolder.java
Normal file
26
src/main/java/de/plugin/zalmikstorage/StorageHolder.java
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package de.plugin.zalmikstorage;
|
||||||
|
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.InventoryHolder;
|
||||||
|
|
||||||
|
public class StorageHolder implements InventoryHolder {
|
||||||
|
private final int page;
|
||||||
|
private Inventory inventory;
|
||||||
|
|
||||||
|
public StorageHolder(int page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPage() {
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInventory(Inventory inventory) {
|
||||||
|
this.inventory = inventory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/main/java/de/plugin/zalmikstorage/StorageListener.java
Normal file
52
src/main/java/de/plugin/zalmikstorage/StorageListener.java
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
package de.plugin.zalmikstorage;
|
||||||
|
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||||
|
|
||||||
|
public class StorageListener implements Listener {
|
||||||
|
|
||||||
|
private final StorageManager storageManager;
|
||||||
|
|
||||||
|
public StorageListener(StorageManager storageManager) {
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onInventoryClick(InventoryClickEvent event) {
|
||||||
|
if (!(event.getWhoClicked() instanceof Player player)) return;
|
||||||
|
|
||||||
|
if (event.getInventory().getHolder() instanceof StorageHolder holder) {
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
if (slot >= 45 && slot <= 53) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
if (slot == 45 && holder.getPage() > 1) {
|
||||||
|
storageManager.saveStorage(player, event.getInventory(), holder.getPage());
|
||||||
|
storageManager.openStorage(player, holder.getPage() - 1);
|
||||||
|
} else if (slot == 53) {
|
||||||
|
int maxPages = storageManager.getMaxPages(player);
|
||||||
|
if (holder.getPage() >= maxPages) {
|
||||||
|
player.sendMessage("§cDu hast dein Seitenlimit von " + maxPages + " erreicht!");
|
||||||
|
player.playSound(player.getLocation(), org.bukkit.Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
storageManager.saveStorage(player, event.getInventory(), holder.getPage());
|
||||||
|
storageManager.openStorage(player, holder.getPage() + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onInventoryClose(InventoryCloseEvent event) {
|
||||||
|
if (!(event.getPlayer() instanceof Player player)) return;
|
||||||
|
|
||||||
|
if (event.getInventory().getHolder() instanceof StorageHolder holder) {
|
||||||
|
storageManager.saveStorage(player, event.getInventory(), holder.getPage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
src/main/java/de/plugin/zalmikstorage/StorageManager.java
Normal file
137
src/main/java/de/plugin/zalmikstorage/StorageManager.java
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
package de.plugin.zalmikstorage;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import org.bukkit.permissions.PermissionAttachmentInfo;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
public class StorageManager {
|
||||||
|
|
||||||
|
private final ZalmikStorage plugin;
|
||||||
|
private final DatabaseManager dbManager;
|
||||||
|
|
||||||
|
// Pro-Spieler serielle IO-Queue: garantiert FIFO-Reihenfolge von Laden/Speichern
|
||||||
|
// und verhindert so, dass ein Laden eine noch ausstehende Speicherung überholt.
|
||||||
|
private final Map<UUID, ExecutorService> ioExecutors = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public StorageManager(ZalmikStorage plugin, DatabaseManager dbManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.dbManager = dbManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExecutorService executorFor(UUID uuid) {
|
||||||
|
return ioExecutors.computeIfAbsent(uuid, key -> Executors.newSingleThreadExecutor(runnable -> {
|
||||||
|
Thread thread = new Thread(runnable, "ZalmikStorage-IO-" + key);
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxPages(Player player) {
|
||||||
|
if (player.hasPermission("zalmikstorage.pages.unlimited")) {
|
||||||
|
return Integer.MAX_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int maxPages = 1;
|
||||||
|
for (PermissionAttachmentInfo info : player.getEffectivePermissions()) {
|
||||||
|
if (!info.getValue()) continue;
|
||||||
|
String perm = info.getPermission().toLowerCase();
|
||||||
|
if (perm.startsWith("zalmikstorage.pages.")) {
|
||||||
|
try {
|
||||||
|
int pages = Integer.parseInt(perm.replace("zalmikstorage.pages.", ""));
|
||||||
|
if (pages > maxPages) {
|
||||||
|
maxPages = pages;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openStorage(Player player, int page) {
|
||||||
|
UUID uuid = player.getUniqueId();
|
||||||
|
executorFor(uuid).execute(() -> {
|
||||||
|
String base64Data = dbManager.loadPage(uuid.toString(), page);
|
||||||
|
ItemStack[] items = ItemSerializer.itemsFromBase64(base64Data);
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
StorageHolder holder = new StorageHolder(page);
|
||||||
|
Inventory inv = Bukkit.createInventory(holder, 54, Component.text("ZalmikStorage - Seite " + page));
|
||||||
|
holder.setInventory(inv);
|
||||||
|
|
||||||
|
for (int i = 0; i < 45; i++) {
|
||||||
|
if (i < items.length && items[i] != null) {
|
||||||
|
inv.setItem(i, items[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack prevPage = createGuiItem(Material.ARROW, "§cVorherige Seite");
|
||||||
|
ItemStack nextPage = createGuiItem(Material.ARROW, "§aNächste Seite");
|
||||||
|
ItemStack info = createGuiItem(Material.PAPER, "§bAktuelle Seite: " + page);
|
||||||
|
ItemStack placeholder = createGuiItem(Material.GRAY_STAINED_GLASS_PANE, " ");
|
||||||
|
|
||||||
|
for (int i = 45; i < 54; i++) {
|
||||||
|
inv.setItem(i, placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page > 1) {
|
||||||
|
inv.setItem(45, prevPage);
|
||||||
|
}
|
||||||
|
inv.setItem(49, info);
|
||||||
|
inv.setItem(53, nextPage);
|
||||||
|
|
||||||
|
player.openInventory(inv);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveStorage(Player player, Inventory inventory, int page) {
|
||||||
|
UUID uuid = player.getUniqueId();
|
||||||
|
String base64Data = ItemSerializer.itemsToBase64(extractContents(inventory));
|
||||||
|
executorFor(uuid).execute(() -> dbManager.savePage(uuid.toString(), page, base64Data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speichert synchron auf dem aufrufenden Thread. Wird beim Server-Stop genutzt,
|
||||||
|
* da der Bukkit-Scheduler dann keine asynchronen Tasks mehr ausführt.
|
||||||
|
*/
|
||||||
|
public void saveStorageSync(Player player, Inventory inventory, int page) {
|
||||||
|
String base64Data = ItemSerializer.itemsToBase64(extractContents(inventory));
|
||||||
|
dbManager.savePage(player.getUniqueId().toString(), page, base64Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
for (ExecutorService executor : ioExecutors.values()) {
|
||||||
|
executor.shutdown();
|
||||||
|
}
|
||||||
|
ioExecutors.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack[] extractContents(Inventory inventory) {
|
||||||
|
ItemStack[] contents = new ItemStack[45];
|
||||||
|
for (int i = 0; i < 45; i++) {
|
||||||
|
contents[i] = inventory.getItem(i);
|
||||||
|
}
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack createGuiItem(Material material, String name) {
|
||||||
|
ItemStack item = new ItemStack(material);
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
if (meta != null) {
|
||||||
|
meta.setDisplayName(name);
|
||||||
|
item.setItemMeta(meta);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/main/java/de/plugin/zalmikstorage/ZalmikStorage.java
Normal file
57
src/main/java/de/plugin/zalmikstorage/ZalmikStorage.java
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
package de.plugin.zalmikstorage;
|
||||||
|
|
||||||
|
import org.bukkit.command.PluginCommand;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.InventoryHolder;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
public final class ZalmikStorage extends JavaPlugin {
|
||||||
|
|
||||||
|
private DatabaseManager databaseManager;
|
||||||
|
private StorageManager storageManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
saveDefaultConfig();
|
||||||
|
|
||||||
|
this.databaseManager = new DatabaseManager(this);
|
||||||
|
try {
|
||||||
|
this.databaseManager.connect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
getLogger().severe("Datenbankverbindung fehlgeschlagen, Plugin wird deaktiviert: " + e.getMessage());
|
||||||
|
getServer().getPluginManager().disablePlugin(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.storageManager = new StorageManager(this, this.databaseManager);
|
||||||
|
|
||||||
|
PluginCommand command = getCommand("storage");
|
||||||
|
if (command != null) {
|
||||||
|
command.setExecutor(new StorageCommand(this.storageManager));
|
||||||
|
} else {
|
||||||
|
getLogger().severe("Befehl 'storage' konnte nicht registriert werden (fehlt in plugin.yml?).");
|
||||||
|
}
|
||||||
|
getServer().getPluginManager().registerEvents(new StorageListener(this.storageManager), this);
|
||||||
|
|
||||||
|
getLogger().info("ZalmikStorage aktiviert!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
if (this.storageManager != null) {
|
||||||
|
for (Player player : getServer().getOnlinePlayers()) {
|
||||||
|
Inventory top = player.getOpenInventory().getTopInventory();
|
||||||
|
InventoryHolder holder = top.getHolder();
|
||||||
|
if (holder instanceof StorageHolder storageHolder) {
|
||||||
|
this.storageManager.saveStorageSync(player, top, storageHolder.getPage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.storageManager.shutdown();
|
||||||
|
}
|
||||||
|
if (this.databaseManager != null) {
|
||||||
|
this.databaseManager.disconnect();
|
||||||
|
}
|
||||||
|
getLogger().info("ZalmikStorage deaktiviert! Datenbankverbindung getrennt.");
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/main/resources/config.yml
Normal file
11
src/main/resources/config.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# ZalmikStorage Configuration
|
||||||
|
|
||||||
|
database:
|
||||||
|
type: sqlite
|
||||||
|
host: "127.0.0.1"
|
||||||
|
port: 3306
|
||||||
|
database: "minecraft"
|
||||||
|
username: "root"
|
||||||
|
password: "password"
|
||||||
|
pool_size: 10
|
||||||
|
timeout: 30000
|
||||||
14
src/main/resources/plugin.yml
Normal file
14
src/main/resources/plugin.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
name: ZalmikStorage
|
||||||
|
version: 1.0
|
||||||
|
main: de.plugin.zalmikstorage.ZalmikStorage
|
||||||
|
api-version: "1.20"
|
||||||
|
author: Tronax
|
||||||
|
description: Mehrseitiges, datenbankgestütztes Spieler-Lagersystem mit konfigurierbarem Seitenlimit per Permission (SQLite, MySQL/MariaDB, PostgreSQL).
|
||||||
|
commands:
|
||||||
|
storage:
|
||||||
|
description: Öffnet den ZalmikStorage
|
||||||
|
usage: /storage
|
||||||
|
permissions:
|
||||||
|
zalmikstorage.use:
|
||||||
|
description: Erlaubt das Öffnen des Storages
|
||||||
|
default: false
|
||||||
Loading…
Add table
Add a link
Reference in a new issue