Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 83 additions & 7 deletions src/main/java/com/yourorg/servershop/commands/ShopLogCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

import com.yourorg.servershop.ServerShopPlugin;
import com.yourorg.servershop.logging.Transaction;
import org.bukkit.Material;
import org.bukkit.command.*;

import java.time.ZoneId;
import java.io.File;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

public final class ShopLogCommand implements CommandExecutor {
private final ServerShopPlugin plugin;
Expand All @@ -14,12 +20,28 @@ public final class ShopLogCommand implements CommandExecutor {
public ShopLogCommand(ServerShopPlugin plugin) { this.plugin = plugin; }

@Override public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
int limit = 10; String player = null;
if (args.length >= 1) player = args[0];
if (args.length >= 2) try { limit = Integer.parseInt(args[1]); } catch (Exception ignored) {}
final int flimit = limit; final String fplayer = player;
plugin.logger().lastAsync(fplayer, flimit, list -> {
sender.sendMessage(plugin.prefixed(plugin.getConfig().getString("messages.log-header").replace("%n%", String.valueOf(flimit))));
if (args.length > 0 && args[0].equalsIgnoreCase("export")) {
return handleExport(sender, args);
}

int page = 0; String player = null; Material item = null; String category = null;
for (int i = 0; i < args.length; i++) {
String a = args[i];
switch (a.toLowerCase()) {
case "player": if (i + 1 < args.length) player = args[++i]; break;
case "item": if (i + 1 < args.length) item = Material.matchMaterial(args[++i]); break;
case "category": if (i + 1 < args.length) category = args[++i]; break;
case "page": if (i + 1 < args.length) try { page = Math.max(0, Integer.parseInt(args[++i]) - 1); } catch (Exception ignored) {} break;
default:
if (page == 0) try { page = Math.max(0, Integer.parseInt(a) - 1); } catch (Exception ignored) {}
}
}
final int pageSize = 10;
final int offset = page * pageSize;
final String fPlayer = player; final Material fItem = item; final String fCategory = category; final int fPage = page;
plugin.logger().queryAsync(fPlayer, fItem, fCategory, offset, pageSize, list -> {
sender.sendMessage(plugin.prefixed(plugin.getConfig().getString("messages.log-header").replace("%n%", String.valueOf(pageSize)) + " (page " + (fPage + 1) + ")"));
if (list.isEmpty()) { sender.sendMessage(plugin.prefixed("No entries.")); return; }
for (Transaction t : list) {
String line = plugin.getConfig().getString("messages.log-line");
line = line.replace("%time%", fmt.format(t.time))
Expand All @@ -33,4 +55,58 @@ public final class ShopLogCommand implements CommandExecutor {
});
return true;
}

private boolean handleExport(CommandSender sender, String[] args) {
if (args.length < 2) { sender.sendMessage(plugin.prefixed("Usage: /shoplog export <time> [player <p>] [item <m>] [category <c>]")); return true; }
Duration dur = parseDuration(args[1]);
if (dur == null) { sender.sendMessage(plugin.prefixed("Invalid duration.")); return true; }
String player = null; Material item = null; String category = null;
for (int i = 2; i < args.length; i++) {
String a = args[i];
switch (a.toLowerCase()) {
case "player": if (i + 1 < args.length) player = args[++i]; break;
case "item": if (i + 1 < args.length) item = Material.matchMaterial(args[++i]); break;
case "category": if (i + 1 < args.length) category = args[++i]; break;
}
}
Instant from = Instant.now().minus(dur);
final String fPlayer = player; final Material fItem = item; final String fCategory = category;
plugin.logger().sinceAsync(from, list -> {
List<Transaction> filtered = new ArrayList<>();
for (Transaction t : list) {
if (fPlayer != null && !t.player.equalsIgnoreCase(fPlayer)) continue;
if (fItem != null && t.material != fItem) continue;
if (fCategory != null && !t.category.equalsIgnoreCase(fCategory)) continue;
filtered.add(t);
}
File out = new File(plugin.getDataFolder(), "shoplog-" + System.currentTimeMillis() + ".csv");
try (PrintWriter pw = new PrintWriter(out, StandardCharsets.UTF_8)) {
pw.println("time,player,type,material,quantity,amount,category");
for (Transaction t : filtered) {
pw.println(fmt.format(t.time) + "," + t.player + "," + t.type.name().toLowerCase() + "," + t.material.name() + "," + t.quantity + "," + String.format("%.2f", t.amount) + "," + t.category);
}
} catch (Exception e) {
sender.sendMessage(plugin.prefixed("Export failed: " + e.getMessage()));
return;
}
sender.sendMessage(plugin.prefixed("Exported " + filtered.size() + " entries to " + out.getName()));
});
return true;
}

private Duration parseDuration(String s) {
try {
if (s.length() < 2) return null;
char unit = Character.toLowerCase(s.charAt(s.length() - 1));
long val = Long.parseLong(s.substring(0, s.length() - 1));
switch (unit) {
case 'd': return Duration.ofDays(val);
case 'h': return Duration.ofHours(val);
case 'm': return Duration.ofMinutes(val);
default: return null;
}
} catch (Exception e) {
return null;
}
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/yourorg/servershop/gui/SellMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ private void sellAll(Player p) {
double amount = unit * qty;
total += amount; stacks++;
p.getInventory().setItem(i, null);
plugin.logger().logAsync(new com.yourorg.servershop.logging.Transaction(java.time.Instant.now(), p.getName(), com.yourorg.servershop.logging.Transaction.Type.SELL, m, qty, amount));
String cat = plugin.catalog().categoryOf(m);
plugin.logger().logAsync(new com.yourorg.servershop.logging.Transaction(java.time.Instant.now(), p.getName(), com.yourorg.servershop.logging.Transaction.Type.SELL, m, qty, amount, cat));
// TODO: consider calling plugin.dynamic().adjustOnSell(m, qty) per TODO list
}
if (total > 0) plugin.economy().depositPlayer(p, total);
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/yourorg/servershop/logging/LogStorage.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,18 @@ public interface LogStorage {
void append(Transaction tx) throws Exception;
java.util.List<Transaction> last(int limit) throws Exception;
java.util.List<Transaction> lastOf(String player, int limit) throws Exception;
/**
* Query the log with optional filters.
* @param player player name or null
* @param material material filter or null
* @param category category filter or null
* @param offset starting index (0-based)
* @param limit max number of rows to return
*/
java.util.List<Transaction> query(String player, org.bukkit.Material material, String category, int offset, int limit) throws Exception;
/**
* Get all transactions since the given instant.
*/
java.util.List<Transaction> since(java.time.Instant from) throws Exception;
void close() throws Exception;
}
18 changes: 17 additions & 1 deletion src/main/java/com/yourorg/servershop/logging/LoggerManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,25 @@ public void logAsync(Transaction tx) {
public void close() { try { storage.close(); } catch (Exception ignored) { } }

public void lastAsync(String playerOrNull, int limit, java.util.function.Consumer<List<Transaction>> cb) {
queryAsync(playerOrNull, null, null, 0, limit, cb);
}

public void queryAsync(String player, org.bukkit.Material material, String category, int offset, int limit, java.util.function.Consumer<List<Transaction>> cb) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
List<Transaction> list = storage.query(player, material, category, offset, limit);
Bukkit.getScheduler().runTask(plugin, () -> cb.accept(list));
} catch (Exception e) {
plugin.getLogger().warning("Failed to read log: " + e.getMessage());
Bukkit.getScheduler().runTask(plugin, () -> cb.accept(java.util.Collections.emptyList()));
}
});
}

public void sinceAsync(java.time.Instant from, java.util.function.Consumer<List<Transaction>> cb) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
List<Transaction> list = (playerOrNull == null) ? storage.last(limit) : storage.lastOf(playerOrNull, limit);
List<Transaction> list = storage.since(from);
Bukkit.getScheduler().runTask(plugin, () -> cb.accept(list));
} catch (Exception e) {
plugin.getLogger().warning("Failed to read log: " + e.getMessage());
Expand Down
54 changes: 43 additions & 11 deletions src/main/java/com/yourorg/servershop/logging/SQLLogStorage.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,36 +31,65 @@ private void init() throws Exception {
"material VARCHAR(64) NOT NULL," +
"quantity INT NOT NULL," +
"amount DOUBLE NOT NULL," +
"category VARCHAR(64) NOT NULL," +
"INDEX idx_player_time (player, time_ms)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
try { st.executeUpdate("ALTER TABLE servershop_transactions ADD COLUMN category VARCHAR(64) NOT NULL DEFAULT '' AFTER amount"); } catch (SQLException ignored) {}
}
}

@Override public void append(Transaction tx) throws Exception {
try (Connection c = ds.getConnection(); PreparedStatement ps = c.prepareStatement(
"INSERT INTO servershop_transactions(time_ms, player, type, material, quantity, amount) VALUES (?,?,?,?,?,?)")) {
"INSERT INTO servershop_transactions(time_ms, player, type, material, quantity, amount, category) VALUES (?,?,?,?,?,?,?)")) {
ps.setLong(1, tx.time.toEpochMilli());
ps.setString(2, tx.player);
ps.setString(3, tx.type.name());
ps.setString(4, tx.material.name());
ps.setInt(5, tx.quantity);
ps.setDouble(6, tx.amount);
ps.setString(7, tx.category);
ps.executeUpdate();
}
}

@Override public java.util.List<Transaction> last(int limit) throws Exception { return query(null, limit); }
@Override public java.util.List<Transaction> lastOf(String player, int limit) throws Exception { return query(player, limit); }
@Override public void close() { if (ds != null) ds.close(); }
@Override public java.util.List<Transaction> last(int limit) throws Exception { return query(null, null, null, 0, limit); }
@Override public java.util.List<Transaction> lastOf(String player, int limit) throws Exception { return query(player, null, null, 0, limit); }

private java.util.List<Transaction> query(String player, int limit) throws Exception {
String sql = "SELECT time_ms, player, type, material, quantity, amount FROM servershop_transactions " +
(player != null ? "WHERE player=? " : "") +
"ORDER BY time_ms DESC LIMIT ?";
try (Connection c = ds.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
@Override public java.util.List<Transaction> query(String player, org.bukkit.Material material, String category, int offset, int limit) throws Exception {
StringBuilder sb = new StringBuilder("SELECT time_ms, player, type, material, quantity, amount, category FROM servershop_transactions");
boolean where = false;
if (player != null) { sb.append(" WHERE player=?"); where = true; }
if (material != null) { sb.append(where ? " AND" : " WHERE").append(" material=?"); where = true; }
if (category != null) { sb.append(where ? " AND" : " WHERE").append(" category=?"); }
sb.append(" ORDER BY time_ms DESC LIMIT ? OFFSET ?");
try (Connection c = ds.getConnection(); PreparedStatement ps = c.prepareStatement(sb.toString())) {
int idx = 1;
if (player != null) ps.setString(idx++, player);
ps.setInt(idx, limit);
if (material != null) ps.setString(idx++, material.name());
if (category != null) ps.setString(idx++, category);
ps.setInt(idx++, limit);
ps.setInt(idx, offset);
try (ResultSet rs = ps.executeQuery()) {
java.util.List<Transaction> list = new java.util.ArrayList<>();
while (rs.next()) {
list.add(new Transaction(
java.time.Instant.ofEpochMilli(rs.getLong(1)),
rs.getString(2),
Transaction.Type.valueOf(rs.getString(3)),
org.bukkit.Material.matchMaterial(rs.getString(4)),
rs.getInt(5),
rs.getDouble(6),
rs.getString(7)));
}
return list;
}
}
}

@Override public java.util.List<Transaction> since(java.time.Instant from) throws Exception {
String sql = "SELECT time_ms, player, type, material, quantity, amount, category FROM servershop_transactions WHERE time_ms >= ? ORDER BY time_ms ASC";
try (Connection c = ds.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
ps.setLong(1, from.toEpochMilli());
try (ResultSet rs = ps.executeQuery()) {
java.util.List<Transaction> list = new java.util.ArrayList<>();
while (rs.next()) {
Expand All @@ -70,10 +99,13 @@ private java.util.List<Transaction> query(String player, int limit) throws Excep
Transaction.Type.valueOf(rs.getString(3)),
org.bukkit.Material.matchMaterial(rs.getString(4)),
rs.getInt(5),
rs.getDouble(6)));
rs.getDouble(6),
rs.getString(7)));
}
return list;
}
}
}

@Override public void close() { if (ds != null) ds.close(); }
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ public enum Type { BUY, SELL }
public final Material material;
public final int quantity;
public final double amount;
public final String category;

public Transaction(Instant time, String player, Type type, Material material, int quantity, double amount, String category) {
this.time = time; this.player = player; this.type = type; this.material = material; this.quantity = quantity; this.amount = amount; this.category = category;
}

public Transaction(Instant time, String player, Type type, Material material, int quantity, double amount) {
this.time = time; this.player = player; this.type = type; this.material = material; this.quantity = quantity; this.amount = amount;
this(time, player, type, material, quantity, amount, "");
}
}
56 changes: 49 additions & 7 deletions src/main/java/com/yourorg/servershop/logging/YAMLLogStorage.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,36 +22,78 @@ public YAMLLogStorage(File dataFolder, int maxEntries) {
row.put("material", tx.material.name());
row.put("quantity", tx.quantity);
row.put("amount", tx.amount);
row.put("category", tx.category);
entries.add(row);
while (entries.size() > maxEntries) entries.remove(0);
y.set("entries", entries);
y.save(file);
}

@Override public synchronized java.util.List<Transaction> last(int limit) throws Exception { return filter(null, limit); }
@Override public synchronized java.util.List<Transaction> lastOf(String player, int limit) throws Exception { return filter(player.toLowerCase(java.util.Locale.ROOT), limit); }
@Override public void close() { }
@Override public synchronized java.util.List<Transaction> last(int limit) throws Exception {
return query(null, null, null, 0, limit);
}

private java.util.List<Transaction> filter(String playerLower, int limit) throws Exception {
@Override public synchronized java.util.List<Transaction> lastOf(String player, int limit) throws Exception {
return query(player, null, null, 0, limit);
}

@Override public synchronized java.util.List<Transaction> query(String player, org.bukkit.Material material, String category, int offset, int limit) throws Exception {
YamlConfiguration y = load();
java.util.List<java.util.Map<String, Object>> entries = (java.util.List<java.util.Map<String, Object>>) y.getList("entries", java.util.Collections.emptyList());
java.util.List<Transaction> list = new java.util.ArrayList<>();
for (int i = entries.size() - 1; i >= 0 && list.size() < limit; i--) {
int skipped = 0;
String playerLower = player == null ? null : player.toLowerCase(java.util.Locale.ROOT);
String categoryLower = category == null ? null : category.toLowerCase(java.util.Locale.ROOT);
for (int i = entries.size() - 1; i >= 0; i--) {
java.util.Map<String, Object> e = entries.get(i);
String p = String.valueOf(e.get("player"));
if (playerLower != null && !p.toLowerCase(java.util.Locale.ROOT).equals(playerLower)) continue;
org.bukkit.Material m = org.bukkit.Material.matchMaterial(String.valueOf(e.get("material")));
if (material != null && m != material) continue;
String cat = String.valueOf(e.getOrDefault("category", ""));
if (categoryLower != null && !cat.toLowerCase(java.util.Locale.ROOT).equals(categoryLower)) continue;
if (skipped < offset) { skipped++; continue; }
Transaction t = new Transaction(
java.time.Instant.ofEpochMilli(((Number) e.get("time")).longValue()),
p,
Transaction.Type.valueOf(String.valueOf(e.get("type"))),
org.bukkit.Material.matchMaterial(String.valueOf(e.get("material"))),
m,
((Number) e.get("quantity")).intValue(),
((Number) e.get("amount")).doubleValue(),
cat
);
list.add(t);
if (list.size() >= limit) break;
}
return list;
}

@Override public synchronized java.util.List<Transaction> since(java.time.Instant from) throws Exception {
YamlConfiguration y = load();
java.util.List<java.util.Map<String, Object>> entries = (java.util.List<java.util.Map<String, Object>>) y.getList("entries", java.util.Collections.emptyList());
java.util.List<Transaction> list = new java.util.ArrayList<>();
long min = from.toEpochMilli();
for (java.util.Map<String, Object> e : entries) {
long tms = ((Number) e.get("time")).longValue();
if (tms < min) continue;
String p = String.valueOf(e.get("player"));
org.bukkit.Material m = org.bukkit.Material.matchMaterial(String.valueOf(e.get("material")));
String cat = String.valueOf(e.getOrDefault("category", ""));
Transaction t = new Transaction(
java.time.Instant.ofEpochMilli(tms),
p,
Transaction.Type.valueOf(String.valueOf(e.get("type"))),
m,
((Number) e.get("quantity")).intValue(),
((Number) e.get("amount")).doubleValue()
((Number) e.get("amount")).doubleValue(),
cat
);
list.add(t);
}
return list;
}

@Override public void close() { }

private YamlConfiguration load() { return YamlConfiguration.loadConfiguration(file); }
}
Loading
Loading