Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 15b875a723 | |||
| de97f3dea7 | |||
| 42b0cf8279 | |||
| 1c39ab5557 | |||
| 38c7327291 | |||
| ff9336c8d1 | |||
| 3567a93918 | |||
| 615b404ce6 | |||
| a2f3260854 | |||
| 48648c5632 | |||
| 10f2a66e3a | |||
| e196507634 | |||
| b208dc8592 | |||
| f2f2fb3353 | |||
| 5429c91498 | |||
| 40febaf7e2 | |||
| 9039dcc9ad | |||
| 8f1a1de252 | |||
| d107835455 | |||
| a09dad30ec | |||
| 18a1991af9 | |||
| 6e03ffa962 | |||
| 66a95c48fa | |||
| 51559f12ed | |||
| 605d7f44d5 | |||
| 75668031e0 | |||
| 3248ad9e8d | |||
| c59e37ef68 | |||
| db4255a0ad | |||
| 5e2ae5ba0a | |||
| afeb22422e | |||
| 9b84b18952 | |||
| 3e32e3af32 | |||
| da6fe6cb33 | |||
| 37538a9632 | |||
| e0b63ee2e4 | |||
| d4b5aa350f | |||
| b34d74a62e | |||
| 1f36b22916 | |||
| e19378d815 | |||
| 4720f3b4b6 | |||
| 4832b259a5 | |||
| 0ea27c815c | |||
| 8510e601d9 | |||
| 603ece2f7b | |||
| 2e9c2b2579 | |||
| bd47187b95 | |||
| 4d94218e22 | |||
| 2c8260191f | |||
| 85b6aa8d88 | |||
| 6f5b7d7fcf | |||
| 49a2057705 | |||
| 25b3716fdc | |||
| 1d84f5ffe7 | |||
| 9313fb2e7a | |||
| 2426d4654f | |||
| 5284253ce8 | |||
| d3a17ff5f1 | |||
| fbdd42a856 | |||
| 12786cb553 | |||
| 0e99638960 | |||
| 4f476e478e | |||
| e7a90e5a82 |
@@ -4,10 +4,14 @@ import org.bukkit.Bukkit;
|
|||||||
import org.bukkit.ChatColor;
|
import org.bukkit.ChatColor;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.Sound;
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
import org.bukkit.inventory.Inventory;
|
import org.bukkit.inventory.Inventory;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.inventory.meta.ItemMeta;
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
@@ -24,6 +28,7 @@ import java.io.OutputStream;
|
|||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -38,21 +43,29 @@ import com.google.gson.JsonParser;
|
|||||||
public class IngameShopSpigot extends JavaPlugin implements Listener {
|
public class IngameShopSpigot extends JavaPlugin implements Listener {
|
||||||
|
|
||||||
private static Economy econ = null;
|
private static Economy econ = null;
|
||||||
private String wpBase;
|
private String wpBase;
|
||||||
private String wpUrlPending;
|
private String wpUrlPending;
|
||||||
|
private String wpUrlPendingOffline; // NEU: Offline-Queue
|
||||||
private String wpUrlExecute;
|
private String wpUrlExecute;
|
||||||
private String wpUrlComplete;
|
private String wpUrlComplete;
|
||||||
private String wpUrlCancel; // NEU
|
private String wpUrlCancel;
|
||||||
|
|
||||||
private Gson gson = new Gson();
|
private Gson gson = new Gson();
|
||||||
private boolean debug = false;
|
private boolean debug = false;
|
||||||
private BukkitTask task;
|
private BukkitTask task;
|
||||||
|
|
||||||
private String currency = "Coins";
|
private String currency = "Coins";
|
||||||
private String targetServer = "survival";
|
private String targetServer = "survival";
|
||||||
|
|
||||||
private Map<Integer, OrderData> orderCache = new HashMap<>();
|
// NEU: API-Key für gesicherte Endpunkte
|
||||||
private Map<UUID, Integer> activeOrderIds = new HashMap<>();
|
private String apiKey = "";
|
||||||
|
|
||||||
|
private Map<Integer, OrderData> orderCache = new HashMap<>();
|
||||||
|
private Map<UUID, Integer> activeOrderIds = new HashMap<>();
|
||||||
|
|
||||||
|
// ===========================================================
|
||||||
|
// LIFECYCLE
|
||||||
|
// ===========================================================
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
@@ -64,32 +77,48 @@ public class IngameShopSpigot extends JavaPlugin implements Listener {
|
|||||||
|
|
||||||
saveDefaultConfig();
|
saveDefaultConfig();
|
||||||
reloadConfig();
|
reloadConfig();
|
||||||
|
|
||||||
String domain = getConfig().getString("wordpress-url", "http://localhost/Windelgeschichten.org");
|
String domain = getConfig().getString("wordpress-url", "http://localhost");
|
||||||
if (domain.endsWith("/")) {
|
if (domain.endsWith("/")) domain = domain.substring(0, domain.length() - 1);
|
||||||
domain = domain.substring(0, domain.length() - 1);
|
|
||||||
}
|
|
||||||
wpBase = domain + "/wp-json/wis/v1";
|
wpBase = domain + "/wp-json/wis/v1";
|
||||||
|
|
||||||
wpUrlPending = wpBase + "/pending_orders";
|
wpUrlPending = wpBase + "/pending_orders";
|
||||||
wpUrlExecute = wpBase + "/execute_order";
|
wpUrlPendingOffline = wpBase + "/pending_offline"; // NEU
|
||||||
wpUrlComplete = wpBase + "/complete_order";
|
wpUrlExecute = wpBase + "/execute_order";
|
||||||
wpUrlCancel = wpBase + "/cancel_order"; // NEU
|
wpUrlComplete = wpBase + "/complete_order";
|
||||||
|
wpUrlCancel = wpBase + "/cancel_order";
|
||||||
this.targetServer = getConfig().getString("server-name", "survival").toLowerCase();
|
|
||||||
this.currency = getConfig().getString("currency-name", "Coins");
|
targetServer = getConfig().getString("server-name", "survival").toLowerCase();
|
||||||
|
currency = getConfig().getString("currency-name", "Coins");
|
||||||
|
apiKey = getConfig().getString("api-key", ""); // NEU
|
||||||
|
debug = getConfig().getBoolean("debug-mode", false);
|
||||||
|
|
||||||
|
if (apiKey.isEmpty()) {
|
||||||
|
getLogger().warning("⚠️ Kein api-key in config.yml gesetzt! Geschützte Endpunkte sind nicht erreichbar.");
|
||||||
|
getLogger().warning(" Trage den Key aus den WordPress-Einstellungen (Ingame Shop → Einstellungen → 🔑 API-Key) ein.");
|
||||||
|
}
|
||||||
|
|
||||||
int intervalSeconds = getConfig().getInt("check-interval", 10);
|
int intervalSeconds = getConfig().getInt("check-interval", 10);
|
||||||
long pollInterval = intervalSeconds * 20L;
|
long pollInterval = intervalSeconds * 20L;
|
||||||
debug = getConfig().getBoolean("debug-mode", false);
|
|
||||||
|
|
||||||
getServer().getPluginManager().registerEvents(this, this);
|
getServer().getPluginManager().registerEvents(this, this);
|
||||||
|
|
||||||
|
// /orders Befehl registrieren
|
||||||
|
getCommand("orders").setExecutor(new OrdersCommand());
|
||||||
|
|
||||||
startPolling(pollInterval);
|
startPolling(pollInterval);
|
||||||
|
|
||||||
getLogger().info("=== IngameShopSpigot v6.2 (Cancel Logic) CONFIG ===");
|
getLogger().info("=== IngameShopSpigot v6.3 (API-Key + Offline-Queue) ===");
|
||||||
getLogger().info("Domain: " + domain);
|
getLogger().info("Domain: " + domain);
|
||||||
getLogger().info("Target Server: " + this.targetServer);
|
getLogger().info("Target Server: " + targetServer);
|
||||||
getLogger().info("Currency: " + this.currency);
|
getLogger().info("Currency: " + currency);
|
||||||
|
getLogger().info("API-Key: " + (apiKey.isEmpty() ? "❌ NICHT GESETZT" : "✅ gesetzt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
if (task != null) task.cancel();
|
||||||
|
getLogger().info("IngameShopSpigot gestoppt");
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean setupEconomy() {
|
private boolean setupEconomy() {
|
||||||
@@ -99,6 +128,10 @@ public class IngameShopSpigot extends JavaPlugin implements Listener {
|
|||||||
return econ != null;
|
return econ != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===========================================================
|
||||||
|
// POLLING
|
||||||
|
// ===========================================================
|
||||||
|
|
||||||
private void startPolling(long intervalTicks) {
|
private void startPolling(long intervalTicks) {
|
||||||
this.task = new BukkitRunnable() {
|
this.task = new BukkitRunnable() {
|
||||||
@Override
|
@Override
|
||||||
@@ -107,61 +140,13 @@ public class IngameShopSpigot extends JavaPlugin implements Listener {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||||
try {
|
// RACE CONDITION FIX: Spieler der bereits eine aktive Order im GUI hat
|
||||||
String urlString = wpUrlPending + "?player=" + p.getName();
|
// bekommt keine neue Order angezeigt bis die vorherige abgeschlossen ist
|
||||||
URL url = new URL(urlString);
|
if (activeOrderIds.containsKey(p.getUniqueId())) {
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
if (debug) getLogger().info("Spieler " + p.getName() + " hat bereits aktive Order – überspringe Poll.");
|
||||||
conn.setRequestMethod("GET");
|
continue;
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
|
||||||
conn.setConnectTimeout(5000);
|
|
||||||
conn.setReadTimeout(5000);
|
|
||||||
|
|
||||||
if (conn.getResponseCode() == 200) {
|
|
||||||
BufferedReader reader = new BufferedReader(
|
|
||||||
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)
|
|
||||||
);
|
|
||||||
StringBuilder response = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
response.append(line);
|
|
||||||
}
|
|
||||||
reader.close();
|
|
||||||
|
|
||||||
JsonObject json = JsonParser.parseString(response.toString()).getAsJsonObject();
|
|
||||||
JsonArray orders = json.getAsJsonArray("orders");
|
|
||||||
|
|
||||||
if (orders != null && orders.size() > 0) {
|
|
||||||
for (int i = 0; i < orders.size(); i++) {
|
|
||||||
JsonObject order = orders.get(i).getAsJsonObject();
|
|
||||||
|
|
||||||
int id = order.get("id").getAsInt();
|
|
||||||
String orderServer = order.has("server") ? order.get("server").getAsString().toLowerCase() : "";
|
|
||||||
|
|
||||||
if (!orderServer.equals(targetServer)) {
|
|
||||||
if (debug) getLogger().info("Order #" + id + " ist für Server '" + orderServer + "'. Ignoriere.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String jsonResponse = order.has("response") ? order.get("response").getAsString() : "[]";
|
|
||||||
String itemTitle = order.get("item_title").getAsString();
|
|
||||||
double price = order.get("price").getAsDouble();
|
|
||||||
String status = order.get("status").getAsString();
|
|
||||||
|
|
||||||
if ("pending".equals(status)) {
|
|
||||||
OrderData data = new OrderData(id, "multi_item", itemTitle, price, 1, jsonResponse);
|
|
||||||
orderCache.put(id, data);
|
|
||||||
activeOrderIds.put(p.getUniqueId(), id);
|
|
||||||
|
|
||||||
Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
|
|
||||||
openConfirmGUI(p, id, itemTitle, price);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (debug) Bukkit.getLogger().log(Level.WARNING, "Polling Fehler", e);
|
|
||||||
}
|
}
|
||||||
|
fetchPendingOrders(p, wpUrlPending);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.runTaskAsynchronously(IngameShopSpigot.this);
|
}.runTaskAsynchronously(IngameShopSpigot.this);
|
||||||
@@ -169,25 +154,92 @@ public class IngameShopSpigot extends JavaPlugin implements Listener {
|
|||||||
}.runTaskTimer(this, 20L, intervalTicks);
|
}.runTaskTimer(this, 20L, intervalTicks);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openConfirmGUI(Player player, int orderId, String itemTitle, double price) {
|
private void fetchPendingOrders(Player p, String endpointUrl) {
|
||||||
Inventory gui = Bukkit.createInventory(null, 27, "§eKauf bestätigen?");
|
try {
|
||||||
|
String urlString = endpointUrl + "?player=" + p.getName();
|
||||||
|
HttpURLConnection conn = openAuthConnection(urlString, "GET");
|
||||||
|
|
||||||
ItemStack info = new ItemStack(Material.WRITTEN_BOOK);
|
if (conn.getResponseCode() == 200) {
|
||||||
|
String body = readResponse(conn);
|
||||||
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
JsonArray orders = json.getAsJsonArray("orders");
|
||||||
|
|
||||||
|
if (orders != null && orders.size() > 0) {
|
||||||
|
JsonObject order = orders.get(0).getAsJsonObject(); // immer nur erste Order zeigen
|
||||||
|
|
||||||
|
int id = order.get("id").getAsInt();
|
||||||
|
String orderServer = order.has("server") ? order.get("server").getAsString().toLowerCase() : "";
|
||||||
|
|
||||||
|
if (!orderServer.equals(targetServer)) {
|
||||||
|
if (debug) getLogger().info("Order #" + id + " ist für Server '" + orderServer + "'. Ignoriere.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String jsonResponse = order.has("response") ? order.get("response").getAsString() : "[]";
|
||||||
|
String itemTitle = order.get("item_title").getAsString();
|
||||||
|
double price = order.get("price").getAsDouble();
|
||||||
|
|
||||||
|
OrderData data = new OrderData(id, "multi_item", itemTitle, price, 1, jsonResponse);
|
||||||
|
orderCache.put(id, data);
|
||||||
|
activeOrderIds.put(p.getUniqueId(), id);
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> openConfirmGUI(p, id, itemTitle, price, jsonResponse));
|
||||||
|
}
|
||||||
|
} else if (conn.getResponseCode() == 401) {
|
||||||
|
getLogger().warning("❌ API-Key ungültig oder nicht gesetzt! HTTP 401 von " + endpointUrl);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (debug) getLogger().log(Level.WARNING, "Polling Fehler für " + p.getName(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================
|
||||||
|
// OFFLINE-QUEUE: beim Login ausstehende Orders liefern
|
||||||
|
// ===========================================================
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||||
|
Player p = event.getPlayer();
|
||||||
|
|
||||||
|
new BukkitRunnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
fetchPendingOrders(p, wpUrlPendingOffline);
|
||||||
|
}
|
||||||
|
}.runTaskAsynchronously(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================
|
||||||
|
// GUI: zeigt echtes Item-Icon aus dem Warenkorb-Payload
|
||||||
|
// ===========================================================
|
||||||
|
|
||||||
|
private void openConfirmGUI(Player player, int orderId, String itemTitle, double price, String jsonPayload) {
|
||||||
|
Inventory gui = Bukkit.createInventory(null, 27, ChatColor.YELLOW + "Kauf bestätigen?");
|
||||||
|
|
||||||
|
// NEU: erstes Item aus dem Payload als Icon verwenden
|
||||||
|
Material iconMaterial = getFirstItemMaterial(jsonPayload);
|
||||||
|
ItemStack info = new ItemStack(iconMaterial);
|
||||||
ItemMeta infoMeta = info.getItemMeta();
|
ItemMeta infoMeta = info.getItemMeta();
|
||||||
infoMeta.setDisplayName(ChatColor.GOLD + itemTitle);
|
infoMeta.setDisplayName(ChatColor.GOLD + "" + ChatColor.BOLD + itemTitle);
|
||||||
infoMeta.setLore(java.util.Arrays.asList(
|
infoMeta.setLore(Arrays.asList(
|
||||||
ChatColor.WHITE + "Preis: " + price + " " + currency
|
ChatColor.GRAY + "──────────────────",
|
||||||
|
ChatColor.WHITE + "Preis: " + ChatColor.YELLOW + price + " " + currency,
|
||||||
|
ChatColor.GRAY + "──────────────────",
|
||||||
|
ChatColor.GRAY + "Klicke Grün zum Bestätigen",
|
||||||
|
ChatColor.GRAY + "Klicke Rot zum Abbrechen"
|
||||||
));
|
));
|
||||||
info.setItemMeta(infoMeta);
|
info.setItemMeta(infoMeta);
|
||||||
|
|
||||||
ItemStack yes = new ItemStack(Material.LIME_WOOL);
|
ItemStack yes = new ItemStack(Material.LIME_WOOL);
|
||||||
ItemMeta yesMeta = yes.getItemMeta();
|
ItemMeta yesMeta = yes.getItemMeta();
|
||||||
yesMeta.setDisplayName(ChatColor.GREEN + "§lJA, kaufen!");
|
yesMeta.setDisplayName(ChatColor.GREEN + "" + ChatColor.BOLD + "✔ JA, kaufen!");
|
||||||
|
yesMeta.setLore(Arrays.asList(ChatColor.GRAY + "" + price + " " + currency + " werden abgezogen"));
|
||||||
yes.setItemMeta(yesMeta);
|
yes.setItemMeta(yesMeta);
|
||||||
|
|
||||||
ItemStack no = new ItemStack(Material.RED_WOOL);
|
ItemStack no = new ItemStack(Material.RED_WOOL);
|
||||||
ItemMeta noMeta = no.getItemMeta();
|
ItemMeta noMeta = no.getItemMeta();
|
||||||
noMeta.setDisplayName(ChatColor.RED + "§lNEIN, abbrechen");
|
noMeta.setDisplayName(ChatColor.RED + "" + ChatColor.BOLD + "✘ NEIN, abbrechen");
|
||||||
|
noMeta.setLore(Arrays.asList(ChatColor.GRAY + "Bestellung wird storniert"));
|
||||||
no.setItemMeta(noMeta);
|
no.setItemMeta(noMeta);
|
||||||
|
|
||||||
ItemStack pane = new ItemStack(Material.BLACK_STAINED_GLASS_PANE);
|
ItemStack pane = new ItemStack(Material.BLACK_STAINED_GLASS_PANE);
|
||||||
@@ -203,15 +255,43 @@ public class IngameShopSpigot extends JavaPlugin implements Listener {
|
|||||||
player.openInventory(gui);
|
player.openInventory(gui);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NEU: Liest das erste Item aus dem JSON-Payload und gibt das passende Material zurück.
|
||||||
|
* Fällt auf CHEST zurück wenn nichts parsebar ist.
|
||||||
|
*/
|
||||||
|
private Material getFirstItemMaterial(String jsonPayload) {
|
||||||
|
try {
|
||||||
|
JsonElement root = gson.fromJson(jsonPayload, JsonElement.class);
|
||||||
|
JsonArray items;
|
||||||
|
if (root.isJsonObject()) {
|
||||||
|
items = root.getAsJsonObject().getAsJsonArray("items");
|
||||||
|
} else {
|
||||||
|
items = root.getAsJsonArray();
|
||||||
|
}
|
||||||
|
if (items != null && items.size() > 0) {
|
||||||
|
String itemId = items.get(0).getAsJsonObject().get("id").getAsString();
|
||||||
|
ItemStack test = parseItem(itemId);
|
||||||
|
if (test != null) return test.getType();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (debug) getLogger().log(Level.WARNING, "getFirstItemMaterial Fehler", e);
|
||||||
|
}
|
||||||
|
return Material.CHEST; // sicherer Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================
|
||||||
|
// INVENTORY CLICK
|
||||||
|
// ===========================================================
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onInventoryClick(InventoryClickEvent event) {
|
public void onInventoryClick(InventoryClickEvent event) {
|
||||||
if (!event.getView().getTitle().contains("Kauf bestätigen?")) return;
|
if (!event.getView().getTitle().contains("Kauf bestätigen?")) return;
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
if (!(event.getWhoClicked() instanceof Player)) return;
|
if (!(event.getWhoClicked() instanceof Player)) return;
|
||||||
|
|
||||||
Player p = (Player) event.getWhoClicked();
|
Player p = (Player) event.getWhoClicked();
|
||||||
int slot = event.getRawSlot();
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
if (slot == 11) { // JA
|
if (slot == 11) { // JA
|
||||||
Integer orderId = activeOrderIds.get(p.getUniqueId());
|
Integer orderId = activeOrderIds.get(p.getUniqueId());
|
||||||
if (orderId != null) {
|
if (orderId != null) {
|
||||||
@@ -225,13 +305,17 @@ public class IngameShopSpigot extends JavaPlugin implements Listener {
|
|||||||
} else if (slot == 15) { // NEIN
|
} else if (slot == 15) { // NEIN
|
||||||
Integer orderId = activeOrderIds.get(p.getUniqueId());
|
Integer orderId = activeOrderIds.get(p.getUniqueId());
|
||||||
if (orderId != null) {
|
if (orderId != null) {
|
||||||
cancelOrder(p, orderId); // NEU
|
cancelOrder(p, orderId);
|
||||||
p.closeInventory();
|
p.closeInventory();
|
||||||
activeOrderIds.remove(p.getUniqueId());
|
activeOrderIds.remove(p.getUniqueId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===========================================================
|
||||||
|
// ORDER PROCESSING
|
||||||
|
// ===========================================================
|
||||||
|
|
||||||
private void processOrder(Player player, int orderId) {
|
private void processOrder(Player player, int orderId) {
|
||||||
OrderData data = orderCache.get(orderId);
|
OrderData data = orderCache.get(orderId);
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
@@ -243,76 +327,54 @@ public class IngameShopSpigot extends JavaPlugin implements Listener {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
String jsonInputString = "{\"id\":\"" + orderId + "\"}";
|
HttpURLConnection conn = openAuthConnection(wpUrlExecute, "POST");
|
||||||
URL url = new URL(wpUrlExecute);
|
writeJson(conn, "{\"id\":\"" + orderId + "\"}");
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("POST");
|
int code = conn.getResponseCode();
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
if (code == 200) {
|
||||||
conn.setDoOutput(true);
|
|
||||||
|
|
||||||
try(OutputStream os = conn.getOutputStream()) {
|
|
||||||
byte[] input = jsonInputString.getBytes("utf-8");
|
|
||||||
os.write(input, 0, input.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
int responseCode = conn.getResponseCode();
|
|
||||||
if (responseCode == 200) {
|
|
||||||
Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> executeShopLogic(player, data, orderId));
|
Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> executeShopLogic(player, data, orderId));
|
||||||
|
} else if (code == 401) {
|
||||||
|
getLogger().warning("❌ API-Key ungültig bei /execute_order");
|
||||||
|
Bukkit.getScheduler().runTask(IngameShopSpigot.this, () ->
|
||||||
|
player.sendMessage(ChatColor.RED + "❌ Server-Konfigurationsfehler (Auth)."));
|
||||||
} else {
|
} else {
|
||||||
if (debug) getLogger().warning("Execute Order API Code: " + responseCode);
|
if (debug) getLogger().warning("Execute Order HTTP " + code);
|
||||||
Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
|
Bukkit.getScheduler().runTask(IngameShopSpigot.this, () ->
|
||||||
player.sendMessage(ChatColor.RED + "❌ Server-Fehler beim Starten des Kaufs.");
|
player.sendMessage(ChatColor.RED + "❌ Server-Fehler beim Starten des Kaufs."));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
getLogger().log(Level.SEVERE, "Fehler bei /execute_order", e);
|
getLogger().log(Level.SEVERE, "Fehler bei /execute_order", e);
|
||||||
Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
|
Bukkit.getScheduler().runTask(IngameShopSpigot.this, () ->
|
||||||
player.sendMessage(ChatColor.RED + "❌ Interner Fehler beim Kauf.");
|
player.sendMessage(ChatColor.RED + "❌ Interner Fehler beim Kauf."));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.runTaskAsynchronously(this);
|
}.runTaskAsynchronously(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================
|
|
||||||
// NEU: ABBRECHEN LOGIC
|
|
||||||
// ===========================================================
|
|
||||||
private void cancelOrder(Player player, int orderId) {
|
private void cancelOrder(Player player, int orderId) {
|
||||||
new BukkitRunnable() {
|
new BukkitRunnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
String jsonInputString = "{\"id\":\"" + orderId + "\"}";
|
HttpURLConnection conn = openAuthConnection(wpUrlCancel, "POST");
|
||||||
URL url = new URL(wpUrlCancel);
|
writeJson(conn, "{\"id\":\"" + orderId + "\"}");
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("POST");
|
int code = conn.getResponseCode();
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
orderCache.remove(orderId);
|
||||||
conn.setDoOutput(true);
|
|
||||||
|
if (code == 200) {
|
||||||
try(OutputStream os = conn.getOutputStream()) {
|
|
||||||
byte[] input = jsonInputString.getBytes("utf-8");
|
|
||||||
os.write(input, 0, input.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
int responseCode = conn.getResponseCode();
|
|
||||||
|
|
||||||
if (responseCode == 200) {
|
|
||||||
orderCache.remove(orderId);
|
|
||||||
player.sendMessage(ChatColor.YELLOW + "❌ Kauf abgebrochen.");
|
player.sendMessage(ChatColor.YELLOW + "❌ Kauf abgebrochen.");
|
||||||
if (debug) getLogger().info("✅ Order #" + orderId + " successfully cancelled");
|
if (debug) getLogger().info("✅ Order #" + orderId + " cancelled");
|
||||||
} else {
|
} else {
|
||||||
if (debug) getLogger().warning("⚠️ Cancel Order API returned code: " + responseCode);
|
if (debug) getLogger().warning("⚠️ Cancel HTTP " + code);
|
||||||
player.sendMessage(ChatColor.RED + "❌ Fehler beim Abbrechen des Kaufs.");
|
player.sendMessage(ChatColor.RED + "❌ Fehler beim Abbrechen – Order wird lokal verworfen.");
|
||||||
// Fallback: Cache leeren damit es nicht als Loop erscheint
|
|
||||||
orderCache.remove(orderId);
|
|
||||||
activeOrderIds.remove(player.getUniqueId());
|
activeOrderIds.remove(player.getUniqueId());
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
getLogger().log(Level.WARNING, "Fehler bei /cancel_order", e);
|
getLogger().log(Level.WARNING, "Fehler bei /cancel_order", e);
|
||||||
// Fallback: Cache leeren
|
|
||||||
orderCache.remove(orderId);
|
orderCache.remove(orderId);
|
||||||
activeOrderIds.remove(player.getUniqueId());
|
activeOrderIds.remove(player.getUniqueId());
|
||||||
player.sendMessage(ChatColor.YELLOW + "❌ Kauf abgebrochen (lokale Bestätigung).");
|
player.sendMessage(ChatColor.YELLOW + "❌ Kauf abgebrochen (lokal).");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.runTaskAsynchronously(this);
|
}.runTaskAsynchronously(this);
|
||||||
@@ -321,70 +383,68 @@ public class IngameShopSpigot extends JavaPlugin implements Listener {
|
|||||||
private void executeShopLogic(Player player, OrderData data, int orderId) {
|
private void executeShopLogic(Player player, OrderData data, int orderId) {
|
||||||
try {
|
try {
|
||||||
if (econ.getBalance(player) < data.price) {
|
if (econ.getBalance(player) < data.price) {
|
||||||
player.sendMessage(ChatColor.RED + "❌ Du hast nicht genug Geld! (Benötigt: " + data.price + " " + currency + ")");
|
player.sendMessage(ChatColor.RED + "❌ Nicht genug " + currency + "! (Benötigt: " + data.price + ")");
|
||||||
// Wenn der Kauf fehlschagt (Geld), sollte die Bestellung abgebrochen werden
|
|
||||||
cancelOrder(player, orderId);
|
cancelOrder(player, orderId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
econ.withdrawPlayer(player, data.price);
|
econ.withdrawPlayer(player, data.price);
|
||||||
player.sendMessage(ChatColor.GREEN + "💰 " + data.price + " " + currency + " abgezogen.");
|
player.sendMessage(ChatColor.GREEN + "💰 " + data.price + " " + currency + " abgezogen.");
|
||||||
|
|
||||||
try {
|
JsonElement root = gson.fromJson(data.jsonPayload, JsonElement.class);
|
||||||
JsonElement root = gson.fromJson(data.jsonPayload, JsonElement.class);
|
JsonArray items;
|
||||||
JsonArray items;
|
if (root.isJsonObject()) {
|
||||||
|
items = root.getAsJsonObject().getAsJsonArray("items");
|
||||||
if (root.isJsonObject()) {
|
} else {
|
||||||
items = root.getAsJsonObject().getAsJsonArray("items");
|
items = root.getAsJsonArray();
|
||||||
} else {
|
|
||||||
items = root.getAsJsonArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
int totalItemsGiven = 0;
|
|
||||||
|
|
||||||
for (JsonElement e : items) {
|
|
||||||
JsonObject itemObj = e.getAsJsonObject();
|
|
||||||
String itemId = itemObj.get("id").getAsString();
|
|
||||||
int amount = itemObj.get("amount").getAsInt();
|
|
||||||
|
|
||||||
ItemStack item = parseItem(itemId);
|
|
||||||
if (item != null) {
|
|
||||||
int remaining = amount;
|
|
||||||
while (remaining > 0) {
|
|
||||||
int stackSize = Math.min(remaining, item.getMaxStackSize());
|
|
||||||
ItemStack stack = item.clone();
|
|
||||||
stack.setAmount(stackSize);
|
|
||||||
|
|
||||||
if (player.getInventory().firstEmpty() == -1) {
|
|
||||||
player.getWorld().dropItemNaturally(player.getLocation(), stack);
|
|
||||||
} else {
|
|
||||||
player.getInventory().addItem(stack);
|
|
||||||
}
|
|
||||||
remaining -= stackSize;
|
|
||||||
}
|
|
||||||
totalItemsGiven++;
|
|
||||||
} else {
|
|
||||||
player.sendMessage(ChatColor.RED + "❌ Item '" + itemId + "' konnte nicht gefunden werden.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalItemsGiven > 0) {
|
|
||||||
player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.0F);
|
|
||||||
player.sendMessage(ChatColor.GREEN + "✅ Kauf erfolgreich abgeschlossen!");
|
|
||||||
}
|
|
||||||
|
|
||||||
markOrderCompleted(orderId);
|
|
||||||
orderCache.remove(orderId);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
getLogger().log(Level.SEVERE, "Fehler beim Verarbeiten der JSON-Items", e);
|
|
||||||
player.sendMessage(ChatColor.RED + "❌ Fehler beim Verteilen der Items. Bitte Admin kontaktieren.");
|
|
||||||
markOrderCompleted(orderId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int totalGiven = 0;
|
||||||
|
for (JsonElement e : items) {
|
||||||
|
JsonObject itemObj = e.getAsJsonObject();
|
||||||
|
String itemId = itemObj.get("id").getAsString();
|
||||||
|
int amount = itemObj.get("amount").getAsInt();
|
||||||
|
|
||||||
|
ItemStack item = parseItem(itemId);
|
||||||
|
if (item != null) {
|
||||||
|
giveItems(player, item, amount);
|
||||||
|
totalGiven++;
|
||||||
|
} else {
|
||||||
|
player.sendMessage(ChatColor.RED + "❌ Item '" + itemId + "' nicht gefunden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalGiven > 0) {
|
||||||
|
player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.0F);
|
||||||
|
player.sendMessage(ChatColor.GREEN + "✅ Kauf erfolgreich abgeschlossen!");
|
||||||
|
}
|
||||||
|
|
||||||
|
markOrderCompleted(orderId);
|
||||||
|
orderCache.remove(orderId);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
getLogger().log(Level.SEVERE, "Fehler bei Shop-Logik", e);
|
getLogger().log(Level.SEVERE, "Fehler bei Shop-Logik", e);
|
||||||
player.sendMessage(ChatColor.RED + "❌ Interner Fehler.");
|
player.sendMessage(ChatColor.RED + "❌ Interner Fehler beim Verteilen. Admin kontaktieren.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NEU: Gibt Items an den Spieler – volles Inventar → natürlicher Drop
|
||||||
|
* (bestehende Logik unverändert, aber in eigene Methode ausgelagert)
|
||||||
|
*/
|
||||||
|
private void giveItems(Player player, ItemStack template, int amount) {
|
||||||
|
int remaining = amount;
|
||||||
|
while (remaining > 0) {
|
||||||
|
int stackSize = Math.min(remaining, template.getMaxStackSize());
|
||||||
|
ItemStack stack = template.clone();
|
||||||
|
stack.setAmount(stackSize);
|
||||||
|
if (player.getInventory().firstEmpty() == -1) {
|
||||||
|
player.getWorld().dropItemNaturally(player.getLocation(), stack);
|
||||||
|
player.sendMessage(ChatColor.YELLOW + "⚠ Inventar voll – Items wurden gedroppt.");
|
||||||
|
} else {
|
||||||
|
player.getInventory().addItem(stack);
|
||||||
|
}
|
||||||
|
remaining -= stackSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,103 +453,195 @@ public class IngameShopSpigot extends JavaPlugin implements Listener {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
String jsonInputString = "{\"id\":\"" + orderId + "\"}";
|
HttpURLConnection conn = openAuthConnection(wpUrlComplete, "POST");
|
||||||
|
writeJson(conn, "{\"id\":\"" + orderId + "\"}");
|
||||||
if (debug) getLogger().info("🔄 Marking order #" + orderId + " as completed...");
|
int code = conn.getResponseCode();
|
||||||
|
if (debug) getLogger().info("Complete Order #" + orderId + " → HTTP " + code);
|
||||||
URL url = new URL(wpUrlComplete);
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("POST");
|
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
|
||||||
conn.setDoOutput(true);
|
|
||||||
|
|
||||||
try(OutputStream os = conn.getOutputStream()) {
|
|
||||||
byte[] input = jsonInputString.getBytes("utf-8");
|
|
||||||
os.write(input, 0, input.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
int responseCode = conn.getResponseCode();
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
if (responseCode == 200) {
|
|
||||||
getLogger().info("✅ Order #" + orderId + " successfully marked as completed");
|
|
||||||
} else {
|
|
||||||
getLogger().warning("⚠️ Complete Order API returned code: " + responseCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
getLogger().log(Level.WARNING, "Complete Order Error for #" + orderId, e);
|
getLogger().log(Level.WARNING, "Complete Order Error #" + orderId, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.runTaskAsynchronously(this);
|
}.runTaskAsynchronously(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===========================================================
|
||||||
|
// /orders BEFEHL – Bestellhistorie im Chat
|
||||||
|
// ===========================================================
|
||||||
|
|
||||||
|
private class OrdersCommand implements CommandExecutor {
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (!(sender instanceof Player)) {
|
||||||
|
sender.sendMessage("Nur für Spieler verfügbar.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Player p = (Player) sender;
|
||||||
|
p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||||
|
p.sendMessage(ChatColor.YELLOW + "📦 Deine letzten Bestellungen:");
|
||||||
|
p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||||
|
|
||||||
|
new BukkitRunnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
String urlString = wpBase + "/orders_history?player=" + p.getName();
|
||||||
|
HttpURLConnection conn = openAuthConnection(urlString, "GET");
|
||||||
|
int code = conn.getResponseCode();
|
||||||
|
|
||||||
|
if (code == 200) {
|
||||||
|
String body = readResponse(conn);
|
||||||
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
JsonArray orders = json.getAsJsonArray("orders");
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
|
||||||
|
if (orders == null || orders.size() == 0) {
|
||||||
|
p.sendMessage(ChatColor.GRAY + "Noch keine Bestellungen vorhanden.");
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < Math.min(orders.size(), 10); i++) {
|
||||||
|
JsonObject o = orders.get(i).getAsJsonObject();
|
||||||
|
String title = o.get("item_title").getAsString();
|
||||||
|
double price = o.get("price").getAsDouble();
|
||||||
|
String status = o.get("status").getAsString();
|
||||||
|
String date = o.has("created_at") ? o.get("created_at").getAsString().substring(0,10) : "?";
|
||||||
|
|
||||||
|
String statusColor;
|
||||||
|
switch (status) {
|
||||||
|
case "completed": statusColor = ChatColor.GREEN + "✔ Geliefert"; break;
|
||||||
|
case "cancelled": statusColor = ChatColor.RED + "✘ Storniert"; break;
|
||||||
|
case "processing": statusColor = ChatColor.AQUA + "⟳ In Arbeit"; break;
|
||||||
|
default: statusColor = ChatColor.YELLOW + "⌛ Ausstehend"; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Titel kürzen wenn zu lang
|
||||||
|
String display = title.length() > 35 ? title.substring(0, 32) + "…" : title;
|
||||||
|
p.sendMessage(
|
||||||
|
ChatColor.WHITE + " #" + o.get("id").getAsInt() +
|
||||||
|
" " + ChatColor.AQUA + display +
|
||||||
|
ChatColor.GRAY + " | " + ChatColor.YELLOW + price + " " + currency +
|
||||||
|
ChatColor.GRAY + " | " + statusColor +
|
||||||
|
ChatColor.GRAY + " (" + date + ")"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||||
|
});
|
||||||
|
} else if (code == 404) {
|
||||||
|
// Endpunkt noch nicht implementiert
|
||||||
|
Bukkit.getScheduler().runTask(IngameShopSpigot.this, () ->
|
||||||
|
p.sendMessage(ChatColor.GRAY + "Bestellhistorie nicht verfügbar (WP-Plugin zu alt?)."));
|
||||||
|
} else {
|
||||||
|
Bukkit.getScheduler().runTask(IngameShopSpigot.this, () ->
|
||||||
|
p.sendMessage(ChatColor.RED + "Fehler beim Laden der Bestellungen (HTTP " + code + ")."));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
getLogger().log(Level.WARNING, "/orders Fehler", e);
|
||||||
|
Bukkit.getScheduler().runTask(IngameShopSpigot.this, () ->
|
||||||
|
p.sendMessage(ChatColor.RED + "Verbindungsfehler zum Shop-Server."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.runTaskAsynchronously(IngameShopSpigot.this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================
|
||||||
|
// HTTP HILFSMETHODEN
|
||||||
|
// ===========================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffnet eine HTTP-Verbindung mit gesetztem API-Key Header.
|
||||||
|
*/
|
||||||
|
private HttpURLConnection openAuthConnection(String urlString, String method) throws Exception {
|
||||||
|
URL url = new URL(urlString);
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod(method);
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("X-WIS-Key", apiKey); // NEU: API-Key bei jedem Request
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(5000);
|
||||||
|
if ("POST".equals(method)) conn.setDoOutput(true);
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeJson(HttpURLConnection conn, String json) throws Exception {
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(json.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readResponse(HttpURLConnection conn) throws Exception {
|
||||||
|
try (BufferedReader reader = new BufferedReader(
|
||||||
|
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) sb.append(line);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================
|
||||||
|
// ITEM PARSING
|
||||||
|
// ===========================================================
|
||||||
|
|
||||||
private ItemStack parseItem(String itemId) {
|
private ItemStack parseItem(String itemId) {
|
||||||
try {
|
try {
|
||||||
String cleanId = itemId.toUpperCase().trim().replace("MINECRAFT:", "").replace("MC:", "");
|
String clean = itemId.toUpperCase().trim()
|
||||||
String materialName = numericIdToMaterial(cleanId);
|
.replace("MINECRAFT:", "").replace("MC:", "");
|
||||||
if (materialName == null) materialName = cleanId;
|
String materialName = numericIdToMaterial(clean);
|
||||||
|
if (materialName == null) materialName = clean;
|
||||||
if (materialName.equalsIgnoreCase("VIP") || materialName.startsWith("VIP")) {
|
|
||||||
return null;
|
if (materialName.equalsIgnoreCase("VIP") || materialName.startsWith("VIP")) return null;
|
||||||
}
|
|
||||||
|
|
||||||
Material material = Material.matchMaterial(materialName);
|
Material material = Material.matchMaterial(materialName);
|
||||||
if (material != null && material.isItem()) {
|
if (material != null && material.isItem()) return new ItemStack(material, 1);
|
||||||
return new ItemStack(material,1);
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
getLogger().log(Level.WARNING, "parseItem Fehler: " + itemId, e);
|
getLogger().log(Level.WARNING, "parseItem Fehler: " + itemId, e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String numericIdToMaterial(String id) {
|
private String numericIdToMaterial(String id) {
|
||||||
if (!id.matches("\\d+")) return null;
|
if (!id.matches("\\d+")) return null;
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case "352": return "BONE_MEAL";
|
case "352": return "BONE_MEAL";
|
||||||
case "264": return "EMERALD";
|
case "264": return "DIAMOND";
|
||||||
case "388": return "EMERALD_BLOCK";
|
case "388": return "EMERALD";
|
||||||
case "357": return "GOLDEN_APPLE";
|
case "357": return "COOKIE";
|
||||||
case "322": return "GOLDEN_CARROT";
|
case "322": return "GOLDEN_CARROT";
|
||||||
case "348": return "GOLD_INGOT";
|
case "348": return "GOLD_INGOT";
|
||||||
case "133": return "EMERALD_ORE";
|
case "133": return "EMERALD_BLOCK";
|
||||||
case "41": return "GOLD_ORE";
|
case "41": return "GOLD_BLOCK";
|
||||||
case "14": return "GOLD_BLOCK";
|
case "14": return "GOLD_ORE";
|
||||||
case "289": return "COOKIE";
|
case "289": return "GUNPOWDER";
|
||||||
case "1": return "STONE";
|
case "1": return "STONE";
|
||||||
case "5": return "PLANKS";
|
case "5": return "OAK_PLANKS";
|
||||||
case "17": return "OAK_LOG";
|
case "17": return "OAK_LOG";
|
||||||
case "260": return "BONE_BLOCK";
|
case "260": return "APPLE";
|
||||||
case "287": return "TOTEM_OF_UNDYING";
|
case "287": return "STRING";
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===========================================================
|
||||||
|
// DATA CLASS
|
||||||
|
// ===========================================================
|
||||||
|
|
||||||
private static class OrderData {
|
private static class OrderData {
|
||||||
int id;
|
int id;
|
||||||
String itemId;
|
String itemId;
|
||||||
String itemTitle;
|
String itemTitle;
|
||||||
double price;
|
double price;
|
||||||
int quantity;
|
int quantity;
|
||||||
String jsonPayload;
|
String jsonPayload;
|
||||||
|
|
||||||
OrderData(int id, String itemId, String itemTitle, double price, int quantity, String jsonPayload) {
|
OrderData(int id, String itemId, String itemTitle, double price, int quantity, String jsonPayload) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.itemId = itemId;
|
this.itemId = itemId;
|
||||||
this.itemTitle = itemTitle;
|
this.itemTitle = itemTitle;
|
||||||
this.price = price;
|
this.price = price;
|
||||||
this.quantity = quantity;
|
this.quantity = quantity;
|
||||||
this.jsonPayload = jsonPayload;
|
this.jsonPayload = jsonPayload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDisable() {
|
|
||||||
if (task != null) {
|
|
||||||
task.cancel();
|
|
||||||
}
|
|
||||||
getLogger().info("IngameShopSpigot gestoppt");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,21 @@
|
|||||||
# Hier nur die Domain eingeben (ohne /wp-json am Ende)
|
# IngameShopSpigot v6.3 Konfiguration
|
||||||
wordpress-url: "<Wordpress URL>"
|
# ==========================================
|
||||||
|
|
||||||
# WICHTIG: Der Name des Servers, wie er in WordPress hinterlegt ist (Slug).
|
# Deine WordPress-URL (kein abschließendes /)
|
||||||
# Damit wird verhindert, dass ein Spieler auf Lobby etwas für Survival kauft und es dort erhält.
|
wordpress-url: "https://deine-domain.de"
|
||||||
server-name: "Lobby"
|
|
||||||
|
|
||||||
# Name der Währung (ersetze Coins durch deinen Namen, z.B. Dollar, Points, etc.)
|
# API-Key aus den WordPress-Einstellungen:
|
||||||
currency-name: "Euro"
|
# Ingame Shop → Einstellungen → 🔑 Spigot API-Key → Kopieren
|
||||||
|
api-key: "HIER_DEN_KEY_AUS_WORDPRESS_EINTRAGEN"
|
||||||
|
|
||||||
# Alle wie viel Sekunden (ticks) nach neuen Bestellungen gesucht werden soll (Standard: 10 Sekunden)
|
# Name dieses Servers (muss mit dem Server-Slug in WordPress übereinstimmen)
|
||||||
check-interval: 5
|
server-name: "survival"
|
||||||
|
|
||||||
# Debug Modus (true/false)
|
# Währungsname (muss mit WordPress-Einstellung übereinstimmen)
|
||||||
debug-mode: true
|
currency-name: "Coins"
|
||||||
|
|
||||||
|
# Wie oft (in Sekunden) nach ausstehenden Bestellungen gesucht wird
|
||||||
|
check-interval: 10
|
||||||
|
|
||||||
|
# Debug-Modus (ausführliche Logs in der Konsole)
|
||||||
|
debug-mode: false
|
||||||
@@ -1,25 +1,18 @@
|
|||||||
name: IngameShopSpigot
|
name: IngameShopSpigot
|
||||||
|
version: 6.3
|
||||||
main: de.mviper.spigot.IngameShopSpigot
|
main: de.mviper.spigot.IngameShopSpigot
|
||||||
version: 1.0
|
api-version: 1.19
|
||||||
api-version: "1.20"
|
depend: [Vault]
|
||||||
|
description: Verbindet den WP Ingame Shop Pro mit dem Spigot-Server
|
||||||
author: M_Viper
|
author: M_Viper
|
||||||
description: Ingame-Shop Plugin (Spigot) empfPlugin-Messages vom Bungee und vergibt Items / zieht Geld via Vault.
|
|
||||||
|
|
||||||
# Vault ist optional, daher softdepend
|
|
||||||
softdepend:
|
|
||||||
- Vault
|
|
||||||
|
|
||||||
# Beispiel-Command (optional, kann in Java implementiert werden)
|
|
||||||
commands:
|
commands:
|
||||||
ingameshop:
|
orders:
|
||||||
description: oder testet den IngameShop (Admin/Debug)
|
description: Zeigt deine letzten Bestellungen im Shop an
|
||||||
usage: /ingameshop
|
usage: /orders
|
||||||
|
permission: ingameshop.orders
|
||||||
|
|
||||||
# Beispiel-Permissions (anpassbar)
|
|
||||||
permissions:
|
permissions:
|
||||||
ingameshop.admin:
|
ingameshop.orders:
|
||||||
description: Zugriff auf Admin-Funktionen des IngameShops
|
description: Kann eigene Bestellhistorie einsehen
|
||||||
default: op
|
|
||||||
ingameshop.use:
|
|
||||||
description: Basisrecht, um Shop-Features zu nutzen
|
|
||||||
default: true
|
default: true
|
||||||
495
README.md
@@ -1,205 +1,382 @@
|
|||||||
# WP Ingame Shop Pro – NO RCON & CUSTOM CURRENCY
|
# WP Ingame Shop Pro
|
||||||
|
|
||||||
Vollautomatischer, Minecraft-basierter Ingame-Shop für WordPress mit Warenkorb, Daily Deal, Kategorien, Gutscheinen und eigenem Währungssystem – komplett ohne RCON, ideal für Bungee-/Proxy-Setups mit externer Ingame-Bestätigung.
|
Ein leistungsstarker, vollständig eigenständiger Ingame-Shop für WordPress, entwickelt für Minecraft-Server (Spigot/Paper).
|
||||||
|
Er bietet ein komfortables Warenkorb-System, Gutscheine, Daily Deals und eine benutzerdefinierte Währung – komplett ohne RCON.
|
||||||
|
|
||||||
## Features
|
Dieses Plugin nutzt eine eigene Datenbankstruktur für maximale Performance und vollständige Unabhängigkeit von WordPress-Beiträgen.
|
||||||
|
|
||||||
- 🛒 **Warenkorb-System**: Spieler können mehrere Items gleichzeitig kaufen, alles wird als eine Bestellung in der Datenbank gespeichert.
|
---
|
||||||
- 💾 **Saubere Order-Struktur**: Eine Bestellung = eine Zeile in `wp_wis_orders` mit JSON-Detaildaten (Items + Gutschein).
|
|
||||||
- 📋 **Detail-Ansicht im Backend**: Übersicht wie „1x Diamant, 64x Stein“ inkl. Gutschein und Debug-JSON.
|
|
||||||
- 💰 **Eigene Währung**: Frei konfigurierbarer Währungsname (z. B. Coins, Tokens) statt Euro.
|
|
||||||
- 🖼️ **Auto-Itembilder**: Bilder werden automatisch aus einer Basis-URL + Item-ID (`minecraft:diamond` → `minecraft_diamond.png`) generiert.
|
|
||||||
- 🔥 **Angebote & Sales**: Items können als Angebot markiert und mit eigenem Angebots-Preis versehen werden.
|
|
||||||
- 🎁 **Daily Deal**: Automatisches „Angebot des Tages“ mit frei einstellbarem prozentualem Rabatt, gesteuert über Cron.
|
|
||||||
- 🎫 **Smart Coupons**: Gutscheine als Festbetrag oder Prozent, optional keine Wirkung auf Angebot-Items (Sale).
|
|
||||||
- 🏷️ **Kategorien**: Eigene Taxonomie `wis_category` mit Backend-Tabs/Filter im Shop-Frontend.
|
|
||||||
- 🏆 **Top Spender**: Statistikseite mit Summe aller abgeschlossenen/cancelled Orders pro Spieler.
|
|
||||||
- 🌐 **REST API**: Endpoints für Orders, Pending Orders, Ausführung, Abschluss, Storno, Gutscheinprüfung und Bulk-Import.
|
|
||||||
- 📥 **Bulk-Import**: JSON-Import von Items (z. B. von `minecraft-ids.com`) in Batches inkl. Fortschrittsanzeige.
|
|
||||||
- 🧹 **Bulk Delete**: Massenlöschung für Items, Server und Gutscheine mit Admin-Notice.
|
|
||||||
- 🎮 **Ingame-Integration ready**: Orders werden über REST abgeholt und serverseitig (z. B. Plugin/Bot) bestätigt, kein RCON notwendig.
|
|
||||||
|
|
||||||
## Installation
|
## ✨ Highlights
|
||||||
|
|
||||||
1. Plugin-Datei (`wp-ingame-shop-pro.php` o. ä.) in den Ordner `wp-content/plugins/wp-ingame-shop-pro` legen.
|
- 🛒 **Warenkorb-System**
|
||||||
2. Im WordPress-Backend unter „Plugins“ das Plugin **aktivieren**.
|
Mehrere Items pro Bestellung.
|
||||||
3. Bei Aktivierung werden automatisch die Tabellen `wp_wis_orders` und `wp_wis_coupons` erstellt sowie Cron-Events für den Daily Deal registriert.
|
Eine Bestellung = eine saubere Datenbankzeile mit JSON-Details.
|
||||||
4. Cron muss auf dem Server funktionieren (WP-Cron oder System-Cron, der `wp-cron.php` triggert).
|
|
||||||
|
|
||||||
## Admin-Bereich
|
- ⚡ **Mehrfachbearbeitung (Bulk Edit)**
|
||||||
|
Hunderte Items gleichzeitig bearbeiten (Preis, Server, Kategorie, Status).
|
||||||
|
|
||||||
Nach der Aktivierung erscheint im Backend das Menü **„Ingame Shop“** mit mehreren Unterseiten.
|
- 💰 **Eigene Währung**
|
||||||
|
„Coins“, „Tokens“ oder beliebige virtuelle Währungen.
|
||||||
|
|
||||||
### Menüs
|
- 🖼️ **Automatische Bildzuweisung**
|
||||||
|
`minecraft:diamond` → `minecraft_diamond.png`
|
||||||
|
Ideal für große Bildsammlungen (1500+ Dateien).
|
||||||
|
|
||||||
- **Ingame Shop (Übersicht)**
|
- 🔥 **Daily Deal & Sale-System**
|
||||||
- Globale Einstellungen, Feature-Übersicht, Bulk-Import.
|
|
||||||
- **Bestellungen**
|
|
||||||
- Liste aller Orders, Detailansicht, manuelles Löschen/Abschließen.
|
|
||||||
- **Top Spender**
|
|
||||||
- Rangliste nach ausgegebenen Coins (inkl. Anzahl Bestellungen).
|
|
||||||
- **Items** (CPT `wis_item`)
|
|
||||||
- Verwaltung der Shop-Items inkl. Preis, Serverzuordnung, Kategorie, Angebot, Daily Deal.
|
|
||||||
- **Kategorien** (`wis_category`)
|
|
||||||
- Hierarchische Kategorien für Items (werden im Frontend als Tabs angezeigt).
|
|
||||||
- **Servers** (CPT `wis_server`)
|
|
||||||
- Reine Serverdefinition (Name/Slug), dient zur Filterung und Zuordnung im Shop.
|
|
||||||
- **Gutscheine** (CPT `wis_coupon`)
|
|
||||||
- Verwaltung von Gutscheincodes inkl. Typ, Wert, Limit, Ablaufdatum.
|
|
||||||
|
|
||||||
### Globale Einstellungen
|
- 🎫 **Smart Coupons**
|
||||||
|
Prozent- oder Festbetrag, optional nicht kombinierbar mit Angeboten.
|
||||||
|
|
||||||
Zu finden unter **Ingame Shop → Ingame Shop (Übersicht)**.
|
- 🌐 **REST API ohne RCON**
|
||||||
|
|
||||||
- **Shop Header Text**
|
- 📊 **Top-Spender & Umsatzstatistik**
|
||||||
- Text im grünen Info-Balken im Frontend, leer = kein Balken.
|
|
||||||
- **Währungsname**
|
|
||||||
- Anzeigeeinheit für Preise, z. B. „Coins“.
|
|
||||||
- **Bilder Basis-URL**
|
|
||||||
- Basis-URL, an die `ITEM_ID.png` angehängt wird, z. B. `https://assets.minecraft-ids.com/1_21_10/`.
|
|
||||||
- **Gutscheine bei Angeboten ausschließen**
|
|
||||||
- Wenn aktiviert, wirken Gutscheine nur auf normale Items, nicht auf als Angebot markierte Items.
|
|
||||||
- **Daily Deal aktivieren + Rabatt (%)**
|
|
||||||
- Aktiviert den täglichen Cron, der ein zufälliges Item (mit Preis > 0, bisher kein Daily Deal) auswählt und rabattiert.
|
|
||||||
|
|
||||||
## Custom Post Types & Metaboxen
|
---
|
||||||
|
|
||||||
### Shop Items (`wis_item`)
|
## 🚀 Installation
|
||||||
|
|
||||||
Unter **Ingame Shop → Items**.
|
1. Ordner `wp-ingame-shop-pro` nach:
|
||||||
|
|
||||||
Metabox **„Item Einstellungen“**:
|
wp-content/plugins/
|
||||||
|
|
||||||
- **Status-Anzeige**
|
2. Plugin im WordPress Backend aktivieren.
|
||||||
- Zeigt an, ob das Item aktiv im Shop ist (publish + Preis > 0) oder inaktiv.
|
3. Fertig.
|
||||||
- **Preis (Coins)**
|
|
||||||
- Integer > 0 macht das Item automatisch aktiv (Status publish), 0 schaltet auf Entwurf.
|
|
||||||
- **Item ID (z. B. `minecraft:diamond`)**
|
|
||||||
- Wird für die Bild-URL und das JSON-Payload (Ingame-Give) verwendet.
|
|
||||||
- **Kategorie(n)**
|
|
||||||
- Checkbox-Liste aller `wis_category`-Terms, beeinflusst die Tabs im Frontend.
|
|
||||||
- **Daily Deal (Angebot des Tages)**
|
|
||||||
- Option „manuell als Angebot des Tages setzen“, überschreibt die Automatik bis Mitternacht.
|
|
||||||
- **Angebot / Sale**
|
|
||||||
- Checkbox „Als Angebot markieren 🔥“ + optionaler Angebots-Preis.
|
|
||||||
- Angebots-Preis wird angezeigt, normaler Preis durchgestrichen.
|
|
||||||
- **Beschreibung**
|
|
||||||
- Langtext, der im Frontend unter dem Titel angezeigt wird.
|
|
||||||
- **Server zuweisen**
|
|
||||||
- Checkbox-Liste aller definierten `wis_server`, steuert Sichtbarkeit und Kaufbarkeit pro Server.
|
|
||||||
|
|
||||||
Zusätzlich existiert eine Auto-Status-Logik, die beim Speichern `publish/draft` abhängig vom Preis setzt.
|
Bei Aktivierung werden automatisch erstellt:
|
||||||
|
|
||||||
### Server (`wis_server`)
|
- Datenbanktabellen (`wp_wis_*`)
|
||||||
|
- Cron-Jobs für Daily Deals
|
||||||
|
|
||||||
Metabox „Server Einstellungen“ zeigt nur einen Hinweis, dass keine RCON-Daten notwendig sind.
|
---
|
||||||
|
|
||||||
- Der **Post-Slug** des Servers wird als Server-ID im Frontend/REST verwendet.
|
# 🖼️ Bilder-Konfiguration (Gitea Raw)
|
||||||
|
|
||||||
### Gutscheine (`wis_coupon`)
|
## 📂 Basis-URL für alle Item-Bilder
|
||||||
|
|
||||||
Metabox **„Gutschein Einstellungen“**:
|
```
|
||||||
|
https://git.viper.ipv64.net/M_Viper/WP-Ingame-Shop-Pro/raw/branch/main/images/
|
||||||
|
```
|
||||||
|
|
||||||
- **Gutschein Code** (wird automatisch in Großbuchstaben gespeichert).
|
### 🔧 Eintragen im Plugin
|
||||||
- **Rabattart**: „Festbetrag“ oder „Prozentual“.
|
|
||||||
- **Wert**: Coins oder Prozent, abhängig vom Typ.
|
|
||||||
- **Nutzungslimit**: Wie oft der Gutschein eingelöst werden darf.
|
|
||||||
- **Ablaufdatum** (optional).
|
|
||||||
- Hinweis, wenn global eingestellt ist, dass Gutscheine bei Angeboten nicht gelten.
|
|
||||||
|
|
||||||
Beim Speichern werden die Daten zusätzlich in der Tabelle `wp_wis_coupons` gespiegelt und `used_count` synchronisiert.
|
**Ingame Shop → Einstellungen → Bilder Basis-URL**
|
||||||
|
|
||||||
Beim Löschen eines Gutschein-Posts wird der Eintrag aus `wp_wis_coupons` wieder entfernt.
|
Eintragen:
|
||||||
|
|
||||||
## REST API
|
```
|
||||||
|
https://git.viper.ipv64.net/M_Viper/WP-Ingame-Shop-Pro/raw/branch/main/images/
|
||||||
|
```
|
||||||
|
|
||||||
Alle Routen liegen unter `wis/v1/…`.
|
---
|
||||||
|
|
||||||
- `POST /wis/v1/order`
|
## 📌 Automatische Bildlogik
|
||||||
- Erstellt eine neue Bestellung.
|
|
||||||
- Request-Body: `{ player, cart: [{id, quantity}], server, coupon_code? }`.
|
|
||||||
- Validiert Items (Preis > 0, Serverzuordnung, Angebot/Normal, Gutscheinlogik) und speichert Order inkl. JSON mit realen Item-IDs und ggf. Gutschein-Rabatt.
|
|
||||||
- `GET /wis/v1/pending_orders?player=Name`
|
|
||||||
- Liefert bis zu 10 offene Bestellungen (`status = pending`) eines Spielers.
|
|
||||||
- `POST /wis/v1/execute_order`
|
|
||||||
- Setzt Status einer Bestellung auf `processing` (z. B. wenn der Ingame-Executor mit der Ausgabe beginnt).
|
|
||||||
- `POST /wis/v1/complete_order`
|
|
||||||
- Setzt Status auf `completed`.
|
|
||||||
- `POST /wis/v1/cancel_order`
|
|
||||||
- Setzt Status auf `cancelled`.
|
|
||||||
- `POST /wis/v1/validate_coupon`
|
|
||||||
- Prüft einen Gutschein gegen aktuelle Cart-Daten (inkl. „nur normale Items“ Logik) und liefert Typ, Wert und Message.
|
|
||||||
- `POST /wis/v1/fetch_remote_data`
|
|
||||||
- Lädt JSON von externer URL (z. B. `https://minecraft-ids.com/data/1.21.10.json`) und gibt eine normalisierte Items-Liste zurück.
|
|
||||||
- `POST /wis/v1/import_batch`
|
|
||||||
- Importiert ein Items-Array als `wis_item`-Posts (draft, `_wis_item_id`, `_wis_price` = 0), überspringt Duplikate.
|
|
||||||
|
|
||||||
Alle REST-Endpunkte sind aktuell ohne Authentifizierung (`permission_callback => __return_true`), für öffentliche Nutzung sollte gegebenenfalls Absicherung ergänzt werden.
|
Item-ID im Shop:
|
||||||
|
|
||||||
## Frontend Shortcode
|
```
|
||||||
|
minecraft:diamond
|
||||||
|
```
|
||||||
|
|
||||||
Der Shop wird per Shortcode eingebunden:
|
Automatisch geladen wird:
|
||||||
|
|
||||||
```text
|
```
|
||||||
|
https://git.viper.ipv64.net/M_Viper/WP-Ingame-Shop-Pro/raw/branch/main/images/minecraft_diamond.png
|
||||||
|
```
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
|
||||||
|
- `:` wird zu `_`
|
||||||
|
- Dateiendung = `.png`
|
||||||
|
- Keine manuelle Bildzuweisung notwendig
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📥 Quick JSON Import (Direkt von Gitea)
|
||||||
|
|
||||||
|
## 🔗 Import-URL
|
||||||
|
|
||||||
|
```
|
||||||
|
https://git.viper.ipv64.net/M_Viper/WP-Ingame-Shop-Pro/raw/branch/main/items.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Import durchführen
|
||||||
|
|
||||||
|
1. **Ingame Shop → JSON Tools**
|
||||||
|
2. **Import**
|
||||||
|
3. URL einfügen
|
||||||
|
4. Import starten
|
||||||
|
|
||||||
|
Ideal für:
|
||||||
|
|
||||||
|
- Kompletten Shop-Aufbau in Sekunden
|
||||||
|
- Regelmäßige Synchronisation
|
||||||
|
- Kombination mit Bulk Edit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🛠️ Admin-Bereich
|
||||||
|
|
||||||
|
## 1️⃣ Einstellungen
|
||||||
|
|
||||||
|
- Shop Header Text
|
||||||
|
- Währungsname (z.B. Coins)
|
||||||
|
- Bilder Basis-URL
|
||||||
|
- Daily Deal (% Rabatt)
|
||||||
|
- Gutscheine auf Sale-Items erlauben / verbieten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2️⃣ Items Verwaltung
|
||||||
|
|
||||||
|
### Einzelbearbeitung
|
||||||
|
|
||||||
|
- Item ID (`minecraft:diamond`)
|
||||||
|
- Preis
|
||||||
|
- Angebotspreis
|
||||||
|
- Server-Zuweisung
|
||||||
|
- Kategorie
|
||||||
|
- Status (`publish` / `draft`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Bulk Edit (Mehrfachbearbeitung)
|
||||||
|
|
||||||
|
Beispiel: 100 Items einem Server zuweisen.
|
||||||
|
|
||||||
|
1. Items anhaken
|
||||||
|
2. „Massenaktionen“ wählen:
|
||||||
|
- Server zuweisen
|
||||||
|
- Preis ändern
|
||||||
|
- Kategorie setzen
|
||||||
|
- Status ändern
|
||||||
|
3. Anwenden
|
||||||
|
4. Änderungen speichern
|
||||||
|
|
||||||
|
Überschreibt bestehende Server-/Kategorie-Zuweisungen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3️⃣ Weitere Bereiche
|
||||||
|
|
||||||
|
### 📦 Bestellungen
|
||||||
|
|
||||||
|
- Bestellungen anzeigen
|
||||||
|
- Manuell abschließen
|
||||||
|
- Stornieren
|
||||||
|
|
||||||
|
Status:
|
||||||
|
|
||||||
|
```
|
||||||
|
pending
|
||||||
|
processing
|
||||||
|
completed
|
||||||
|
cancelled
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🌐 Server
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
|
||||||
|
```
|
||||||
|
lobby
|
||||||
|
survival
|
||||||
|
skyblock
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontend-Filter werden automatisch generiert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📂 Kategorien
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
|
||||||
|
```
|
||||||
|
Blöcke
|
||||||
|
Waffen
|
||||||
|
Rüstung
|
||||||
|
Ranks
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎫 Gutscheine
|
||||||
|
|
||||||
|
- `fixed` (Festbetrag)
|
||||||
|
- `percent` (Prozent)
|
||||||
|
- Ablaufdatum
|
||||||
|
- Nutzungslimit
|
||||||
|
- Ausschluss bei Angeboten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📊 Top Spender
|
||||||
|
|
||||||
|
Automatische Auswertung der Bestellungen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🎮 Frontend Nutzung
|
||||||
|
|
||||||
|
Shortcode:
|
||||||
|
|
||||||
|
```
|
||||||
[ingame_shop_form]
|
[ingame_shop_form]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Layout & Funktionen
|
---
|
||||||
|
|
||||||
- Modernes Grid mit Karten (Bild, Name, Preis, Server, Badges, Menge).
|
## Spieler-Funktionen
|
||||||
- Filter: Suche, „Nur Angebote“, Server-Auswahl und Kategorie-Tabs.
|
|
||||||
- Badges:
|
|
||||||
- „🔥 Angebot“ für Sale-Items.
|
|
||||||
- „🎁 Angebot des Tages“ für den aktiven Daily Deal.
|
|
||||||
- Mengensteuerung je Item (Buttons + Input).
|
|
||||||
- Warenkorb-Button (Badge mit Gesamtanzahl der Items) öffnet ein Modal.
|
|
||||||
- Gutscheincode-Feld im Checkout mit Live-Validierung via `validate_coupon`.
|
|
||||||
- Dynamische Berechnung:
|
|
||||||
- Normaler Teil des Warenkorbs, ggf. rabattiert.
|
|
||||||
- Angebots-Items, die optional von Gutscheinen ausgenommen sind.
|
|
||||||
- Validierung:
|
|
||||||
- Alle ausgewählten Items müssen für den gewählten Server freigeschaltet sein.
|
|
||||||
- Spielername und Server sind Pflichtfelder.
|
|
||||||
- Beim Abschließen des Kaufs wird `POST /wis/v1/order` aufgerufen, die Antwort wird im Modal angezeigt und der Warenkorb geleert.
|
|
||||||
|
|
||||||
## Datenbank-Struktur
|
- Suche
|
||||||
|
- Server-Filter
|
||||||
|
- Kategorie-Tabs
|
||||||
|
- Warenkorb
|
||||||
|
- Gutschein-Eingabe
|
||||||
|
- Server-Validierung beim Checkout
|
||||||
|
|
||||||
### Tabelle `wp_wis_orders`
|
---
|
||||||
|
|
||||||
- `id` – Auto-Increment.
|
# 📡 REST API
|
||||||
- `player_name` – Spielername.
|
|
||||||
- `server` – Server-Slug (`wis_server` Postname).
|
|
||||||
- `item_id` – Hier für den Warenkorb pauschal `multi_item_cart`.
|
|
||||||
- `item_title` – Lesbare Zusammenfassung (z. B. „Warenkorb: 1x Diamant, 64x Stein…“).
|
|
||||||
- `price` – Endpreis nach Rabatt.
|
|
||||||
- `quantity` – Anzahl der unterschiedlichen Items im Warenkorb.
|
|
||||||
- `status` – `pending`, `processing`, `completed`, `cancelled`, `failed`.
|
|
||||||
- `response` – JSON mit `items` (real Item IDs + Mengen) und optionalem `coupon`.
|
|
||||||
- `created_at` – Timestamp.
|
|
||||||
|
|
||||||
### Tabelle `wp_wis_coupons`
|
Basis:
|
||||||
|
|
||||||
- `id` – Auto-Increment.
|
```
|
||||||
- `code` – Gutschein-Code (unique).
|
/wp-json/wis/v1/
|
||||||
- `value` – Wert (Coins oder Prozent).
|
```
|
||||||
- `type` – `fixed` oder `percent`.
|
|
||||||
- `usage_limit` – Maximal verwendbar.
|
|
||||||
- `used_count` – Bisherige Verwendung.
|
|
||||||
- `expiry` – Ablaufdatum.
|
|
||||||
- `created_at` – Timestamp.
|
|
||||||
|
|
||||||
## Cron & Daily Deal
|
---
|
||||||
|
|
||||||
- Beim Aktivieren wird ein tägliches Event `wis_daily_deal_event` registriert.
|
## Bestellung erstellen
|
||||||
- Callback `WIS_Activator::run_daily_deal()`:
|
|
||||||
- Deaktiviert das aktuelle Daily-Deal-Item (`_wis_daily_deal` → 0).
|
|
||||||
- Wählt ein random `wis_item` mit `_wis_price > 0` und ohne `_wis_daily_deal`-Meta.
|
|
||||||
- Berechnet den Rabatt basierend auf `wis_daily_deal_discount` und setzt `_wis_is_offer` + `_wis_offer_price`.
|
|
||||||
|
|
||||||
## Bulk Import
|
```
|
||||||
|
POST /wis/v1/order
|
||||||
|
```
|
||||||
|
|
||||||
- Im Admin-Overview vorhanden.
|
```json
|
||||||
- Eingabefeld für die Import-URL (Default: `https://minecraft-ids.com/data/1.21.10.json`).
|
{
|
||||||
- Button „Daten laden“ → lädt via REST `fetch_remote_data` und zeigt Anzahl Items an.
|
"player": "Name",
|
||||||
- Button „Import starten“ → ruft wiederholt `import_batch` mit Batches von 20 Items auf und zeigt Fortschritt an.
|
"server": "survival",
|
||||||
- Vorhandene Items (gleiche `_wis_item_id`) werden übersprungen, neue Items als Draft mit Preis 0 angelegt.
|
"cart": [
|
||||||
|
{ "id": "item_id", "quantity": 1 }
|
||||||
|
],
|
||||||
|
"coupon_code": "CODE"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Offene Bestellungen abrufen
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /wis/v1/pending_orders?player=Name
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status ändern
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /wis/v1/execute_order
|
||||||
|
POST /wis/v1/complete_order
|
||||||
|
POST /wis/v1/cancel_order
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gutschein validieren
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /wis/v1/validate_coupon
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 💾 Datenbank-Struktur
|
||||||
|
|
||||||
|
## `wp_wis_items`
|
||||||
|
|
||||||
|
| Feld | Beschreibung |
|
||||||
|
|------|-------------|
|
||||||
|
| item_id | minecraft:diamond |
|
||||||
|
| name | Anzeigename |
|
||||||
|
| price | Standardpreis |
|
||||||
|
| offer_price | Angebotspreis |
|
||||||
|
| is_offer | 0 / 1 |
|
||||||
|
| servers | JSON Array |
|
||||||
|
| categories | JSON Array |
|
||||||
|
| status | publish / draft |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `wp_wis_orders`
|
||||||
|
|
||||||
|
| Feld | Beschreibung |
|
||||||
|
|------|-------------|
|
||||||
|
| player_name | Minecraft Name |
|
||||||
|
| server | Zielserver |
|
||||||
|
| item_title | Zusammenfassung |
|
||||||
|
| price | Gesamtpreis |
|
||||||
|
| response | JSON |
|
||||||
|
| status | pending / processing / completed / cancelled |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `wp_wis_coupons`
|
||||||
|
|
||||||
|
| Feld | Beschreibung |
|
||||||
|
|------|-------------|
|
||||||
|
| code | Gutschein |
|
||||||
|
| type | fixed / percent |
|
||||||
|
| value | Rabattwert |
|
||||||
|
| usage_limit | Limit |
|
||||||
|
| used_count | Nutzung |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `wp_wis_servers`
|
||||||
|
|
||||||
|
Serverdefinitionen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `wp_wis_categories`
|
||||||
|
|
||||||
|
Kategoriedefinitionen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# ⚡ Performance Empfehlung
|
||||||
|
|
||||||
|
Bei 1500+ PNG-Dateien:
|
||||||
|
|
||||||
|
- CDN empfohlen
|
||||||
|
- PNG verlustfrei komprimieren
|
||||||
|
- Gitea Caching aktivieren
|
||||||
|
- Optional WebP-Versionen erstellen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔐 Sicherheitshinweis
|
||||||
|
|
||||||
|
Aktuell sind API-Endpunkte öffentlich (`__return_true`).
|
||||||
|
|
||||||
|
Für Produktion empfohlen:
|
||||||
|
|
||||||
|
- API-Key Header
|
||||||
|
- Bearer Token
|
||||||
|
- IP Whitelisting
|
||||||
|
- Nonce/Signature System
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📦 Meta
|
||||||
|
|
||||||
|
Version: 2.1.0
|
||||||
|
Autor: M_Viper
|
||||||
|
Lizenz: GPL-2.0-or-later
|
||||||
|
|||||||
BIN
images/minecraft_acacia_boat.png
Normal file
|
After Width: | Height: | Size: 932 B |
BIN
images/minecraft_acacia_button.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
images/minecraft_acacia_chest_boat.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
images/minecraft_acacia_door.png
Normal file
|
After Width: | Height: | Size: 580 B |
BIN
images/minecraft_acacia_fence.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
images/minecraft_acacia_fence_gate.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
images/minecraft_acacia_hanging_sign.png
Normal file
|
After Width: | Height: | Size: 781 B |
BIN
images/minecraft_acacia_leaves.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
images/minecraft_acacia_log.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
images/minecraft_acacia_planks.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
images/minecraft_acacia_pressure_plate.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
images/minecraft_acacia_sapling.png
Normal file
|
After Width: | Height: | Size: 873 B |
BIN
images/minecraft_acacia_shelf.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
images/minecraft_acacia_sign.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
images/minecraft_acacia_slab.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
images/minecraft_acacia_stairs.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
images/minecraft_acacia_trapdoor.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
images/minecraft_acacia_wood.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
images/minecraft_activator_rail.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
images/minecraft_air.png
Normal file
|
After Width: | Height: | Size: 390 B |
BIN
images/minecraft_allay_spawn_egg.png
Normal file
|
After Width: | Height: | Size: 895 B |
BIN
images/minecraft_allium.png
Normal file
|
After Width: | Height: | Size: 658 B |
BIN
images/minecraft_amethyst_block.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
images/minecraft_amethyst_cluster.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
images/minecraft_amethyst_shard.png
Normal file
|
After Width: | Height: | Size: 764 B |
BIN
images/minecraft_ancient_debris.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
images/minecraft_andesite.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
images/minecraft_andesite_slab.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
images/minecraft_andesite_stairs.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
images/minecraft_andesite_wall.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
images/minecraft_angler_pottery_sherd.png
Normal file
|
After Width: | Height: | Size: 825 B |
BIN
images/minecraft_anvil.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
images/minecraft_apple.png
Normal file
|
After Width: | Height: | Size: 790 B |
BIN
images/minecraft_archer_pottery_sherd.png
Normal file
|
After Width: | Height: | Size: 833 B |
BIN
images/minecraft_armadillo_scute.png
Normal file
|
After Width: | Height: | Size: 648 B |
BIN
images/minecraft_armadillo_spawn_egg.png
Normal file
|
After Width: | Height: | Size: 846 B |
BIN
images/minecraft_armor_stand.png
Normal file
|
After Width: | Height: | Size: 744 B |
BIN
images/minecraft_arms_up_pottery_sherd.png
Normal file
|
After Width: | Height: | Size: 844 B |
BIN
images/minecraft_arrow.png
Normal file
|
After Width: | Height: | Size: 635 B |
BIN
images/minecraft_axolotl_bucket.png
Normal file
|
After Width: | Height: | Size: 977 B |
BIN
images/minecraft_axolotl_spawn_egg.png
Normal file
|
After Width: | Height: | Size: 808 B |
BIN
images/minecraft_azalea.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
images/minecraft_azalea_leaves.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
images/minecraft_azure_bluet.png
Normal file
|
After Width: | Height: | Size: 711 B |
BIN
images/minecraft_baked_potato.png
Normal file
|
After Width: | Height: | Size: 804 B |
BIN
images/minecraft_bamboo.png
Normal file
|
After Width: | Height: | Size: 683 B |
BIN
images/minecraft_bamboo_block.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
images/minecraft_bamboo_button.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
images/minecraft_bamboo_chest_raft.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
images/minecraft_bamboo_door.png
Normal file
|
After Width: | Height: | Size: 879 B |
BIN
images/minecraft_bamboo_fence.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
images/minecraft_bamboo_fence_gate.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
images/minecraft_bamboo_hanging_sign.png
Normal file
|
After Width: | Height: | Size: 963 B |
BIN
images/minecraft_bamboo_mosaic.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
images/minecraft_bamboo_mosaic_slab.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
images/minecraft_bamboo_mosaic_stairs.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
images/minecraft_bamboo_planks.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
images/minecraft_bamboo_pressure_plate.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
images/minecraft_bamboo_raft.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
images/minecraft_bamboo_shelf.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
images/minecraft_bamboo_sign.png
Normal file
|
After Width: | Height: | Size: 909 B |
BIN
images/minecraft_bamboo_slab.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
images/minecraft_bamboo_stairs.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
images/minecraft_bamboo_trapdoor.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
images/minecraft_barrel.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
images/minecraft_barrier.png
Normal file
|
After Width: | Height: | Size: 566 B |
BIN
images/minecraft_basalt.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
images/minecraft_bat_spawn_egg.png
Normal file
|
After Width: | Height: | Size: 787 B |
BIN
images/minecraft_beacon.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
images/minecraft_bedrock.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
images/minecraft_bee_nest.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
images/minecraft_bee_spawn_egg.png
Normal file
|
After Width: | Height: | Size: 840 B |
BIN
images/minecraft_beef.png
Normal file
|
After Width: | Height: | Size: 789 B |
BIN
images/minecraft_beehive.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
images/minecraft_beetroot.png
Normal file
|
After Width: | Height: | Size: 731 B |
BIN
images/minecraft_beetroot_seeds.png
Normal file
|
After Width: | Height: | Size: 614 B |
BIN
images/minecraft_beetroot_soup.png
Normal file
|
After Width: | Height: | Size: 656 B |
BIN
images/minecraft_bell.png
Normal file
|
After Width: | Height: | Size: 786 B |
BIN
images/minecraft_big_dripleaf.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
images/minecraft_birch_boat.png
Normal file
|
After Width: | Height: | Size: 937 B |
BIN
images/minecraft_birch_button.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
images/minecraft_birch_chest_boat.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
images/minecraft_birch_door.png
Normal file
|
After Width: | Height: | Size: 625 B |
BIN
images/minecraft_birch_fence.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
images/minecraft_birch_fence_gate.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
images/minecraft_birch_hanging_sign.png
Normal file
|
After Width: | Height: | Size: 805 B |
BIN
images/minecraft_birch_leaves.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
images/minecraft_birch_log.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
images/minecraft_birch_planks.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
images/minecraft_birch_pressure_plate.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
images/minecraft_birch_sapling.png
Normal file
|
After Width: | Height: | Size: 854 B |
BIN
images/minecraft_birch_shelf.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
images/minecraft_birch_sign.png
Normal file
|
After Width: | Height: | Size: 736 B |
BIN
images/minecraft_birch_slab.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
images/minecraft_birch_stairs.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
images/minecraft_birch_trapdoor.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |