Skip to content

Commit 3a5b8ba

Browse files
committed
feat: add Vault and PlaceholderAPI integrations with async logging
1 parent a47dc5f commit 3a5b8ba

4 files changed

Lines changed: 466 additions & 0 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.example.disenchanter.integration;
2+
3+
import com.example.disenchanter.DisenchanterPlugin;
4+
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
5+
import org.bukkit.Bukkit;
6+
import org.bukkit.OfflinePlayer;
7+
import org.jetbrains.annotations.NotNull;
8+
import org.jetbrains.annotations.Nullable;
9+
10+
/**
11+
* Integrates with PlaceholderAPI to provide custom placeholders.
12+
* <p>
13+
* Exports read-only placeholders for plugin status and configuration.
14+
* All placeholders are player-agnostic and safe to query with a null player.
15+
*
16+
* <h3>Provided placeholders</h3>
17+
* <ul>
18+
* <li>{@code %disenchanter_version%} — plugin version</li>
19+
* <li>{@code %disenchanter_vault_enabled%} — {@code true} / {@code false}</li>
20+
* <li>{@code %disenchanter_placeholderapi_enabled%} — {@code true} / {@code false}</li>
21+
* <li>{@code %disenchanter_logging_console%} — {@code true} / {@code false}</li>
22+
* <li>{@code %disenchanter_logging_file%} — {@code true} / {@code false}</li>
23+
* <li>{@code %disenchanter_removal_mode%} — config removal mode string</li>
24+
* </ul>
25+
*/
26+
public class PlaceholderAPIIntegration extends PlaceholderExpansion {
27+
28+
private final DisenchanterPlugin plugin;
29+
private volatile boolean registered = false;
30+
31+
public PlaceholderAPIIntegration(DisenchanterPlugin plugin) {
32+
this.plugin = plugin;
33+
}
34+
35+
@Override
36+
public @NotNull String getIdentifier() {
37+
return "disenchanter";
38+
}
39+
40+
@Override
41+
public @NotNull String getAuthor() {
42+
return String.join(", ", plugin.getDescription().getAuthors());
43+
}
44+
45+
@Override
46+
public @NotNull String getVersion() {
47+
return plugin.getDescription().getVersion();
48+
}
49+
50+
@Override
51+
public boolean persist() {
52+
return true;
53+
}
54+
55+
@Override
56+
public @Nullable String onRequest(OfflinePlayer player, @NotNull String params) {
57+
// All placeholders are player-agnostic; player may be null.
58+
return switch (params) {
59+
case "version" -> plugin.getDescription().getVersion();
60+
case "vault_enabled" -> String.valueOf(plugin.getVaultIntegration().isEnabled());
61+
case "placeholderapi_enabled" -> String.valueOf(
62+
Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null);
63+
case "logging_console" -> String.valueOf(plugin.getConfigManager().isConsoleLogging());
64+
case "logging_file" -> String.valueOf(plugin.getConfigManager().isFileLogging());
65+
case "removal_mode" -> plugin.getConfigManager().getRemovalMode();
66+
default -> null; // unknown placeholder
67+
};
68+
}
69+
70+
/**
71+
* Register this expansion with PlaceholderAPI if the plugin is present.
72+
* This method does NOT override the parent's {@code boolean register()};
73+
* it is a separate setup hook called from {@code DisenchanterPlugin#onEnable}.
74+
*
75+
* @return true if the expansion was successfully registered
76+
*/
77+
public boolean tryRegister() {
78+
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") == null) {
79+
plugin.getLogger().info("PlaceholderAPI not found. Placeholder expansion skipped.");
80+
return false;
81+
}
82+
83+
// isRegistered() is final in parent; use our own flag
84+
if (registered) {
85+
plugin.getLogger().info("PlaceholderAPI expansion already registered; skipping.");
86+
return true;
87+
}
88+
89+
boolean result = register(); // calls PlaceholderExpansion#register() -> PlaceholderAPI
90+
if (result) {
91+
registered = true;
92+
plugin.getLogger().info("PlaceholderAPI expansion registered.");
93+
} else {
94+
plugin.getLogger().warning("PlaceholderAPI expansion registration failed.");
95+
}
96+
return result;
97+
}
98+
99+
/**
100+
* @return true if this expansion has been registered via {@link #tryRegister()}
101+
*/
102+
public boolean isExpansionRegistered() {
103+
return registered;
104+
}
105+
106+
/**
107+
* Unregister this expansion from PlaceholderAPI.
108+
* Safe to call even if not registered.
109+
*
110+
* @return true if the expansion was unregistered
111+
*/
112+
public boolean unregisterExpansion() {
113+
registered = false;
114+
// Call parent's final unregister()
115+
return super.unregister();
116+
}
117+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.example.disenchanter.integration;
2+
3+
import com.example.disenchanter.DisenchanterPlugin;
4+
import net.milkbowl.vault.economy.Economy;
5+
import org.bukkit.Bukkit;
6+
import org.bukkit.plugin.RegisteredServiceProvider;
7+
8+
/**
9+
* Integrates with Vault for economy support.
10+
* <p>
11+
* Sets up Vault Economy provider if available.
12+
* Money-based cost transactions are handled by {@link com.example.disenchanter.cost.MoneyCost}.
13+
* <p>
14+
* Warning messages are emitted at most once per plugin lifecycle to avoid
15+
* log spam on reload.
16+
*/
17+
public class VaultIntegration {
18+
19+
private final DisenchanterPlugin plugin;
20+
private Economy economy;
21+
private boolean warningShown;
22+
23+
public VaultIntegration(DisenchanterPlugin plugin) {
24+
this.plugin = plugin;
25+
}
26+
27+
/**
28+
* Attempt to hook into Vault's economy provider.
29+
* Logs a single warning if Vault is not present or no economy provider is registered.
30+
* Safe to call on reload (warning shown only once).
31+
*/
32+
public void setup() {
33+
if (Bukkit.getPluginManager().getPlugin("Vault") == null) {
34+
if (!warningShown) {
35+
plugin.getLogger().warning("Vault not found. Money-based costs will be disabled.");
36+
warningShown = true;
37+
}
38+
economy = null;
39+
return;
40+
}
41+
42+
RegisteredServiceProvider<Economy> rsp = Bukkit.getServicesManager().getRegistration(Economy.class);
43+
if (rsp == null) {
44+
if (!warningShown) {
45+
plugin.getLogger().warning("Vault found but no economy provider registered. Money costs disabled.");
46+
warningShown = true;
47+
}
48+
economy = null;
49+
return;
50+
}
51+
52+
economy = rsp.getProvider();
53+
warningShown = false; // reset on successful hook so reload can re-warn if provider disappears
54+
plugin.getLogger().info("Vault economy hooked: " + economy.getName());
55+
}
56+
57+
public Economy getEconomy() {
58+
return economy;
59+
}
60+
61+
public boolean isEnabled() {
62+
return economy != null;
63+
}
64+
65+
/**
66+
* Format a monetary amount using Vault's economy format.
67+
* Falls back to a plain decimal string when Vault is unavailable.
68+
*
69+
* @param amount the amount to format
70+
* @return a formatted currency string
71+
*/
72+
public String format(double amount) {
73+
if (economy != null) {
74+
try {
75+
return economy.format(amount);
76+
} catch (Exception ignored) {
77+
// fall through to fallback
78+
}
79+
}
80+
return String.format("%.2f", amount);
81+
}
82+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package com.example.disenchanter.logging;
2+
3+
import com.example.disenchanter.DisenchanterPlugin;
4+
import org.bukkit.Bukkit;
5+
6+
import java.io.BufferedWriter;
7+
import java.io.File;
8+
import java.io.FileWriter;
9+
import java.io.IOException;
10+
import java.nio.charset.StandardCharsets;
11+
import java.time.LocalDate;
12+
import java.time.LocalDateTime;
13+
import java.time.format.DateTimeFormatter;
14+
import java.util.concurrent.ConcurrentLinkedQueue;
15+
import java.util.concurrent.ExecutorService;
16+
import java.util.concurrent.Executors;
17+
import java.util.concurrent.TimeUnit;
18+
19+
/**
20+
* Handles operation logging to console and file.
21+
* <p>
22+
* Queue-based logging with asynchronous flush to daily log files.
23+
* Uses config's {@code logging.format} string with placeholder replacement:
24+
* <ul>
25+
* <li>{@code {time}} — timestamp</li>
26+
* <li>{@code {player}} — player name</li>
27+
* <li>{@code {uuid}} — player UUID</li>
28+
* <li>{@code {item}} — item type name</li>
29+
* <li>{@code {enchant}} — enchantment key</li>
30+
* <li>{@code {level}} — enchantment level</li>
31+
* <li>{@code {mode}} — removal mode (individual/bulk)</li>
32+
* <li>{@code {cost}} — cost description</li>
33+
* <li>{@code {result}} — result status (success/skip/deny)</li>
34+
* </ul>
35+
*/
36+
public class DisenchantLogger {
37+
38+
private static final DateTimeFormatter TIME_FORMATTER =
39+
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
40+
41+
private final DisenchanterPlugin plugin;
42+
private final ConcurrentLinkedQueue<String> logQueue = new ConcurrentLinkedQueue<>();
43+
private final ExecutorService fileExecutor = Executors.newSingleThreadExecutor(r -> {
44+
Thread t = new Thread(r, "Disenchanter-LogWriter");
45+
t.setDaemon(true);
46+
return t;
47+
});
48+
49+
private File logDir;
50+
private volatile boolean shutdown = false;
51+
52+
public DisenchantLogger(DisenchanterPlugin plugin) {
53+
this.plugin = plugin;
54+
}
55+
56+
/** Initialize logger — create log directory if file logging is enabled. */
57+
public void init() {
58+
boolean fileLogging = plugin.getConfigManager().isFileLogging();
59+
if (fileLogging) {
60+
logDir = new File(plugin.getDataFolder(), "logs");
61+
if (!logDir.exists()) {
62+
logDir.mkdirs();
63+
}
64+
}
65+
}
66+
67+
/**
68+
* Log a disenchant operation.
69+
*
70+
* @param playerName player name
71+
* @param playerUuid player UUID
72+
* @param enchant enchantment display key (e.g. "minecraft:sharpness")
73+
* @param level enchantment level
74+
* @param itemName item type name
75+
* @param cost cost description
76+
* @param mode removal mode label ("individual" or "bulk")
77+
* @param result result label ("success", "skip", "deny")
78+
*/
79+
public void log(String playerName, String playerUuid, String enchant, int level,
80+
String itemName, String cost, String mode, String result) {
81+
if (shutdown) return;
82+
83+
String time = LocalDateTime.now().format(TIME_FORMATTER);
84+
String format = plugin.getConfigManager().getLogFormat();
85+
86+
// Apply placeholder replacement
87+
String message = format
88+
.replace("{time}", time)
89+
.replace("{player}", playerName)
90+
.replace("{uuid}", playerUuid)
91+
.replace("{enchant}", enchant)
92+
.replace("{level}", String.valueOf(level))
93+
.replace("{item}", itemName)
94+
.replace("{cost}", cost)
95+
.replace("{mode}", mode)
96+
.replace("{result}", result);
97+
98+
// Console logging — on main thread is fine
99+
if (plugin.getConfigManager().isConsoleLogging()) {
100+
plugin.getLogger().info("[Disenchant] " + message);
101+
}
102+
103+
// File logging — queue and flush asynchronously
104+
if (plugin.getConfigManager().isFileLogging() && logDir != null) {
105+
logQueue.add(message);
106+
scheduleFlush();
107+
}
108+
}
109+
110+
/** Schedule an async flush of queued log entries using a safe Java executor. */
111+
private void scheduleFlush() {
112+
if (shutdown) return;
113+
fileExecutor.submit(this::flushQueue);
114+
}
115+
116+
/** Flush queued log entries to disk on the executor thread. */
117+
private void flushQueue() {
118+
if (logQueue.isEmpty()) return;
119+
120+
String today = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
121+
File logFile = new File(logDir, "disenchant-" + today + ".log");
122+
123+
try (BufferedWriter writer = new BufferedWriter(
124+
new FileWriter(logFile, StandardCharsets.UTF_8, true))) {
125+
String entry;
126+
while ((entry = logQueue.poll()) != null) {
127+
writer.write(entry);
128+
writer.newLine();
129+
}
130+
} catch (IOException e) {
131+
plugin.getLogger().warning("Failed to write log: " + e.getMessage());
132+
}
133+
}
134+
135+
/**
136+
* Shutdown the logger — flush remaining entries synchronously and
137+
* shut down the async executor. Blocks until all pending writes complete.
138+
*/
139+
public void shutdown() {
140+
shutdown = true;
141+
// Flush remaining queue synchronously
142+
flushQueue();
143+
// Shut down the async executor
144+
fileExecutor.shutdown();
145+
try {
146+
if (!fileExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
147+
fileExecutor.shutdownNow();
148+
plugin.getLogger().warning("Log file executor did not terminate in time.");
149+
}
150+
} catch (InterruptedException e) {
151+
fileExecutor.shutdownNow();
152+
Thread.currentThread().interrupt();
153+
}
154+
}
155+
}

0 commit comments

Comments
 (0)