62 Commits
1.0.2 ... main

Author SHA1 Message Date
15b875a723 wp-ingame-shop/wp-ingame-shop.php aktualisiert 2026-02-17 17:00:38 +00:00
de97f3dea7 Update from Git Manager GUI 2026-02-17 17:58:45 +01:00
42b0cf8279 Upload pom.xml via GUI 2026-02-17 16:58:43 +00:00
1c39ab5557 items.json aktualisiert 2026-02-17 09:04:26 +00:00
38c7327291 items.json aktualisiert 2026-02-17 09:01:58 +00:00
ff9336c8d1 items.json aktualisiert 2026-02-17 08:58:49 +00:00
3567a93918 items.json aktualisiert 2026-02-17 08:16:36 +00:00
615b404ce6 items.json aktualisiert 2026-02-15 21:57:31 +00:00
a2f3260854 wp-ingame-shop/wp-ingame-shop.php aktualisiert 2026-02-15 10:00:44 +00:00
48648c5632 README.md aktualisiert 2026-02-15 09:58:25 +00:00
10f2a66e3a Dateien nach "images" hochladen 2026-02-15 09:40:24 +00:00
e196507634 Dateien nach "images" hochladen 2026-02-15 09:40:05 +00:00
b208dc8592 Dateien nach "images" hochladen 2026-02-15 09:39:40 +00:00
f2f2fb3353 Dateien nach "images" hochladen 2026-02-15 09:39:21 +00:00
5429c91498 Dateien nach "images" hochladen 2026-02-15 09:38:53 +00:00
40febaf7e2 Dateien nach "images" hochladen 2026-02-15 09:38:23 +00:00
9039dcc9ad Dateien nach "images" hochladen 2026-02-15 09:37:58 +00:00
8f1a1de252 Dateien nach "images" hochladen 2026-02-15 09:37:39 +00:00
d107835455 Dateien nach "images" hochladen 2026-02-15 09:37:11 +00:00
a09dad30ec Dateien nach "images" hochladen 2026-02-15 09:36:52 +00:00
18a1991af9 Dateien nach "images" hochladen 2026-02-15 09:36:33 +00:00
6e03ffa962 Dateien nach "images" hochladen 2026-02-15 09:36:03 +00:00
66a95c48fa Dateien nach "images" hochladen 2026-02-15 09:35:43 +00:00
51559f12ed Dateien nach "images" hochladen 2026-02-15 09:35:23 +00:00
605d7f44d5 Dateien nach "images" hochladen 2026-02-15 09:34:46 +00:00
75668031e0 Dateien nach "images" hochladen 2026-02-15 09:34:17 +00:00
3248ad9e8d Dateien nach "images" hochladen 2026-02-15 09:33:53 +00:00
c59e37ef68 Dateien nach "images" hochladen 2026-02-15 09:33:30 +00:00
db4255a0ad Dateien nach "images" hochladen 2026-02-15 09:32:56 +00:00
5e2ae5ba0a Remove old shop images 2026-02-15 10:32:13 +01:00
afeb22422e Dateien nach "images" hochladen 2026-02-15 09:30:18 +00:00
9b84b18952 Dateien nach "images" hochladen 2026-02-15 09:29:59 +00:00
3e32e3af32 Dateien nach "images" hochladen 2026-02-15 09:29:26 +00:00
da6fe6cb33 Dateien nach "images" hochladen 2026-02-15 09:28:41 +00:00
37538a9632 Dateien nach "images" hochladen 2026-02-15 09:28:24 +00:00
e0b63ee2e4 Dateien nach "images" hochladen 2026-02-15 09:28:05 +00:00
d4b5aa350f Dateien nach "images" hochladen 2026-02-15 09:27:44 +00:00
b34d74a62e Dateien nach "images" hochladen 2026-02-15 09:27:20 +00:00
1f36b22916 Dateien nach "images" hochladen 2026-02-15 09:26:59 +00:00
e19378d815 Dateien nach "images" hochladen 2026-02-15 09:26:38 +00:00
4720f3b4b6 Dateien nach "images" hochladen 2026-02-15 09:26:09 +00:00
4832b259a5 Dateien nach "images" hochladen 2026-02-15 09:25:46 +00:00
0ea27c815c Dateien nach "images" hochladen 2026-02-15 09:25:21 +00:00
8510e601d9 Dateien nach "images" hochladen 2026-02-15 09:24:47 +00:00
603ece2f7b Remove old shop images 2026-02-15 10:23:54 +01:00
2e9c2b2579 items.json aktualisiert 2026-02-15 08:57:11 +00:00
bd47187b95 items.json aktualisiert 2026-02-14 22:35:01 +00:00
4d94218e22 items.json aktualisiert 2026-02-14 22:05:19 +00:00
2c8260191f items.json aktualisiert 2026-02-14 22:05:00 +00:00
85b6aa8d88 items.json aktualisiert 2026-02-14 21:45:04 +00:00
6f5b7d7fcf items.json aktualisiert 2026-02-14 21:44:19 +00:00
49a2057705 items.json aktualisiert 2026-02-14 21:30:21 +00:00
25b3716fdc Dateien nach "images" hochladen 2026-02-14 21:09:36 +00:00
1d84f5ffe7 Remove old shop images 2026-02-14 22:08:33 +01:00
9313fb2e7a images/minecraft_acacia_chest_boat.png gelöscht 2026-02-14 21:01:21 +00:00
2426d4654f images/minecraft_acacia_boat.png gelöscht 2026-02-14 21:01:11 +00:00
5284253ce8 items.json aktualisiert 2026-02-14 18:43:51 +00:00
d3a17ff5f1 items.json aktualisiert 2026-02-14 18:10:23 +00:00
fbdd42a856 items.json aktualisiert 2026-02-14 12:35:25 +00:00
12786cb553 Dateien nach "/" hochladen 2026-02-14 12:22:29 +00:00
0e99638960 Upload file prepare-minecraft-items.ps1 via GUI 2026-02-14 09:11:47 +01:00
4f476e478e Update from Git Manager GUI 2026-02-14 08:51:53 +01:00
1496 changed files with 15204 additions and 2124 deletions

View File

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

View File

@@ -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

View File

@@ -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
View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
images/minecraft_air.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

BIN
images/minecraft_allium.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

BIN
images/minecraft_anvil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
images/minecraft_apple.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

BIN
images/minecraft_arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 B

BIN
images/minecraft_azalea.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

BIN
images/minecraft_bamboo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
images/minecraft_barrel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

BIN
images/minecraft_basalt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

BIN
images/minecraft_beacon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

BIN
images/minecraft_beef.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

BIN
images/minecraft_bell.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Some files were not shown because too many files have changed in this diff Show More