diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index cea3d13..fe67953 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -111,7 +111,7 @@ body: id: server-version attributes: label: Server version - placeholder: Paper 1.21.11 build ... + placeholder: Paper 26.1.2 build ... validations: required: true @@ -128,7 +128,7 @@ body: id: client-version attributes: label: Minecraft client version - placeholder: 1.21.11 + placeholder: 26.1.2 validations: required: false diff --git a/.github/workflows/promote-release.yml b/.github/workflows/promote-release.yml index fb7f364..c4b5734 100644 --- a/.github/workflows/promote-release.yml +++ b/.github/workflows/promote-release.yml @@ -319,7 +319,6 @@ jobs: paper folia game-versions: |- - 1.21.x 26.1.x 26.2.x files: ${{ steps.asset.outputs.jar }} @@ -342,5 +341,5 @@ jobs: ] platform_dependencies: |- { - "PAPER": ["1.21.x", "26.1.x", "26.2.x"] + "PAPER": ["26.1.x", "26.2.x"] } \ No newline at end of file diff --git a/.github/workflows/publish-marketplaces.yml b/.github/workflows/publish-marketplaces.yml index ea11f66..fe7dac9 100644 --- a/.github/workflows/publish-marketplaces.yml +++ b/.github/workflows/publish-marketplaces.yml @@ -100,8 +100,8 @@ jobs: paper folia game-versions: |- - 1.21.x 26.1.x + 26.2.x files: ${{ steps.asset.outputs.jar }} - name: Publish to Hangar @@ -121,5 +121,5 @@ jobs: ] platform_dependencies: |- { - "PAPER": ["1.21.x", "26.1.x"] + "PAPER": ["26.1.x", "26.2.x"] } \ No newline at end of file diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/HeadDBPlugin.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/HeadDBPlugin.java index bdf349c..2dbd579 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/HeadDBPlugin.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/HeadDBPlugin.java @@ -30,6 +30,7 @@ import io.github.silentdevelopment.headdb.paper.message.Messages; import io.github.silentdevelopment.headdb.paper.metrics.HeadDBMetrics; import io.github.silentdevelopment.headdb.paper.prompt.PromptInputService; +import io.github.silentdevelopment.headdb.paper.runtime.PlatformRequirements; import io.github.silentdevelopment.headdb.paper.runtime.PluginRuntime; import io.github.silentdevelopment.headdb.paper.runtime.RuntimeDiagnostics; import io.github.silentdevelopment.headdb.paper.runtime.StartupChecks; @@ -69,6 +70,11 @@ public void onEnable() { return; } + if (!PlatformRequirements.supported(this)) { + getServer().getPluginManager().disablePlugin(this); + return; + } + try { reload(); diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/RootCommand.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/RootCommand.java index 739d67a..dcef098 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/RootCommand.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/RootCommand.java @@ -16,6 +16,7 @@ import io.github.silentdevelopment.headdb.paper.command.subcommand.RandomCommand; import io.github.silentdevelopment.headdb.paper.command.subcommand.RefreshCommand; import io.github.silentdevelopment.headdb.paper.command.subcommand.ReloadCommand; +import io.github.silentdevelopment.headdb.paper.command.subcommand.ReportCommand; import io.github.silentdevelopment.headdb.paper.command.subcommand.StatusCommand; import io.github.silentdevelopment.headdb.paper.command.subcommand.TagsCommand; import io.github.silentdevelopment.headdb.paper.command.subcommand.UpdateCommand; @@ -47,6 +48,7 @@ public RootCommand(@NotNull HeadDBPlugin plugin) { new VersionCommand(plugin), new StatusCommand(plugin), new DebugCommand(plugin), + new ReportCommand(plugin), new VerifyCommand(plugin), new RefreshCommand(plugin), new ReloadCommand(plugin), diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/format/HelpFormatter.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/format/HelpFormatter.java index cd1e1dc..a347d60 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/format/HelpFormatter.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/format/HelpFormatter.java @@ -36,14 +36,14 @@ private HelpFormatter() { addSection(lines, sender, new HelpSection("General", List.of( new HelpEntry(List.of("help"), "h", List.of(), "/hdb help", "Show this command reference.", Permissions.HELP), new HelpEntry(List.of("version"), null, List.of(), "/hdb version", "Show version and build information.", Permissions.VERSION), - new HelpEntry(List.of("open"), "o", List.of(), "/hdb open", "Open the main HeadDB GUI.", Permissions.OPEN) + new HelpEntry(List.of("open"), "o", List.of(), "/hdb open", "Open the main GUI.", Permissions.OPEN) ))); addSection(lines, sender, new HelpSection("Heads", List.of( new HelpEntry(List.of("info"), "i", args(optional("id")), "/hdb info ", "Inspect a head by ID or held item.", Permissions.INFO), - new HelpEntry(List.of("give"), "g", args(required("id"), optional("player"), optional("amount")), "/hdb give ", "Give a HeadDB item.", Permissions.GIVE), + new HelpEntry(List.of("give"), "g", args(required("id"), optional("player"), optional("amount")), "/hdb give ", "Give a head.", Permissions.GIVE), new HelpEntry(List.of("player"), "p", args(required("name|uuid"), optional("player"), optional("amount")), "/hdb player ", "Give a player head.", Permissions.PLAYER), - new HelpEntry(List.of("random"), "rnd", args(optional("amount"), optional("category"), optional("player")), "/hdb random ", "Give a random HeadDB item.", Permissions.GIVE) + new HelpEntry(List.of("random"), "rnd", args(optional("amount"), optional("category"), optional("player")), "/hdb random ", "Give a random head.", Permissions.GIVE) ))); addSection(lines, sender, new HelpSection("Browse", List.of( @@ -73,7 +73,8 @@ private HelpFormatter() { addSection(lines, sender, new HelpSection("Database", List.of( new HelpEntry(List.of("status"), "st", List.of(), "/hdb status", "Show database state and counts.", Permissions.STATUS), - new HelpEntry(List.of("debug"), "d", List.of(), "/hdb debug", "Show detailed runtime diagnostics.", Permissions.DEBUG), + new HelpEntry(List.of("debug"), "d", List.of(), "/hdb debug", "Show concise runtime diagnostics.", Permissions.DEBUG), + new HelpEntry(List.of("report"), "rpt", List.of(), "/hdb report", "Create a full support report.", Permissions.REPORT), new HelpEntry(List.of("verify"), "v", List.of(), "/hdb verify", "Verify the public remote without replacing the active database.", Permissions.VERIFY), new HelpEntry(List.of("refresh"), "ref", List.of(), "/hdb refresh", "Fetch the latest remote database.", Permissions.REFRESH), new HelpEntry(List.of("reload"), "rl", List.of(), "/hdb reload", "Reload config, messages, and runtime.", Permissions.RELOAD) diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/format/StatusFormatter.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/format/StatusFormatter.java index 668dbb1..26ba69b 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/format/StatusFormatter.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/format/StatusFormatter.java @@ -2,9 +2,9 @@ import io.github.silentdevelopment.headdb.database.DatabaseStats; import io.github.silentdevelopment.headdb.database.DatabaseStatus; +import io.github.silentdevelopment.headdb.paper.HeadDBPlugin; import io.github.silentdevelopment.headdb.paper.permission.Permissions; import io.github.silentdevelopment.headdb.paper.runtime.RefreshState; -import io.github.silentdevelopment.headdb.paper.runtime.PluginRuntime; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; @@ -16,6 +16,7 @@ import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -26,77 +27,126 @@ public final class StatusFormatter { private StatusFormatter() { } - public static @NotNull List format(@NotNull PluginRuntime runtime, @NotNull CommandSender sender) { - Objects.requireNonNull(runtime, "runtime"); + public static @NotNull List format(@NotNull HeadDBPlugin plugin, @NotNull CommandSender sender) { + Objects.requireNonNull(plugin, "plugin"); Objects.requireNonNull(sender, "sender"); - DatabaseStatus status = runtime.database().status(); - DatabaseStats stats = runtime.database().stats(); - RefreshState refresh = runtime.refreshState(); - - return List.of( - Component.empty(), - Component.text("> ", NamedTextColor.GRAY).append(Component.text("HeadDB Status", NamedTextColor.RED)), - databaseStatusLine(status, sender), - line("Source", status.source()), - line("Manifest ID", value(status.manifestId())), - line("Catalog ID", value(status.artifactId())), - line("Loaded at", formatInstant(status.loadedAt())), - Component.empty(), - line("Heads", stats.heads()), - line("Categories", stats.categories()), - line("Tags", stats.tags()), - line("Collections", stats.collections()), - line("Revocations", stats.revocations()), - Component.empty(), - line("Refresh running", yesNo(refresh.running())), - line("Last failure", formatFailure(refresh.lastFailureMessage())), - Component.empty() - ); + DatabaseStatus status = plugin.runtime().database().status(); + DatabaseStats remoteStats = plugin.runtime().database().stats(); + RefreshState refresh = plugin.runtime().refreshState(); + + int hiddenHeads = plugin.headRegistry().hiddenHeads().size(); + int moreHeads = plugin.headRegistry().customHeads().list().size(); + int overrides = plugin.headRegistry().overrides().list().size(); + int playerHeads = plugin.headRegistry().playerHeads().knownPlayers().size(); + int moreCategories = plugin.customCategories().list().size(); + + List lines = new ArrayList<>(); + lines.add(Component.empty()); + lines.add(Component.text("> ", NamedTextColor.DARK_GRAY).append(Component.text("Status", NamedTextColor.RED))); + lines.add(databaseLine(status)); + lines.add(line("Heads", remoteStats.heads())); + lines.add(line("Hidden Heads", hiddenHeads)); + lines.add(line("More Heads", moreHeads)); + lines.add(line("Player Heads", playerHeads)); + lines.add(line("Categories", remoteStats.categories())); + lines.add(line("More Categories", moreCategories)); + lines.add(line("Tags", remoteStats.tags())); + lines.add(line("Collections", remoteStats.collections())); + lines.add(line("Revocations", remoteStats.revocations())); + lines.add(line("Overrides", overrides)); + lines.add(refreshLine(refresh, sender)); + lines.add(lastRefreshLine(refresh)); + + String failure = firstPresent(status.lastError(), refresh.lastFailureMessage()); + if (failure != null) { + lines.add(line("Last error", failure)); + } + + addSupportLine(lines, sender); + lines.add(Component.empty()); + return List.copyOf(lines); } - private static @NotNull Component databaseStatusLine(@NotNull DatabaseStatus status, @NotNull CommandSender sender) { - Component line = Component.text("Status: ", NamedTextColor.GRAY).append(Component.text(String.valueOf(status.state()), statusColor(status))); + private static @NotNull Component databaseLine(@NotNull DatabaseStatus status) { + Component line = Component.text("Database: ", NamedTextColor.GRAY).append(Component.text(String.valueOf(status.state()), statusColor(status))); + String source = value(status.source()); - if (isLoaded(status)) { - return line; + if (!source.equals("none")) { + line = line.append(Component.text(" from ", NamedTextColor.GRAY)).append(Component.text(source, NamedTextColor.GOLD)); } - if (!Permissions.has(sender, Permissions.REFRESH)) { - return line; + return line; + } + + private static @NotNull Component refreshLine(@NotNull RefreshState refresh, @NotNull CommandSender sender) { + String text = refresh.running() ? "running " + refresh.currentOperation() : "idle"; + Component line = line("Refresh", text); + + if (!refresh.running() && Permissions.has(sender, Permissions.REFRESH)) { + line = line.append(Component.text(" ")).append(refreshButton()); } - return line.append(Component.text(" ")).append(refreshButton()); + return line; } - private static boolean isLoaded(@NotNull DatabaseStatus status) { - return "LOADED".equalsIgnoreCase(String.valueOf(status.state())); - } + private static @NotNull Component lastRefreshLine(@NotNull RefreshState refresh) { + if (refresh.lastOutcome() == RefreshState.RefreshOutcome.SUCCESS) { + return line("Last Refresh", refresh.lastOperation() + " completed at " + formatInstant(refresh.lastSuccessfulRefresh())); + } - private static @NotNull NamedTextColor statusColor(@NotNull DatabaseStatus status) { - if (isLoaded(status)) { - return NamedTextColor.GOLD; + if (refresh.lastOutcome() == RefreshState.RefreshOutcome.FAILURE) { + return line("Last Refresh", refresh.lastOperation() + " failed at " + formatInstant(refresh.lastFailedRefresh())); } - return NamedTextColor.RED; + return line("Last Refresh", "never"); } private static @NotNull Component refreshButton() { - return Component.text("[ ", NamedTextColor.DARK_GRAY) - .append(Component.text("REFRESH", NamedTextColor.GOLD).clickEvent(ClickEvent.runCommand("/hdb refresh")).hoverEvent(HoverEvent.showText(Component.text("Click to refresh the HeadDB database.", NamedTextColor.GRAY)))) - .append(Component.text(" ]", NamedTextColor.DARK_GRAY)); + return Component.text("[ ", NamedTextColor.DARK_GRAY).append(Component.text("REFRESH", NamedTextColor.GOLD).clickEvent(ClickEvent.runCommand("/hdb refresh")).hoverEvent(HoverEvent.showText(Component.text("Click to refresh the database.", NamedTextColor.GRAY)))).append(Component.text(" ]", NamedTextColor.DARK_GRAY)); } - private static @NotNull Component line(@NotNull String key, @Nullable Object value) { - return Component.text(key + ": ", NamedTextColor.GRAY).append(Component.text(String.valueOf(value), NamedTextColor.GOLD)); + private static void addSupportLine(@NotNull List lines, @NotNull CommandSender sender) { + boolean canDebug = Permissions.has(sender, Permissions.DEBUG); + boolean canReport = Permissions.has(sender, Permissions.REPORT); + + if (!canDebug && !canReport) { + return; + } + + Component line = Component.text("Support: ", NamedTextColor.GRAY); + + if (canDebug) { + line = line.append(Component.text("/hdb debug", NamedTextColor.GOLD)); + } + + if (canDebug && canReport) { + line = line.append(Component.text(" | ", NamedTextColor.DARK_GRAY)); + } + + if (canReport) { + line = line.append(Component.text("/hdb report", NamedTextColor.GOLD)); + } + + lines.add(line); } - private static @NotNull String yesNo(boolean value) { - if (value) { - return "yes"; + private static @NotNull NamedTextColor statusColor(@NotNull DatabaseStatus status) { + String state = String.valueOf(status.state()); + + if ("LOADED".equalsIgnoreCase(state)) { + return NamedTextColor.GOLD; + } + + if ("LOADING".equalsIgnoreCase(state)) { + return NamedTextColor.YELLOW; } - return "no"; + return NamedTextColor.RED; + } + + private static @NotNull Component line(@NotNull String key, @Nullable Object value) { + return Component.text(key + ": ", NamedTextColor.GRAY).append(Component.text(String.valueOf(value), NamedTextColor.GOLD)); } private static @NotNull String formatInstant(@Nullable Instant instant) { @@ -107,19 +157,35 @@ private static boolean isLoaded(@NotNull DatabaseStatus status) { return TIME_FORMAT.format(instant); } - private static @NotNull String formatFailure(@Nullable String message) { - if (message == null || message.isBlank()) { + private static @NotNull String value(@Nullable Object value) { + if (value == null) { return "none"; } - return message; + String string = String.valueOf(value); + if (string.isBlank()) { + return "none"; + } + + return string; } - private static @NotNull String value(@Nullable Object value) { - if (value == null) { - return "none"; + private static @Nullable String firstPresent(@Nullable String first, @Nullable String second) { + String normalizedFirst = normalize(first); + + if (normalizedFirst != null) { + return normalizedFirst; } - return String.valueOf(value); + return normalize(second); } -} \ No newline at end of file + + private static @Nullable String normalize(@Nullable String value) { + if (value == null || value.isBlank()) { + return null; + } + + return value.trim(); + } + +} diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/format/SupportReport.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/format/SupportReport.java new file mode 100644 index 0000000..53d8db1 --- /dev/null +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/format/SupportReport.java @@ -0,0 +1,230 @@ +package io.github.silentdevelopment.headdb.paper.command.format; + +import io.github.silentdevelopment.headdb.database.DatabaseStats; +import io.github.silentdevelopment.headdb.database.DatabaseStatus; +import io.github.silentdevelopment.headdb.paper.HeadDBPlugin; +import io.github.silentdevelopment.headdb.paper.config.PluginConfig; +import io.github.silentdevelopment.headdb.paper.runtime.BuildInfo; +import io.github.silentdevelopment.headdb.paper.runtime.PlatformRequirements; +import io.github.silentdevelopment.headdb.paper.runtime.RefreshState; +import io.github.silentdevelopment.headdb.paper.updater.GitHubRelease; +import io.github.silentdevelopment.headdb.paper.updater.UpdateCheckResult; +import io.github.silentdevelopment.headdb.paper.updater.UpdateService; +import org.bukkit.Server; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Objects; + +public final class SupportReport { + + private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z").withZone(ZoneId.systemDefault()); + + private SupportReport() { + } + + public static @NotNull String create(@NotNull HeadDBPlugin plugin, @NotNull CommandSender sender) { + Objects.requireNonNull(plugin, "plugin"); + Objects.requireNonNull(sender, "sender"); + + StringBuilder report = new StringBuilder(4096); + BuildInfo buildInfo = BuildInfo.read(plugin); + PluginConfig config = plugin.config(); + DatabaseStatus status = plugin.runtime().database().status(); + DatabaseStats databaseStats = plugin.runtime().database().stats(); + DatabaseStats registryStats = plugin.headRegistry().stats(); + RefreshState refresh = plugin.runtime().refreshState(); + Server server = plugin.getServer(); + PlatformRequirements.Compatibility compatibility = PlatformRequirements.inspect(plugin); + Path dataDirectory = plugin.getDataFolder().toPath().toAbsolutePath().normalize(); + + section(report, "Plugin"); + line(report, "Version", buildInfo.version()); + line(report, "Base version", buildInfo.baseVersion()); + line(report, "Build", value(buildInfo.buildNumber())); + line(report, "Attempt", value(buildInfo.buildAttempt())); + line(report, "Commit", value(buildInfo.commit())); + line(report, "Full commit", value(buildInfo.fullCommit())); + line(report, "Branch", value(buildInfo.branch())); + line(report, "Build time", value(buildInfo.buildTime())); + + section(report, "Server"); + line(report, "Software", server.getName()); + line(report, "Server version", server.getVersion()); + line(report, "Bukkit version", server.getBukkitVersion()); + line(report, "Minecraft version", server.getMinecraftVersion()); + line(report, "Folia", yesNo(isFolia())); + line(report, "Java", System.getProperty("java.version") + " (feature " + compatibility.javaFeature() + ")"); + line(report, "OS", System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch")); + line(report, "Max memory", bytes(Runtime.getRuntime().maxMemory())); + line(report, "Required Paper", PlatformRequirements.REQUIRED_PAPER_VERSION); + line(report, "Required Java", PlatformRequirements.REQUIRED_JAVA_FEATURE + "+"); + line(report, "Java supported", yesNo(compatibility.javaSupported())); + line(report, "Paper supported", yesNo(compatibility.paperSupported())); + line(report, "Runtime supported", yesNo(compatibility.supported())); + + section(report, "Database"); + line(report, "State", status.state()); + line(report, "Source", status.source()); + line(report, "Manifest ID", value(status.manifestId())); + line(report, "Catalog ID", value(status.artifactId())); + line(report, "Loaded at", formatInstant(status.loadedAt())); + line(report, "Last database error", value(status.lastError())); + + section(report, "Remote stats"); + line(report, "Heads", databaseStats.heads()); + line(report, "Categories", databaseStats.categories()); + line(report, "Tags", databaseStats.tags()); + line(report, "Collections", databaseStats.collections()); + line(report, "Revocations", databaseStats.revocations()); + + section(report, "Registry stats"); + line(report, "Effective heads", registryStats.heads()); + line(report, "Effective categories", registryStats.categories()); + line(report, "Effective tags", registryStats.tags()); + line(report, "Effective collections", registryStats.collections()); + line(report, "Hidden remote heads", plugin.headRegistry().hiddenHeads().size()); + line(report, "Remote overrides", plugin.headRegistry().overrides().list().size()); + line(report, "More Heads", plugin.headRegistry().customHeads().list().size()); + line(report, "Known player heads", plugin.headRegistry().playerHeads().knownPlayers().size()); + line(report, "More Categories", plugin.customCategories().list().size()); + + section(report, "Refresh"); + line(report, "Running", yesNo(refresh.running())); + line(report, "Current operation", refresh.running() ? refresh.currentOperation() : "none"); + line(report, "Started at", formatInstant(refresh.startedAt())); + line(report, "Last outcome", refresh.lastOutcome()); + line(report, "Last operation", refresh.lastOutcome() == RefreshState.RefreshOutcome.NONE ? "none" : refresh.lastOperation()); + line(report, "Last successful operation", formatInstant(refresh.lastSuccessfulRefresh())); + line(report, "Last failed operation", formatInstant(refresh.lastFailedRefresh())); + line(report, "Last failure", value(refresh.lastFailureMessage())); + + section(report, "Remote config"); + line(report, "Manifest URL", config.remoteManifestUri()); + line(report, "Preferred mirror", value(config.preferredMirrorId())); + line(report, "Connect timeout", config.connectTimeout()); + line(report, "Read timeout", config.readTimeout()); + + section(report, "Cache and storage"); + line(report, "Data directory", dataDirectory); + line(report, "Cache directory", config.cacheDirectory(dataDirectory)); + line(report, "Local store database", config.localStoreDatabase(dataDirectory)); + line(report, "Load cache on startup", yesNo(config.loadCacheOnStartup())); + line(report, "Refresh on startup", yesNo(config.refreshOnStartup())); + line(report, "Item cache enabled", yesNo(config.cacheItemEnabled())); + line(report, "Item cache size", plugin.itemCacheSize()); + + section(report, "Local features"); + line(report, "Remote overrides enabled", yesNo(config.remoteOverridesEnabled())); + line(report, "More Heads enabled", yesNo(config.customHeadsEnabled())); + line(report, "Player heads enabled", yesNo(config.playerHeadsEnabled())); + line(report, "External player lookup", yesNo(config.playerHeadsAllowExternalLookup())); + + section(report, "Updater"); + appendUpdater(report, plugin); + + section(report, "Invocation"); + line(report, "Sender type", sender instanceof Player ? "player" : "console"); + + if (sender instanceof Player player) { + line(report, "Admin mode", yesNo(plugin.adminModes().enabled(player))); + } + + return report.toString(); + } + + private static void appendUpdater(@NotNull StringBuilder report, @NotNull HeadDBPlugin plugin) { + PluginConfig config = plugin.config(); + UpdateService updater = plugin.updater(); + UpdateCheckResult result = updater.lastResult(); + + line(report, "Update checker enabled", yesNo(config.updateCheckerEnabled())); + line(report, "Check on startup", yesNo(config.updateCheckerCheckOnStartup())); + line(report, "Include prereleases", yesNo(config.updateCheckerIncludePrereleases())); + line(report, "Include builds", yesNo(config.updateCheckerIncludeBuilds())); + line(report, "Auto install", yesNo(config.autoUpdaterInstallUpdates())); + line(report, "Running", yesNo(updater.running())); + + if (result == null) { + line(report, "Last check", "never"); + return; + } + + line(report, "Last check", formatInstant(result.checkedAt())); + line(report, "Current version", result.currentVersion()); + line(report, "Update kind", result.kind()); + line(report, "Update available", yesNo(result.updateAvailable())); + line(report, "Failed", yesNo(result.failed())); + line(report, "Failure", value(result.failureMessage())); + line(report, "Installed path", value(result.installedPath())); + + GitHubRelease release = result.release(); + if (release == null) { + return; + } + + line(report, "Latest version", release.version().raw()); + line(report, "Latest tag", release.tagName()); + line(report, "Latest URL", release.htmlUrl()); + line(report, "Latest prerelease", yesNo(release.prerelease())); + line(report, "Latest asset", value(release.assetName())); + } + + private static void section(@NotNull StringBuilder report, @NotNull String title) { + if (!report.isEmpty()) { + report.append(System.lineSeparator()); + } + + report.append("== ").append(title).append(" ==").append(System.lineSeparator()); + } + + private static void line(@NotNull StringBuilder report, @NotNull String key, @Nullable Object value) { + report.append(key).append(": ").append(value(value)).append(System.lineSeparator()); + } + + private static @NotNull String yesNo(boolean value) { + return value ? "yes" : "no"; + } + + private static @NotNull String formatInstant(@Nullable Instant instant) { + if (instant == null) { + return "never"; + } + + return TIME_FORMAT.format(instant); + } + + private static @NotNull String value(@Nullable Object value) { + if (value == null) { + return "none"; + } + + String string = String.valueOf(value); + if (string.isBlank()) { + return "none"; + } + + return string; + } + + private static @NotNull String bytes(long bytes) { + long mib = bytes / 1024L / 1024L; + return bytes + " bytes (" + mib + " MiB)"; + } + + private static boolean isFolia() { + try { + Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + return true; + } catch (ClassNotFoundException exception) { + return false; + } + } + +} diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/CategoriesCommand.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/CategoriesCommand.java index ebdaaaa..9433e99 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/CategoriesCommand.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/CategoriesCommand.java @@ -38,7 +38,7 @@ protected void handle(@NotNull PaperCommandContext context) { .map(category -> new ListFormatter.Entry(category.id(), category.name())) .toList(); - for (var line : ListFormatter.format("HeadDB Categories", entries, page, PAGE_SIZE)) { + for (var line : ListFormatter.format("Head Categories", entries, page, PAGE_SIZE)) { context.reply(line); } } @@ -47,7 +47,7 @@ protected void handle(@NotNull PaperCommandContext context) { protected @NotNull Command buildCommand() { return PaperCommands.literal("categories") .alias("cat") - .description("Lists HeadDB categories.") + .description("Lists head categories.") .requirement(CommandRequirements.permission(Permissions.SEARCH)) .signature(PAGE) .suggest(PAGE, Suggestions.pages()) diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/CollectionsCommand.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/CollectionsCommand.java index cfdbc29..a83be16 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/CollectionsCommand.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/CollectionsCommand.java @@ -40,7 +40,7 @@ protected void handle(@NotNull PaperCommandContext context) { .map(collection -> new ListFormatter.Entry(collection.id(), collection.name())) .toList(); - for (var line : ListFormatter.format("HeadDB Collections", entries, request.page(), PAGE_SIZE)) { + for (var line : ListFormatter.format("Head Collections", entries, request.page(), PAGE_SIZE)) { context.reply(line); } } @@ -49,7 +49,7 @@ protected void handle(@NotNull PaperCommandContext context) { protected @NotNull Command buildCommand() { return PaperCommands.literal("collections") .alias("col") - .description("Lists HeadDB collections.") + .description("Lists head collections.") .requirement(CommandRequirements.permission(Permissions.SEARCH)) .signature(QUERY, PAGE) .suggest(QUERY, Suggestions.collections(plugin)) diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/CustomCommand.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/CustomCommand.java index bc00b05..9f32bc6 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/CustomCommand.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/CustomCommand.java @@ -75,7 +75,7 @@ protected void handle(@NotNull PaperCommandContext context) { @Override protected @NotNull Command buildCommand() { return PaperCommands.literal("custom") - .description("Manages local custom HeadDB heads.") + .description("Manages local custom heads.") .requirement(CommandRequirements.permission(Permissions.CUSTOM_LIST)) .signature(ACTION, FIRST, SECOND, THIRD, FOURTH) .suggest(ACTION, context -> List.of("list", "info", "create", "createheld", "delete", "rename", "give")) diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/DebugCommand.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/DebugCommand.java index 56f1f66..2c7fa76 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/DebugCommand.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/DebugCommand.java @@ -6,7 +6,9 @@ import io.github.silentdevelopment.headdb.paper.command.CommandRequirements; import io.github.silentdevelopment.headdb.paper.permission.Permissions; import io.github.silentdevelopment.headdb.paper.runtime.BuildInfo; +import io.github.silentdevelopment.headdb.paper.runtime.PlatformRequirements; import io.github.silentdevelopment.headdb.paper.runtime.RefreshState; +import io.github.silentdevelopment.headdb.paper.updater.UpdateCheckResult; import io.github.silentdevelopment.relay.command.Command; import io.github.silentdevelopment.relay.paper.command.AbstractPaperCommand; import io.github.silentdevelopment.relay.paper.command.PaperCommands; @@ -33,75 +35,116 @@ public DebugCommand(@NotNull HeadDBPlugin plugin) { @Override protected void handle(@NotNull PaperCommandContext context) { + BuildInfo buildInfo = BuildInfo.read(plugin); DatabaseStatus status = plugin.runtime().database().status(); - DatabaseStats stats = plugin.runtime().database().stats(); + DatabaseStats remoteStats = plugin.runtime().database().stats(); RefreshState refresh = plugin.runtime().refreshState(); - BuildInfo buildInfo = BuildInfo.read(plugin); + PlatformRequirements.Compatibility compatibility = PlatformRequirements.inspect(plugin); + UpdateCheckResult updateResult = plugin.updater().lastResult(); context.reply(Component.empty()); context.reply(Component.text("> ", NamedTextColor.DARK_GRAY).append(Component.text("Debug", NamedTextColor.RED))); - - context.reply(line("Version", buildInfo.version())); - context.reply(line("Base version", buildInfo.baseVersion())); - context.reply(line("Build", value(buildInfo.buildNumber()))); - context.reply(line("Attempt", value(buildInfo.buildAttempt()))); - context.reply(line("Commit", value(buildInfo.commit()))); - context.reply(line("Full commit", value(buildInfo.fullCommit()))); - context.reply(line("Branch", value(buildInfo.branch()))); - context.reply(line("Timestamp", value(buildInfo.buildTime()))); + context.reply(line("Version", buildInfo.version() + " (" + value(buildInfo.commit()) + ")")); + context.reply(line("Runtime", plugin.getServer().getName() + " " + plugin.getServer().getMinecraftVersion() + " | Java " + compatibility.javaFeature())); + context.reply(line("Supported", yesNo(compatibility.supported()))); + context.reply(databaseLine(status)); + context.reply(line("Heads", remoteStats.heads())); + context.reply(line("Categories", remoteStats.categories())); + context.reply(line("Tags", remoteStats.tags())); + context.reply(line("Collections", remoteStats.collections())); + context.reply(line("Revocations", remoteStats.revocations())); + context.reply(line("Hidden Heads", plugin.headRegistry().hiddenHeads().size())); + context.reply(line("Overrides", plugin.headRegistry().overrides().list().size())); + context.reply(line("More Heads", plugin.headRegistry().customHeads().list().size())); + context.reply(line("Player Heads", plugin.headRegistry().playerHeads().knownPlayers().size())); + context.reply(line("More Categories", plugin.customCategories().list().size())); + context.reply(line("Refresh", refreshText(refresh))); + context.reply(line("Last Refresh", lastRefreshText(refresh))); + context.reply(line("Updater", updateText(updateResult))); + + if (Permissions.has(context.sender(), Permissions.REPORT)) { + context.reply(Component.text("Report: ", NamedTextColor.GRAY).append(Component.text("/hdb report", NamedTextColor.GOLD))); + } context.reply(Component.empty()); - context.reply(line("State", status.state())); - context.reply(line("Source", status.source())); - context.reply(line("Manifest ID", value(status.manifestId()))); - context.reply(line("Catalog ID", value(status.artifactId()))); - context.reply(line("Loaded at", formatInstant(status.loadedAt()))); - context.reply(line("Last database error", value(status.lastError()))); + } - context.reply(Component.empty()); - context.reply(line("Heads", stats.heads())); - context.reply(line("Categories", stats.categories())); - context.reply(line("Tags", stats.tags())); - context.reply(line("Collections", stats.collections())); - context.reply(line("Revocations", stats.revocations())); + @Override + protected @NotNull Command buildCommand() { + return PaperCommands.literal("debug").alias("d").description("Shows concise runtime diagnostics.").requirement(CommandRequirements.permission(Permissions.DEBUG)).noArgs().build(); + } - context.reply(Component.empty()); - context.reply(line("Manifest URL", plugin.config().remoteManifestUri())); - context.reply(line("Preferred mirror", plugin.config().preferredMirrorId())); - context.reply(line("Load cache on startup", yesNo(plugin.config().loadCacheOnStartup()))); - context.reply(line("Refresh on startup", yesNo(plugin.config().refreshOnStartup()))); - context.reply(line("Cache directory", plugin.config().cacheDirectory(plugin.getDataFolder().toPath()).toAbsolutePath().normalize())); - context.reply(line("Item cache enabled", yesNo(plugin.config().cacheItemEnabled()))); - context.reply(line("Item cache size", plugin.itemCacheSize())); + private static @NotNull Component databaseLine(@NotNull DatabaseStatus status) { + Component line = Component.text("Database: ", NamedTextColor.GRAY).append(Component.text(String.valueOf(status.state()), statusColor(status))); + String source = value(status.source()); - context.reply(Component.empty()); - context.reply(line("Refresh running", yesNo(refresh.running()))); - context.reply(line("Last successful refresh", formatInstant(refresh.lastSuccessfulRefresh()))); - context.reply(line("Last failed refresh", formatInstant(refresh.lastFailedRefresh()))); - context.reply(line("Last refresh failure", value(refresh.lastFailureMessage()))); - context.reply(Component.empty()); + if (!source.equals("none")) { + line = line.append(Component.text(" from ", NamedTextColor.GRAY)).append(Component.text(source, NamedTextColor.GOLD)); + } + + return line; } - @Override - protected @NotNull Command buildCommand() { - return PaperCommands.literal("debug") - .alias("d") - .description("Shows detailed runtime diagnostics.") - .requirement(CommandRequirements.permission(Permissions.DEBUG)) - .noArgs() - .build(); + private static @NotNull String refreshText(@NotNull RefreshState refresh) { + if (refresh.running()) { + return "running " + refresh.currentOperation(); + } + + return "idle"; + } + + private static @NotNull String lastRefreshText(@NotNull RefreshState refresh) { + if (refresh.lastOutcome() == RefreshState.RefreshOutcome.SUCCESS) { + return refresh.lastOperation() + " completed at " + formatInstant(refresh.lastSuccessfulRefresh()); + } + + if (refresh.lastOutcome() == RefreshState.RefreshOutcome.FAILURE) { + return refresh.lastOperation() + " failed at " + formatInstant(refresh.lastFailedRefresh()); + } + + return "never"; + } + + private static @NotNull String updateText(@Nullable UpdateCheckResult result) { + if (result == null) { + return "not checked"; + } + + if (result.failed()) { + return "failed: " + value(result.failureMessage()); + } + + if (result.updateAvailable() && result.release() != null) { + return "available " + result.release().version().raw(); + } + + if (result.updateAvailable()) { + return "available"; + } + + return "current"; } private static @NotNull Component line(@NotNull String key, @Nullable Object value) { return Component.text(key + ": ", NamedTextColor.GRAY).append(Component.text(String.valueOf(value), NamedTextColor.GOLD)); } - private static @NotNull String yesNo(boolean value) { - if (value) { - return "yes"; + private static @NotNull NamedTextColor statusColor(@NotNull DatabaseStatus status) { + String state = String.valueOf(status.state()); + + if ("LOADED".equalsIgnoreCase(state)) { + return NamedTextColor.GOLD; + } + + if ("LOADING".equalsIgnoreCase(state)) { + return NamedTextColor.YELLOW; } - return "no"; + return NamedTextColor.RED; + } + + private static @NotNull String yesNo(boolean value) { + return value ? "yes" : "no"; } private static @NotNull String formatInstant(@Nullable Instant instant) { @@ -124,4 +167,5 @@ protected void handle(@NotNull PaperCommandContext context) { return string; } -} \ No newline at end of file + +} diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/GiveCommand.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/GiveCommand.java index 449cb34..399bf8d 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/GiveCommand.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/GiveCommand.java @@ -65,7 +65,7 @@ protected void handle(@NotNull PaperCommandContext context) { protected @NotNull Command buildCommand() { return PaperCommands.literal("give") .alias("g") - .description("Gives a HeadDB head item.") + .description("Gives a head item.") .requirement(CommandRequirements.permission(Permissions.GIVE)) .signature(FIRST, SECOND, AMOUNT) .suggest(FIRST, Suggestions.playerOrHeadIds(plugin)) diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/HelpCommand.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/HelpCommand.java index f95df96..40bc468 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/HelpCommand.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/HelpCommand.java @@ -38,7 +38,7 @@ protected void handle(@NotNull PaperCommandContext context) { protected @NotNull Command buildCommand() { return PaperCommands.literal("help") .alias("h") - .description("Shows HeadDB command help.") + .description("Shows command help.") .requirement(CommandRequirements.permission(Permissions.HELP)) .noArgs() .build(); diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/InfoCommand.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/InfoCommand.java index c29a67a..c43f44a 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/InfoCommand.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/InfoCommand.java @@ -69,7 +69,7 @@ protected void handle(@NotNull PaperCommandContext context) { protected @NotNull Command buildCommand() { return PaperCommands.literal("info") .alias("i") - .description("Shows information about a HeadDB head or the held HeadDB head.") + .description("Shows information about a head or the held head.") .requirement(CommandRequirements.permission(Permissions.INFO)) .signature(ID) .suggest(ID, Suggestions.headIds(plugin)) diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/ItemCacheCommand.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/ItemCacheCommand.java index 92c6b73..ab52a1d 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/ItemCacheCommand.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/ItemCacheCommand.java @@ -46,7 +46,7 @@ protected void handle(@NotNull PaperCommandContext context) { protected @NotNull Command buildCommand() { return PaperCommands.literal("itemcache") .alias("ic") - .description("Manages the generated HeadDB item cache.") + .description("Manages the generated item cache.") .requirement(CommandRequirements.permission(Permissions.ITEM_CACHE)) .signature(ACTION) .noArgs() diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/ReportCommand.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/ReportCommand.java new file mode 100644 index 0000000..4872f58 --- /dev/null +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/ReportCommand.java @@ -0,0 +1,71 @@ +package io.github.silentdevelopment.headdb.paper.command.subcommand; + +import io.github.silentdevelopment.headdb.paper.HeadDBPlugin; +import io.github.silentdevelopment.headdb.paper.command.CommandRequirements; +import io.github.silentdevelopment.headdb.paper.command.format.SupportReport; +import io.github.silentdevelopment.headdb.paper.permission.Permissions; +import io.github.silentdevelopment.relay.command.Command; +import io.github.silentdevelopment.relay.paper.command.AbstractPaperCommand; +import io.github.silentdevelopment.relay.paper.command.PaperCommands; +import io.github.silentdevelopment.relay.paper.command.context.PaperCommandContext; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public final class ReportCommand extends AbstractPaperCommand { + + private final HeadDBPlugin plugin; + + public ReportCommand(@NotNull HeadDBPlugin plugin) { + this.plugin = Objects.requireNonNull(plugin, "plugin"); + } + + @Override + protected void handle(@NotNull PaperCommandContext context) { + String report = SupportReport.create(plugin, context.sender()); + + if (context.sender() instanceof Player) { + sendPlayerReport(context, report); + return; + } + + sendConsoleReport(context, report); + } + + @Override + protected @NotNull Command buildCommand() { + return PaperCommands.literal("report").alias("rpt").description("Creates a full support report.").requirement(CommandRequirements.permission(Permissions.REPORT)).noArgs().build(); + } + + private static void sendPlayerReport(@NotNull PaperCommandContext context, @NotNull String report) { + Component copy = Component.text("HERE", NamedTextColor.GOLD).decorate(TextDecoration.BOLD).clickEvent(ClickEvent.copyToClipboard(report)).hoverEvent(HoverEvent.showText(Component.text("Copy the full support report.", NamedTextColor.GRAY))); + + context.reply(Component.empty()); + context.reply(Component.text("> ", NamedTextColor.DARK_GRAY).append(Component.text("Report", NamedTextColor.RED))); + context.reply(Component.text("Click ", NamedTextColor.GRAY).append(copy).append(Component.text(" to copy the full support report.", NamedTextColor.GRAY))); + context.reply(Component.text("Paste this report when asking for support.", NamedTextColor.GRAY)); + context.reply(Component.empty()); + } + + private static void sendConsoleReport(@NotNull PaperCommandContext context, @NotNull String report) { + context.reply(Component.empty()); + + for (String line : report.split("\\R", -1)) { + if (line.isBlank()) { + context.reply(Component.empty()); + continue; + } + + context.reply(Component.text(line, NamedTextColor.GRAY)); + } + + context.reply(Component.empty()); + } + +} diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/StatusCommand.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/StatusCommand.java index 75b3085..33ec197 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/StatusCommand.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/StatusCommand.java @@ -23,7 +23,7 @@ public StatusCommand(@NotNull HeadDBPlugin plugin) { @Override protected void handle(@NotNull PaperCommandContext context) { - for (Component line : StatusFormatter.format(plugin.runtime(), context.sender())) { + for (Component line : StatusFormatter.format(plugin, context.sender())) { context.reply(line); } } diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/TagsCommand.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/TagsCommand.java index f944523..7f15a65 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/TagsCommand.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/command/subcommand/TagsCommand.java @@ -40,7 +40,7 @@ protected void handle(@NotNull PaperCommandContext context) { .map(tag -> new ListFormatter.Entry(tag.id(), tag.name())) .toList(); - for (var line : ListFormatter.format("HeadDB Tags", entries, request.page(), PAGE_SIZE)) { + for (var line : ListFormatter.format("Head Tags", entries, request.page(), PAGE_SIZE)) { context.reply(line); } } diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/economy/EconomyConfig.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/economy/EconomyConfig.java index 8fa0d76..512eff3 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/economy/EconomyConfig.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/economy/EconomyConfig.java @@ -138,7 +138,7 @@ private static void copyDefault(@NotNull Path file) { Files.writeString(file, defaultConfig()); } catch (IOException exception) { - throw new IllegalStateException("Failed to create HeadDB economy.yml.", exception); + throw new IllegalStateException("Failed to create economy.yml.", exception); } } diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/permission/Permissions.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/permission/Permissions.java index 65f9a85..b029972 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/permission/Permissions.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/permission/Permissions.java @@ -15,6 +15,7 @@ public final class Permissions { public static final String VERSION = "headdb.command.version"; public static final String STATUS = "headdb.command.status"; public static final String DEBUG = "headdb.command.debug"; + public static final String REPORT = "headdb.command.report"; public static final String VERIFY = "headdb.command.verify"; public static final String REFRESH = "headdb.command.refresh"; public static final String RELOAD = "headdb.command.reload"; diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/runtime/PlatformRequirements.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/runtime/PlatformRequirements.java new file mode 100644 index 0000000..7b7f586 --- /dev/null +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/runtime/PlatformRequirements.java @@ -0,0 +1,131 @@ +package io.github.silentdevelopment.headdb.paper.runtime; + +import io.github.silentdevelopment.headdb.paper.HeadDBPlugin; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class PlatformRequirements { + + public static final int REQUIRED_JAVA_FEATURE = 25; + public static final int REQUIRED_PAPER_MAJOR = 26; + public static final String REQUIRED_PAPER_VERSION = "26.1.2+"; + + private static final Pattern VERSION_TOKEN = Pattern.compile("(?= REQUIRED_JAVA_FEATURE; + boolean paperSupported = paper26OrNewer(serverVersion + " " + bukkitVersion); + + return new Compatibility( + javaFeature, + javaVersion, + serverVersion, + bukkitVersion, + javaSupported, + paperSupported + ); + } + + private static boolean paper26OrNewer(@NotNull String version) { + Matcher matcher = VERSION_TOKEN.matcher(version); + + while (matcher.find()) { + int major = parseInt(matcher.group(1)); + + if (major >= REQUIRED_PAPER_MAJOR) { + return true; + } + } + + return false; + } + + private static int parseInt(@NotNull String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException exception) { + return -1; + } + } + + private static @NotNull String value(String value) { + if (value == null || value.isBlank()) { + return "unknown"; + } + + return value; + } + + public record Compatibility( + int javaFeature, + @NotNull String javaVersion, + @NotNull String serverVersion, + @NotNull String bukkitVersion, + boolean javaSupported, + boolean paperSupported + ) { + + public boolean supported() { + return javaSupported && paperSupported; + } + + } + +} \ No newline at end of file diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/runtime/PluginRuntime.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/runtime/PluginRuntime.java index 6bf3903..bef8d87 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/runtime/PluginRuntime.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/runtime/PluginRuntime.java @@ -110,11 +110,15 @@ public boolean refreshAsync() { return false; } - if (refreshState.running()) { + if (!refreshState.begin("remote refresh")) { + return false; + } + + if (!runAsync("refresh remote database", this::refreshRemoteStarted)) { + refreshState.finish(); return false; } - runAsync("refresh remote database", this::refreshRemote); return true; } @@ -126,11 +130,15 @@ public boolean verifyRemoteAsync(@NotNull Consumer success, @N return false; } - if (refreshState.running()) { + if (!refreshState.begin("remote verification")) { + return false; + } + + if (!runAsync("verify remote database", () -> verifyRemoteStarted(success, failure))) { + refreshState.finish(); return false; } - runAsync("verify remote database", () -> verifyRemote(success, failure)); return true; } @@ -161,11 +169,15 @@ private void runStartupRefreshSequence() { } private void loadCached() { - if (!refreshState.begin()) { + if (!refreshState.begin("cache load")) { plugin.getSLF4JLogger().warn("Skipped cache load because another refresh task is already running."); return; } + loadCachedStarted(); + } + + private void loadCachedStarted() { try { boolean loaded = refreshService.loadCached(); @@ -186,11 +198,15 @@ private void loadCached() { } private void refreshRemote() { - if (!refreshState.begin()) { + if (!refreshState.begin("remote refresh")) { plugin.getSLF4JLogger().warn("Skipped remote refresh because another refresh task is already running."); return; } + refreshRemoteStarted(); + } + + private void refreshRemoteStarted() { try { refreshService.refresh(); plugin.clearItemCache(); @@ -203,12 +219,7 @@ private void refreshRemote() { } } - private void verifyRemote(@NotNull Consumer success, @NotNull Consumer failure) { - if (!refreshState.begin()) { - failure.accept(new IllegalStateException("Database refresh or verification is already running.")); - return; - } - + private void verifyRemoteStarted(@NotNull Consumer success, @NotNull Consumer failure) { try { DatabaseSnapshot snapshot = refreshService.verifyRemote(); refreshState.finish(); @@ -240,13 +251,14 @@ private void logFailure(@NotNull String message, @NotNull Throwable throwable) { plugin.getSLF4JLogger().warn("{} {}", message, detail); } - private void runAsync(@NotNull String operation, @NotNull Runnable runnable) { + private boolean runAsync(@NotNull String operation, @NotNull Runnable runnable) { if (closed.get()) { - return; + return false; } ScheduledTask task = plugin.getServer().getAsyncScheduler().runNow(plugin, scheduledTask -> { if (closed.get()) { + refreshState.finish(); return; } @@ -261,5 +273,7 @@ private void runAsync(@NotNull String operation, @NotNull Runnable runnable) { synchronized (scheduledTasks) { scheduledTasks.add(task); } + + return true; } } \ No newline at end of file diff --git a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/runtime/RefreshState.java b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/runtime/RefreshState.java index 18d3895..ec6f77f 100644 --- a/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/runtime/RefreshState.java +++ b/headdb-platforms/headdb-paper/src/main/java/io/github/silentdevelopment/headdb/paper/runtime/RefreshState.java @@ -4,36 +4,68 @@ import org.jetbrains.annotations.Nullable; import java.time.Instant; +import java.util.Locale; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; public final class RefreshState { private final AtomicBoolean running; + private final AtomicReference currentOperation; + private final AtomicReference startedAt; + private final AtomicReference lastOutcome; + private final AtomicReference lastOperation; private final AtomicReference lastSuccessfulRefresh; private final AtomicReference lastFailedRefresh; private final AtomicReference lastFailureMessage; public RefreshState() { this.running = new AtomicBoolean(false); + this.currentOperation = new AtomicReference<>(); + this.startedAt = new AtomicReference<>(); + this.lastOutcome = new AtomicReference<>(RefreshOutcome.NONE); + this.lastOperation = new AtomicReference<>(); this.lastSuccessfulRefresh = new AtomicReference<>(); this.lastFailedRefresh = new AtomicReference<>(); this.lastFailureMessage = new AtomicReference<>(); } public boolean begin() { - return running.compareAndSet(false, true); + return begin("remote refresh"); + } + + public boolean begin(@NotNull String operation) { + Objects.requireNonNull(operation, "operation"); + + if (!running.compareAndSet(false, true)) { + return false; + } + + currentOperation.set(normalizeOperation(operation)); + startedAt.set(Instant.now()); + return true; } public void markSuccess() { + String operation = currentOperation(); lastSuccessfulRefresh.set(Instant.now()); lastFailureMessage.set(null); + lastOperation.set(operation); + lastOutcome.set(RefreshOutcome.SUCCESS); + currentOperation.set(null); + startedAt.set(null); running.set(false); } public void markFailure(@NotNull Throwable throwable) { + String operation = currentOperation(); lastFailedRefresh.set(Instant.now()); lastFailureMessage.set(failureMessage(throwable)); + lastOperation.set(operation); + lastOutcome.set(RefreshOutcome.FAILURE); + currentOperation.set(null); + startedAt.set(null); running.set(false); } @@ -41,6 +73,22 @@ public boolean running() { return running.get(); } + public @NotNull String currentOperation() { + return value(currentOperation.get(), "database operation"); + } + + public @Nullable Instant startedAt() { + return startedAt.get(); + } + + public @NotNull RefreshOutcome lastOutcome() { + return lastOutcome.get(); + } + + public @NotNull String lastOperation() { + return value(lastOperation.get(), "database operation"); + } + public @Nullable Instant lastSuccessfulRefresh() { return lastSuccessfulRefresh.get(); } @@ -54,6 +102,8 @@ public boolean running() { } public void finish() { + currentOperation.set(null); + startedAt.set(null); running.set(false); } @@ -64,4 +114,29 @@ public void finish() { return throwable.getMessage(); } -} \ No newline at end of file + + private static @NotNull String normalizeOperation(@NotNull String operation) { + String normalized = operation.trim().toLowerCase(Locale.ROOT); + + if (normalized.isBlank()) { + return "database operation"; + } + + return normalized; + } + + private static @NotNull String value(@Nullable String value, @NotNull String fallback) { + if (value == null || value.isBlank()) { + return fallback; + } + + return value; + } + + public enum RefreshOutcome { + NONE, + SUCCESS, + FAILURE + } + +} diff --git a/headdb-platforms/headdb-paper/src/main/resources/paper-plugin.yml b/headdb-platforms/headdb-paper/src/main/resources/paper-plugin.yml index 3230e8c..9d36c81 100644 --- a/headdb-platforms/headdb-paper/src/main/resources/paper-plugin.yml +++ b/headdb-platforms/headdb-paper/src/main/resources/paper-plugin.yml @@ -198,6 +198,7 @@ permissions: children: headdb.command.status: true headdb.command.debug: true + headdb.command.report: true headdb.command.verify: true headdb.command.refresh: true headdb.command.reload: true @@ -218,6 +219,9 @@ permissions: headdb.command.debug: description: Allows using /hdb debug. + headdb.command.report: + description: Allows using /hdb report. + headdb.command.verify: description: Allows using /hdb verify.