Minecraft Storage Plugin for PaperMC (works like a backpack)

This commit is contained in:
Tronax 2026-06-13 10:47:19 +02:00
commit a8524ccc47
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
12 changed files with 743 additions and 0 deletions

46
.gitignore vendored Normal file
View 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
View 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 (4553) sind klick-geschützt** (`event.setCancelled(true)`), wodurch Item-Klau aus der GUI-Leiste verhindert wird.

78
pom.xml Normal file
View 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>

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

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

View 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;
}
}

View 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;
}
}

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

View 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;
}
}

View 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.");
}
}

View 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

View 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