From f9fed264a2254502156cd78332958a04a229a777 Mon Sep 17 00:00:00 2001 From: derrickmehaffy Date: Sat, 28 Feb 2026 18:04:03 -0800 Subject: [PATCH 01/10] feat: add Sentry error tracking integration Non-blocking async error reporting to Sentry with fail-safe wrapping. Includes configurable DSN, source context upload, and /f admin sentrytest command. --- .gitignore | 3 + CHANGELOG.md | 14 +- build.gradle | 22 ++ .../command/admin/AdminSubCommand.java | 18 ++ .../hyperfactions/config/ConfigManager.java | 22 ++ .../config/modules/SentryConfig.java | 108 ++++++++++ .../integration/SentryIntegration.java | 190 ++++++++++++++++++ .../platform/HyperFactionsPlugin.java | 8 + 8 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/hyperfactions/config/modules/SentryConfig.java create mode 100644 src/main/java/com/hyperfactions/integration/SentryIntegration.java diff --git a/.gitignore b/.gitignore index f1660e2..78d9f40 100644 --- a/.gitignore +++ b/.gitignore @@ -43,5 +43,8 @@ libs/ # Local config overrides config.local.json +# Sentry auth token (secret — never commit) +.sentry-auth-token + # Serena .serena/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 55d8ef2..01ac94d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -*No changes yet* +### Added + +**Sentry Error Tracking Integration** +- Sentry SDK (v8.33.0) bundled for automatic error reporting to Sentry dashboard +- Non-blocking async event delivery — Sentry never impacts server performance +- All Sentry operations wrapped in try/catch — failures never crash the server +- New config module `config/sentry.json` with settings: enabled, dsn, environment, debug, tracesSampleRate +- DSN pre-configured with default — works out of the box +- Source context upload via Sentry Gradle plugin (stack traces show source code in Sentry) +- HyperFactions frames highlighted in stack traces via `addInAppInclude` +- New admin command: `/f admin sentrytest` — sends a test error with stack trace to verify integration +- Sentry cleanly flushes pending events on server shutdown (2s timeout) +- Auth token stored in `.sentry-auth-token` file (gitignored) with env var fallback ## [0.10.2] - 2026-02-28 diff --git a/build.gradle b/build.gradle index d3c8daf..9b392f5 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { id 'checkstyle' id 'maven-publish' id 'com.gradleup.shadow' version '9.3.1' + id 'io.sentry.jvm.gradle' version '6.1.0' } group = 'com.hyperfactions' @@ -71,6 +72,9 @@ dependencies { // JSON handling implementation 'com.google.code.gson:gson:2.11.0' + // Sentry error tracking (bundled in shadow JAR) + implementation 'io.sentry:sentry:8.33.0' + // PlaceholderAPI Hytale (soft dependency - compileOnly) compileOnly 'at.helpch:placeholderapi-hytale:1.0.4' @@ -144,6 +148,7 @@ shadowJar { // Relocate dependencies to avoid conflicts relocate 'com.google.gson', 'com.hyperfactions.lib.gson' + relocate 'io.sentry', 'com.hyperfactions.lib.sentry' // Don't minimize - it removes Gson's inner classes needed at runtime } @@ -194,6 +199,11 @@ tasks.named('compileJava') { } } +// Sentry's bundle ID generator uses the same generated sources dir — declare dependency +tasks.matching { it.name == 'generateSentryBundleIdJava' }.configureEach { + dependsOn 'generateBuildInfo' +} + // Dev build task - clean build with version set to 'dev' tasks.register('buildDev') { group = 'build' @@ -205,6 +215,18 @@ tasks.named('build') { mustRunAfter 'clean' } +// Sentry source context upload (reads token from .sentry-auth-token file or SENTRY_AUTH_TOKEN env var) +sentry { + includeSourceContext = true + org = "hypersystems" + projectName = "hyperfactions" + authToken = { + def tokenFile = file('.sentry-auth-token') + if (tokenFile.exists()) return tokenFile.text.trim() + return System.getenv("SENTRY_AUTH_TOKEN") + }() +} + // Maven publication for JitPack (publishes API classes, not the shadow JAR) publishing { publications { diff --git a/src/main/java/com/hyperfactions/command/admin/AdminSubCommand.java b/src/main/java/com/hyperfactions/command/admin/AdminSubCommand.java index 432b382..2de569d 100644 --- a/src/main/java/com/hyperfactions/command/admin/AdminSubCommand.java +++ b/src/main/java/com/hyperfactions/command/admin/AdminSubCommand.java @@ -4,6 +4,7 @@ import com.hyperfactions.Permissions; import com.hyperfactions.command.admin.handler.AdminBackupHandler; import com.hyperfactions.command.admin.handler.AdminDebugHandler; +import com.hyperfactions.integration.SentryIntegration; import com.hyperfactions.command.admin.handler.AdminEconomyHandler; import com.hyperfactions.command.admin.handler.AdminImportHandler; import com.hyperfactions.command.admin.handler.AdminIntegrationHandler; @@ -284,6 +285,7 @@ private void dispatchCommand(@NotNull CommandContext ctx, @Nullable Store economyHandler.handleAdminEconomy(ctx, player, senderUuid, subArgs); case "world", "worlds" -> worldHandler.handleAdminWorld(ctx, player, subArgs); case "version" -> handleVersion(ctx, store, ref, player, isPlayer); + case "sentrytest" -> handleSentryTest(ctx); case "log", "logs", "activitylog" -> { if (!requirePlayer(ctx, isPlayer)) { break; @@ -375,6 +377,7 @@ private void showAdminHelp(CommandContext ctx) { commands.add(new CommandHelp("/f admin log", "View global activity log")); commands.add(new CommandHelp("/f admin world", "Per-world settings management")); commands.add(new CommandHelp("/f admin version", "View mod version and integration status")); + commands.add(new CommandHelp("/f admin sentrytest", "Send a test error to Sentry")); ctx.sendMessage(HelpFormatter.buildHelp("Admin Commands", "Server administration", commands, null)); } @@ -402,6 +405,21 @@ private void handleVersion(CommandContext ctx, @Nullable Store stor } } + // === Sentry Test === + private void handleSentryTest(CommandContext ctx) { + if (!SentryIntegration.isInitialized()) { + ctx.sendMessage(prefix().insert(msg("Sentry is not initialized. Check config/sentry.json", COLOR_RED))); + return; + } + + boolean sent = SentryIntegration.sendTestEvent(); + if (sent) { + ctx.sendMessage(prefix().insert(msg("Test error sent to Sentry. Check your Sentry dashboard.", COLOR_GREEN))); + } else { + ctx.sendMessage(prefix().insert(msg("Failed to send test event.", COLOR_RED))); + } + } + // === Reload === private void handleReload(CommandContext ctx, PlayerRef player) { if (!hasPermission(player, Permissions.ADMIN)) { diff --git a/src/main/java/com/hyperfactions/config/ConfigManager.java b/src/main/java/com/hyperfactions/config/ConfigManager.java index 1df50f5..13fbba6 100644 --- a/src/main/java/com/hyperfactions/config/ConfigManager.java +++ b/src/main/java/com/hyperfactions/config/ConfigManager.java @@ -49,6 +49,8 @@ public class ConfigManager { private WorldsConfig worldsConfig; + private SentryConfig sentryConfig; + private final WorldSettingsResolver worldSettingsResolver = new WorldSettingsResolver(); private ConfigManager() {} @@ -133,6 +135,9 @@ public void loadAll(@NotNull Path dataDir) { worldsConfig = new WorldsConfig(configDir.resolve("worlds.json")); worldsConfig.load(); + sentryConfig = new SentryConfig(configDir.resolve("sentry.json")); + sentryConfig.load(); + // Build the world settings resolver from loaded config worldSettingsResolver.rebuild(worldsConfig); @@ -208,6 +213,11 @@ private void validateAll() { combined.merge(worldsConfig.getLastValidationResult()); } + sentryConfig.validateAndLog(); + if (sentryConfig.getLastValidationResult() != null) { + combined.merge(sentryConfig.getLastValidationResult()); + } + // Log summary if (combined.hasIssues()) { int warnings = combined.getWarnings().size(); @@ -253,6 +263,7 @@ public void reloadAll() { announcementConfig.reload(); gravestoneConfig.reload(); worldsConfig.reload(); + sentryConfig.reload(); // Rebuild world settings resolver worldSettingsResolver.rebuild(worldsConfig); @@ -278,6 +289,7 @@ public void saveAll() { announcementConfig.save(); gravestoneConfig.save(); worldsConfig.save(); + sentryConfig.save(); } // === Config Accessors === @@ -404,6 +416,16 @@ public WorldsConfig worlds() { return worldsConfig; } + /** + * Gets the sentry module configuration. + * + * @return sentry config + */ + @NotNull + public SentryConfig sentry() { + return sentryConfig; + } + /** * Gets the world settings resolver for per-world behavior queries. * diff --git a/src/main/java/com/hyperfactions/config/modules/SentryConfig.java b/src/main/java/com/hyperfactions/config/modules/SentryConfig.java new file mode 100644 index 0000000..f933228 --- /dev/null +++ b/src/main/java/com/hyperfactions/config/modules/SentryConfig.java @@ -0,0 +1,108 @@ +package com.hyperfactions.config.modules; + +import com.google.gson.JsonObject; +import com.hyperfactions.config.ModuleConfig; +import java.nio.file.Path; +import org.jetbrains.annotations.NotNull; + +/** + * Configuration for Sentry error tracking integration. + * Controls whether errors are reported to Sentry, the DSN, environment, and debug settings. + */ +public class SentryConfig extends ModuleConfig { + + private static final String DEFAULT_DSN = + "https://cc41f97749e8b8b1562defea6ba3de9c@o4510966614589440.ingest.us.sentry.io/4510966616162304"; + + private String dsn = DEFAULT_DSN; + + private String environment = "production"; + + private boolean debug = false; + + private double tracesSampleRate = 0.0; + + /** + * Creates a new sentry config. + * + * @param filePath path to config/sentry.json + */ + public SentryConfig(@NotNull Path filePath) { + super(filePath); + } + + @Override + @NotNull + public String getModuleName() { + return "sentry"; + } + + @Override + protected boolean getDefaultEnabled() { + return true; + } + + @Override + protected void createDefaults() { + enabled = true; + dsn = DEFAULT_DSN; + environment = "production"; + debug = false; + tracesSampleRate = 0.0; + } + + @Override + protected void loadModuleSettings(@NotNull JsonObject root) { + dsn = getString(root, "dsn", dsn); + environment = getString(root, "environment", environment); + debug = getBool(root, "debug", debug); + tracesSampleRate = getDouble(root, "tracesSampleRate", tracesSampleRate); + } + + @Override + protected void writeModuleSettings(@NotNull JsonObject root) { + root.addProperty("dsn", dsn); + root.addProperty("environment", environment); + root.addProperty("debug", debug); + root.addProperty("tracesSampleRate", tracesSampleRate); + } + + /** + * Gets the Sentry DSN (Data Source Name) URL. + * + * @return DSN string, empty if not configured + */ + @NotNull + public String getDsn() { + return dsn; + } + + /** + * Gets the environment name sent to Sentry (e.g., "production", "development"). + * + * @return environment name + */ + @NotNull + public String getEnvironment() { + return environment; + } + + /** + * Checks if Sentry debug logging is enabled. + * + * @return true if debug mode is on + */ + public boolean isDebug() { + return debug; + } + + /** + * Gets the traces sample rate for performance monitoring. + * 0.0 = no performance traces, 1.0 = capture all. + * + * @return sample rate between 0.0 and 1.0 + */ + public double getTracesSampleRate() { + return tracesSampleRate; + } +} \ No newline at end of file diff --git a/src/main/java/com/hyperfactions/integration/SentryIntegration.java b/src/main/java/com/hyperfactions/integration/SentryIntegration.java new file mode 100644 index 0000000..d4c9bfe --- /dev/null +++ b/src/main/java/com/hyperfactions/integration/SentryIntegration.java @@ -0,0 +1,190 @@ +package com.hyperfactions.integration; + +import com.hyperfactions.BuildInfo; +import com.hyperfactions.config.modules.SentryConfig; +import com.hyperfactions.util.Logger; +import io.sentry.Sentry; +import io.sentry.SentryLevel; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Manages Sentry error tracking integration. + * + *

All operations are wrapped in try/catch to guarantee that Sentry issues + * never crash the server. The Sentry SDK sends events asynchronously by default + * (via {@code AsyncHttpTransport}), so event submission is non-blocking. + */ +public final class SentryIntegration { + + private static boolean initialized = false; + + private SentryIntegration() {} + + /** + * Initializes Sentry from the given config. + * Does nothing if Sentry is disabled or the DSN is empty. + * Safe to call multiple times — subsequent calls are ignored. + * + * @param config the sentry configuration + */ + public static void init(@NotNull SentryConfig config) { + if (initialized) { + return; + } + + if (!config.isEnabled()) { + Logger.info("[Sentry] Sentry is disabled in config"); + return; + } + + String dsn = config.getDsn(); + if (dsn == null || dsn.isBlank()) { + Logger.info("[Sentry] No DSN configured — Sentry will not send events"); + return; + } + + try { + Sentry.init(options -> { + options.setDsn(dsn); + options.setRelease("hyperfactions@" + BuildInfo.VERSION); + options.setEnvironment(config.getEnvironment()); + options.setDebug(config.isDebug()); + + // Performance monitoring (0.0 = off by default) + double traceRate = config.getTracesSampleRate(); + if (traceRate > 0.0) { + options.setTracesSampleRate(traceRate); + } + + // Highlight HyperFactions frames in stack traces + options.addInAppInclude("com.hyperfactions"); + + // Don't install UncaughtExceptionHandler — the Hytale server has its own + options.setEnableUncaughtExceptionHandler(false); + + // Give 2 seconds to flush pending events on shutdown + options.setShutdownTimeoutMillis(2000); + + // Limit breadcrumb memory usage + options.setMaxBreadcrumbs(50); + + // Send PII (player context etc.) — server-side, no browser data + options.setSendDefaultPii(true); + }); + + initialized = true; + Logger.info("[Sentry] Initialized (env=%s, debug=%s)", config.getEnvironment(), config.isDebug()); + } catch (Exception e) { + Logger.severe("Failed to initialize Sentry: %s", e.getMessage()); + } + } + + /** + * Captures an exception and sends it to Sentry. + * Also logs the exception to the server console. + * + * @param message context message describing what was happening + * @param throwable the exception to capture + */ + public static void captureException(@NotNull String message, @NotNull Throwable throwable) { + // Always log to console first + Logger.severe("%s: %s", message, throwable.getMessage()); + + if (!initialized) { + return; + } + + try { + Sentry.captureException(throwable, scope -> { + scope.setExtra("context", message); + }); + } catch (Exception e) { + // Never let Sentry failures propagate + Logger.debug("Sentry captureException failed: %s", e.getMessage()); + } + } + + /** + * Captures an exception and sends it to Sentry (without additional context message). + * + * @param throwable the exception to capture + */ + public static void captureException(@NotNull Throwable throwable) { + if (!initialized) { + return; + } + + try { + Sentry.captureException(throwable); + } catch (Exception e) { + Logger.debug("Sentry captureException failed: %s", e.getMessage()); + } + } + + /** + * Captures a plain message at the given severity level. + * + * @param message the message text + * @param level Sentry severity level + */ + public static void captureMessage(@NotNull String message, @NotNull SentryLevel level) { + if (!initialized) { + return; + } + + try { + Sentry.captureMessage(message, level); + } catch (Exception e) { + Logger.debug("Sentry captureMessage failed: %s", e.getMessage()); + } + } + + /** + * Sends a test exception to Sentry to verify the integration is working. + * + * @return true if the event was sent (or at least queued), false if Sentry is not initialized + */ + public static boolean sendTestEvent() { + if (!initialized) { + return false; + } + + try { + Exception testException = new RuntimeException("HyperFactions Sentry test event — this is not a real error"); + Sentry.captureException(testException); + return true; + } catch (Exception e) { + Logger.debug("Sentry test event failed: %s", e.getMessage()); + return false; + } + } + + /** + * Flushes pending events and closes the Sentry SDK. + * Safe to call even if Sentry was never initialized. + */ + public static void close() { + if (!initialized) { + return; + } + + try { + Sentry.close(); + initialized = false; + Logger.info("[Sentry] Closed"); + } catch (Exception e) { + Logger.debug("Sentry close failed: %s", e.getMessage()); + initialized = false; + } + } + + /** + * Checks whether Sentry has been initialized and is active. + * + * @return true if Sentry is sending events + */ + public static boolean isInitialized() { + return initialized; + } +} diff --git a/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java b/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java index e6e1fff..78b4a5a 100644 --- a/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java +++ b/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java @@ -4,7 +4,9 @@ import com.hyperfactions.api.HyperFactionsAPI; import com.hyperfactions.chat.PublicChatListener; import com.hyperfactions.command.FactionCommand; +import com.hyperfactions.config.ConfigManager; import com.hyperfactions.integration.PermissionRegistrar; +import com.hyperfactions.integration.SentryIntegration; import com.hyperfactions.integration.protection.OrbisGuardIntegration; import com.hyperfactions.integration.protection.ProtectionMixinBridge; import com.hyperfactions.listener.PlayerListener; @@ -102,6 +104,9 @@ protected void setup() { // Set API instance HyperFactionsAPI.setInstance(hyperFactions); + // Initialize Sentry error tracking (non-blocking, fail-safe) + SentryIntegration.init(ConfigManager.get().sentry()); + Logger.info("[Startup] HyperFactions setup complete"); } @@ -193,6 +198,9 @@ protected void shutdown() { hyperFactions.disable(); } + // Flush pending Sentry events and close + SentryIntegration.close(); + // Clear tracked players trackedPlayers.clear(); From 987f4bfa76a3b61de9e5ee82fda31992118ebdb6 Mon Sep 17 00:00:00 2001 From: derrickmehaffy Date: Sat, 28 Feb 2026 18:07:19 -0800 Subject: [PATCH 02/10] fix: move Sentry init to start() after config loads Config loads in enable() during start(), not setup(). Calling ConfigManager.get().sentry() in setup() returned null. --- .../com/hyperfactions/platform/HyperFactionsPlugin.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java b/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java index 78b4a5a..9f6a2be 100644 --- a/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java +++ b/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java @@ -104,9 +104,6 @@ protected void setup() { // Set API instance HyperFactionsAPI.setInstance(hyperFactions); - // Initialize Sentry error tracking (non-blocking, fail-safe) - SentryIntegration.init(ConfigManager.get().sentry()); - Logger.info("[Startup] HyperFactions setup complete"); } @@ -116,9 +113,12 @@ protected void start() { // Configure platform callbacks configurePlatformCallbacks(); - // Enable core + // Enable core (loads config, storage, managers) hyperFactions.enable(); + // Initialize Sentry error tracking (after config is loaded, non-blocking, fail-safe) + SentryIntegration.init(ConfigManager.get().sentry()); + // Initialize GravestonePlugin integration (v2 direct API — needs EventRegistry) hyperFactions.initGravestoneIntegration(getEventRegistry()); From f99a01e73dd04f63f72498a7f8f68be1dca15fc1 Mon Sep 17 00:00:00 2001 From: derrickmehaffy Date: Sat, 28 Feb 2026 18:14:36 -0800 Subject: [PATCH 03/10] fix: wire Sentry source upload into build pipeline Fix task dependency for all Sentry tasks on generateBuildInfo. Auto-upload source bundle after every build via finalizedBy. --- build.gradle | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 9b392f5..0b4565f 100644 --- a/build.gradle +++ b/build.gradle @@ -173,6 +173,7 @@ tasks.withType(Checkstyle).configureEach { build { dependsOn shadowJar + finalizedBy tasks.matching { it.name == 'sentryUploadSourceBundleJava' } } tasks.withType(JavaCompile).configureEach { @@ -199,8 +200,8 @@ tasks.named('compileJava') { } } -// Sentry's bundle ID generator uses the same generated sources dir — declare dependency -tasks.matching { it.name == 'generateSentryBundleIdJava' }.configureEach { +// Sentry tasks use the same generated sources dir as generateBuildInfo — declare dependencies +tasks.matching { it.name.startsWith('sentry') || it.name.startsWith('generateSentry') }.configureEach { dependsOn 'generateBuildInfo' } From 577a1d9183cb4fbef041a1329b69e7bb804123e2 Mon Sep 17 00:00:00 2001 From: derrickmehaffy Date: Sat, 28 Feb 2026 18:20:16 -0800 Subject: [PATCH 04/10] feat: attach server context to all Sentry events Tags: hytale.server.version, plugin.version, java.version, os Context: structured server info with CPU count and max memory --- .../integration/SentryIntegration.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hyperfactions/integration/SentryIntegration.java b/src/main/java/com/hyperfactions/integration/SentryIntegration.java index d4c9bfe..72abbd3 100644 --- a/src/main/java/com/hyperfactions/integration/SentryIntegration.java +++ b/src/main/java/com/hyperfactions/integration/SentryIntegration.java @@ -3,10 +3,10 @@ import com.hyperfactions.BuildInfo; import com.hyperfactions.config.modules.SentryConfig; import com.hyperfactions.util.Logger; +import com.hypixel.hytale.common.util.java.ManifestUtil; import io.sentry.Sentry; import io.sentry.SentryLevel; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; /** * Manages Sentry error tracking integration. @@ -73,6 +73,33 @@ public static void init(@NotNull SentryConfig config) { options.setSendDefaultPii(true); }); + // Set global tags — attached to every event + Sentry.configureScope(scope -> { + scope.setTag("plugin.version", BuildInfo.VERSION); + scope.setTag("java.version", System.getProperty("java.version", "unknown")); + + String serverVersion = null; + try { + serverVersion = ManifestUtil.getVersion(); + } catch (Exception ignored) { + // ManifestUtil may not be available yet + } + scope.setTag("hytale.server.version", serverVersion != null ? serverVersion : "unknown"); + + String osName = System.getProperty("os.name", "unknown"); + String osArch = System.getProperty("os.arch", "unknown"); + scope.setTag("os", osName + " " + osArch); + + scope.setContexts("server", java.util.Map.of( + "hytale_version", serverVersion != null ? serverVersion : "unknown", + "plugin_version", BuildInfo.VERSION, + "java_version", System.getProperty("java.version", "unknown"), + "os", osName + " " + osArch, + "available_processors", Runtime.getRuntime().availableProcessors(), + "max_memory_mb", Runtime.getRuntime().maxMemory() / (1024 * 1024) + )); + }); + initialized = true; Logger.info("[Sentry] Initialized (env=%s, debug=%s)", config.getEnvironment(), config.isDebug()); } catch (Exception e) { From ead79f670ed710b8a582ef4cc92d0bedbbc7d21a Mon Sep 17 00:00:00 2001 From: derrickmehaffy Date: Sat, 28 Feb 2026 18:22:15 -0800 Subject: [PATCH 05/10] feat: add Hytale server config to Sentry context Includes server_name, max_players, and MOTD from HytaleServerConfig. --- .../integration/SentryIntegration.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/com/hyperfactions/integration/SentryIntegration.java b/src/main/java/com/hyperfactions/integration/SentryIntegration.java index 72abbd3..9f1cd6b 100644 --- a/src/main/java/com/hyperfactions/integration/SentryIntegration.java +++ b/src/main/java/com/hyperfactions/integration/SentryIntegration.java @@ -4,6 +4,8 @@ import com.hyperfactions.config.modules.SentryConfig; import com.hyperfactions.util.Logger; import com.hypixel.hytale.common.util.java.ManifestUtil; +import com.hypixel.hytale.server.core.HytaleServer; +import com.hypixel.hytale.server.core.HytaleServerConfig; import io.sentry.Sentry; import io.sentry.SentryLevel; import org.jetbrains.annotations.NotNull; @@ -90,7 +92,25 @@ public static void init(@NotNull SentryConfig config) { String osArch = System.getProperty("os.arch", "unknown"); scope.setTag("os", osName + " " + osArch); + // Pull Hytale server config info + String serverName = "unknown"; + int maxPlayers = 0; + String motd = ""; + try { + HytaleServerConfig serverConfig = HytaleServer.get().getConfig(); + serverName = serverConfig.getServerName(); + maxPlayers = serverConfig.getMaxPlayers(); + motd = serverConfig.getMotd(); + } catch (Exception ignored) { + // Server may not be fully initialized yet + } + + scope.setTag("server.name", serverName); + scope.setContexts("server", java.util.Map.of( + "server_name", serverName, + "max_players", maxPlayers, + "motd", motd != null && !motd.isEmpty() ? motd : "(none)", "hytale_version", serverVersion != null ? serverVersion : "unknown", "plugin_version", BuildInfo.VERSION, "java_version", System.getProperty("java.version", "unknown"), From c54d61283ac3bf2ee3c3ea45ba6d4269908d5d65 Mon Sep 17 00:00:00 2001 From: derrickmehaffy Date: Sat, 28 Feb 2026 18:25:48 -0800 Subject: [PATCH 06/10] feat: auto-detect Sentry environment from server patchline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maps ManifestUtil.getPatchline() to Sentry environments: release → production, pre-release → staging, dev → development. Falls back to config value if detection fails. --- .../integration/SentryIntegration.java | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hyperfactions/integration/SentryIntegration.java b/src/main/java/com/hyperfactions/integration/SentryIntegration.java index 9f1cd6b..dc7fb5f 100644 --- a/src/main/java/com/hyperfactions/integration/SentryIntegration.java +++ b/src/main/java/com/hyperfactions/integration/SentryIntegration.java @@ -50,9 +50,25 @@ public static void init(@NotNull SentryConfig config) { Sentry.init(options -> { options.setDsn(dsn); options.setRelease("hyperfactions@" + BuildInfo.VERSION); - options.setEnvironment(config.getEnvironment()); options.setDebug(config.isDebug()); + // Auto-detect environment from server patchline, fall back to config value + String environment = config.getEnvironment(); + try { + String patchline = ManifestUtil.getPatchline(); + if (patchline != null && !patchline.isEmpty()) { + environment = switch (patchline) { + case "release" -> "production"; + case "pre-release" -> "staging"; + case "dev" -> "development"; + default -> patchline; + }; + } + } catch (Exception ignored) { + // ManifestUtil may not be available — use config value + } + options.setEnvironment(environment); + // Performance monitoring (0.0 = off by default) double traceRate = config.getTracesSampleRate(); if (traceRate > 0.0) { @@ -88,6 +104,15 @@ public static void init(@NotNull SentryConfig config) { } scope.setTag("hytale.server.version", serverVersion != null ? serverVersion : "unknown"); + String patchline = "unknown"; + try { + String pl = ManifestUtil.getPatchline(); + if (pl != null && !pl.isEmpty()) { + patchline = pl; + } + } catch (Exception ignored) {} + scope.setTag("hytale.patchline", patchline); + String osName = System.getProperty("os.name", "unknown"); String osArch = System.getProperty("os.arch", "unknown"); scope.setTag("os", osName + " " + osArch); From 2662a33dd80be127ab35d28e8bf7af6cf12f9cac Mon Sep 17 00:00:00 2001 From: derrickmehaffy Date: Sat, 28 Feb 2026 18:36:34 -0800 Subject: [PATCH 07/10] feat: attach installed mods list to Sentry events Lists all loaded plugins with name, version, and state. Refreshed on BootEvent for a complete picture after all plugins load. --- .../integration/SentryIntegration.java | 33 +++++++++++++++++++ .../platform/HyperFactionsPlugin.java | 6 +++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hyperfactions/integration/SentryIntegration.java b/src/main/java/com/hyperfactions/integration/SentryIntegration.java index dc7fb5f..d889261 100644 --- a/src/main/java/com/hyperfactions/integration/SentryIntegration.java +++ b/src/main/java/com/hyperfactions/integration/SentryIntegration.java @@ -6,6 +6,7 @@ import com.hypixel.hytale.common.util.java.ManifestUtil; import com.hypixel.hytale.server.core.HytaleServer; import com.hypixel.hytale.server.core.HytaleServerConfig; +import com.hypixel.hytale.server.core.plugin.PluginBase; import io.sentry.Sentry; import io.sentry.SentryLevel; import org.jetbrains.annotations.NotNull; @@ -147,6 +148,9 @@ public static void init(@NotNull SentryConfig config) { initialized = true; Logger.info("[Sentry] Initialized (env=%s, debug=%s)", config.getEnvironment(), config.isDebug()); + + // Collect installed mods (may be incomplete during init — refreshed on boot) + refreshInstalledMods(); } catch (Exception e) { Logger.severe("Failed to initialize Sentry: %s", e.getMessage()); } @@ -232,6 +236,35 @@ public static boolean sendTestEvent() { } } + /** + * Refreshes the installed mods context on the Sentry scope. + * Call after all plugins have loaded (e.g., on BootEvent) for a complete list. + */ + public static void refreshInstalledMods() { + if (!initialized) { + return; + } + + try { + var plugins = HytaleServer.get().getPluginManager().getPlugins(); + var modList = new java.util.LinkedHashMap(); + for (PluginBase plugin : plugins) { + var id = plugin.getIdentifier(); + var manifest = plugin.getManifest(); + String name = id.getGroup() + ":" + id.getName(); + String version = manifest.getVersion() != null ? manifest.getVersion().toString() : "unknown"; + modList.put(name, version + " (" + plugin.getState() + ")"); + } + + Sentry.configureScope(scope -> { + scope.setTag("mods.count", String.valueOf(modList.size())); + scope.setContexts("installed_mods", (java.util.Map) (java.util.Map) modList); + }); + } catch (Exception e) { + Logger.debug("Failed to collect installed mods for Sentry: %s", e.getMessage()); + } + } + /** * Flushes pending events and closes the Sentry SDK. * Safe to call even if Sentry was never initialized. diff --git a/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java b/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java index 9f6a2be..b8fbd7c 100644 --- a/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java +++ b/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java @@ -165,7 +165,11 @@ protected void start() { worldSetup.logProtectionCoverage(); // Register permission nodes with LuckPerms on BootEvent (after all plugins loaded) - getEventRegistry().registerGlobal(BootEvent.class, e -> PermissionRegistrar.registerWithLuckPerms()); + // Also refresh Sentry mod list now that all plugins are loaded + getEventRegistry().registerGlobal(BootEvent.class, e -> { + PermissionRegistrar.registerWithLuckPerms(); + SentryIntegration.refreshInstalledMods(); + }); Logger.info("[Startup] HyperFactions v%s enabled", getManifest().getVersion()); } From f0fa3636e83594565ad319eb4bf7b66920f5dcd9 Mon Sep 17 00:00:00 2001 From: derrickmehaffy Date: Sat, 28 Feb 2026 18:42:54 -0800 Subject: [PATCH 08/10] fix: resolve installed mods context not appearing in Sentry events Change HashMap type from with unsafe double-cast to which setContexts() accepts directly. Add total_count field and debug logging for verification. --- .../hyperfactions/integration/SentryIntegration.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hyperfactions/integration/SentryIntegration.java b/src/main/java/com/hyperfactions/integration/SentryIntegration.java index d889261..abfd72a 100644 --- a/src/main/java/com/hyperfactions/integration/SentryIntegration.java +++ b/src/main/java/com/hyperfactions/integration/SentryIntegration.java @@ -247,19 +247,22 @@ public static void refreshInstalledMods() { try { var plugins = HytaleServer.get().getPluginManager().getPlugins(); - var modList = new java.util.LinkedHashMap(); + var modMap = new java.util.HashMap(); for (PluginBase plugin : plugins) { var id = plugin.getIdentifier(); var manifest = plugin.getManifest(); String name = id.getGroup() + ":" + id.getName(); String version = manifest.getVersion() != null ? manifest.getVersion().toString() : "unknown"; - modList.put(name, version + " (" + plugin.getState() + ")"); + modMap.put(name, version + " (" + plugin.getState() + ")"); } + modMap.put("total_count", plugins.size()); Sentry.configureScope(scope -> { - scope.setTag("mods.count", String.valueOf(modList.size())); - scope.setContexts("installed_mods", (java.util.Map) (java.util.Map) modList); + scope.setTag("mods.count", String.valueOf(plugins.size())); + scope.setContexts("installed_mods", modMap); }); + + Logger.debug("[Sentry] Refreshed installed mods list (%d plugins)", plugins.size()); } catch (Exception e) { Logger.debug("Failed to collect installed mods for Sentry: %s", e.getMessage()); } From a8924b8cdf2695d4bce747d47436c70cffd668b6 Mon Sep 17 00:00:00 2001 From: derrickmehaffy Date: Sun, 1 Mar 2026 16:38:30 -0800 Subject: [PATCH 09/10] feat: global error handling with Sentry reporting across entire codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ErrorHandler utility class that centralizes error handling — all errors are now logged to console AND reported to Sentry. Convert ~190 Logger.severe() calls in catch blocks across ~50 files to route through ErrorHandler. Wrap scheduled tasks with wrapTask() to prevent silent thread death, guard async futures, and isolate shutdown steps. Add pre-init error buffering to SentryIntegration so errors during config/data loading (before Sentry initializes) are captured and flushed once Sentry is ready, tagged with pre_init:true. Wire WriteResult.Failure storage errors (with nullable Exception cause) through to Sentry via new report(@Nullable) overload. --- CHANGELOG.md | 10 ++ .../java/com/hyperfactions/HyperFactions.java | 3 +- .../hyperfactions/backup/BackupManager.java | 15 +-- .../chat/PublicChatListener.java | 3 +- .../admin/handler/AdminEconomyHandler.java | 9 +- .../com/hyperfactions/config/ConfigFile.java | 5 +- .../hyperfactions/gui/AdminPageOpener.java | 94 +++++--------- .../hyperfactions/gui/FactionPageOpener.java | 91 +++++--------- .../gui/NewPlayerPageOpener.java | 16 +-- .../gui/admin/page/AdminActionsPage.java | 3 +- .../admin/page/AdminEconomyAdjustPage.java | 5 +- .../gui/faction/ChunkMapAsset.java | 3 +- .../integration/SentryIntegration.java | 49 +++++++- .../protection/HyperProtectIntegration.java | 29 ++--- .../lifecycle/MembershipHistoryHandler.java | 7 +- .../manager/ChatHistoryManager.java | 41 +++--- .../hyperfactions/manager/FactionKDCache.java | 3 +- .../hyperfactions/manager/FactionManager.java | 3 +- .../hyperfactions/manager/InviteManager.java | 7 +- .../manager/JoinRequestManager.java | 7 +- .../hyperfactions/manager/PowerManager.java | 7 +- .../hyperfactions/manager/ZoneManager.java | 3 +- .../migration/MigrationRunner.java | 9 +- .../config/ConfigV1ToV2Migration.java | 3 +- .../config/ConfigV2ToV3Migration.java | 3 +- .../config/ConfigV3ToV4Migration.java | 3 +- .../config/ConfigV4ToV5Migration.java | 3 +- .../config/ConfigV5ToV6Migration.java | 3 +- .../migrations/data/DataV0ToV1Migration.java | 3 +- .../platform/HyperFactionsPlugin.java | 45 ++++--- .../platform/PlayerConnectionHandler.java | 90 +++++++------ .../hyperfactions/platform/WorldSetup.java | 3 +- .../protection/ProtectionChecker.java | 9 +- .../ecs/BlockBreakProtectionSystem.java | 3 +- .../ecs/BlockPlaceProtectionSystem.java | 3 +- .../ecs/BlockUseProtectionSystem.java | 3 +- .../ecs/DamageProtectionSystem.java | 3 +- .../ecs/HarvestPickupProtectionSystem.java | 3 +- .../ecs/ItemDropProtectionSystem.java | 3 +- .../ecs/ItemPickupProtectionSystem.java | 3 +- .../protection/ecs/PlayerDeathSystem.java | 3 +- .../protection/ecs/PlayerRespawnSystem.java | 3 +- .../ecs/TeleportCancelOnDamageSystem.java | 3 +- .../storage/JsonEconomyStorage.java | 13 +- .../hyperfactions/storage/StorageUtils.java | 7 +- .../storage/json/JsonChatHistoryStorage.java | 13 +- .../storage/json/JsonFactionStorage.java | 15 +-- .../storage/json/JsonPlayerStorage.java | 19 +-- .../storage/json/JsonZoneStorage.java | 15 +-- .../hyperfactions/update/UpdateChecker.java | 3 +- .../com/hyperfactions/util/ErrorHandler.java | 119 ++++++++++++++++++ .../worldmap/WorldMapRefreshScheduler.java | 7 +- 52 files changed, 500 insertions(+), 328 deletions(-) create mode 100644 src/main/java/com/hyperfactions/util/ErrorHandler.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 01ac94d..e047e99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Sentry cleanly flushes pending events on server shutdown (2s timeout) - Auth token stored in `.sentry-auth-token` file (gitignored) with env var fallback +**Global Error Handling via ErrorHandler** +- New `ErrorHandler` utility class — centralized error handling that logs to console AND reports to Sentry +- 6 static methods covering all error patterns: `report()`, `report(@Nullable)`, `wrapTask()`, `guard()`, `runSafely()` (2 overloads) +- ~190 `Logger.severe()` calls in catch blocks across ~50 files now route through ErrorHandler to Sentry +- Scheduled/timer tasks wrapped with `wrapTask()` — exceptions no longer silently kill scheduler threads +- CompletableFuture chains guarded with `guard()` — async errors no longer swallowed +- Shutdown sequence steps isolated with `runSafely()` — one failure doesn't skip remaining cleanup +- `WriteResult.Failure` storage errors (with `@Nullable Exception cause`) now report to Sentry +- Pre-init error buffering: errors during config/data loading (before Sentry initializes) are buffered and flushed once Sentry is ready, tagged with `pre_init: true` + ## [0.10.2] - 2026-02-28 **Server Version:** `2026.02.19-1a311a592` diff --git a/src/main/java/com/hyperfactions/HyperFactions.java b/src/main/java/com/hyperfactions/HyperFactions.java index 3eb2f62..ae6cdac 100644 --- a/src/main/java/com/hyperfactions/HyperFactions.java +++ b/src/main/java/com/hyperfactions/HyperFactions.java @@ -36,6 +36,7 @@ import com.hyperfactions.update.UpdateChecker; import com.hyperfactions.update.UpdateNotificationListener; import com.hyperfactions.update.UpdateNotificationPreferences; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hyperfactions.worldmap.MapPlayerFilterService; import com.hyperfactions.worldmap.WorldMapService; @@ -245,7 +246,7 @@ public void enable() { Files.writeString(versionFile, "1"); } } catch (IOException e) { - Logger.severe("[Storage] Failed to initialize data directory: %s", e.getMessage()); + ErrorHandler.report("[Storage] Failed to initialize data directory", e); } // Initialize HyperPerms integration (legacy, for backward compatibility) diff --git a/src/main/java/com/hyperfactions/backup/BackupManager.java b/src/main/java/com/hyperfactions/backup/BackupManager.java index ca6850d..319c105 100644 --- a/src/main/java/com/hyperfactions/backup/BackupManager.java +++ b/src/main/java/com/hyperfactions/backup/BackupManager.java @@ -3,6 +3,7 @@ import com.hyperfactions.HyperFactions; import com.hyperfactions.config.ConfigManager; import com.hyperfactions.storage.StorageUtils; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.io.FileOutputStream; import java.io.IOException; @@ -88,7 +89,7 @@ public void init() { initialized = true; Logger.info("[Backup] Initialized, backup directory: %s", backupsDir); } catch (IOException e) { - Logger.severe("[Backup] Failed to create backups directory: %s", e.getMessage()); + ErrorHandler.report("[Backup] Failed to create backups directory", e); } } @@ -202,7 +203,7 @@ private void runScheduledBackup() { } } }).exceptionally(ex -> { - Logger.severe("[Backup] Backup task failed with exception: %s", ex.getMessage()); + ErrorHandler.report("[Backup] Backup task failed with exception", ex); synchronized (backupLock) { backupInProgress = false; backupLock.notifyAll(); @@ -214,7 +215,7 @@ private void runScheduledBackup() { backupInProgress = false; backupLock.notifyAll(); } - Logger.severe("[Backup] Failed to start backup: %s", e.getMessage()); + ErrorHandler.report("[Backup] Failed to start backup", e); } } @@ -353,7 +354,7 @@ public CompletableFuture createBackup( try { Files.deleteIfExists(backupFile); } catch (IOException ignored) {} - Logger.severe("[Backup] Failed to create backup: %s", e.getMessage()); + ErrorHandler.report("[Backup] Failed to create backup", e); return new BackupResult.Failure("Failed to create backup: " + e.getMessage()); } }); @@ -397,7 +398,7 @@ public CompletableFuture restoreBackup(@NotNull String backupName return new RestoreResult.Success(backupName, filesRestored); } catch (Exception e) { - Logger.severe("[Backup] Failed to restore backup: %s", e.getMessage()); + ErrorHandler.report("[Backup] Failed to restore backup", e); return new RestoreResult.Failure("Failed to restore backup: " + e.getMessage()); } }); @@ -421,7 +422,7 @@ public CompletableFuture deleteBackup(@NotNull String backupName) { Logger.info("[Backup] Deleted backup: %s", backupName); return true; } catch (Exception e) { - Logger.severe("[Backup] Failed to delete backup '%s': %s", backupName, e.getMessage()); + ErrorHandler.report(String.format("[Backup] Failed to delete backup '%s'", backupName), e); return false; } }); @@ -460,7 +461,7 @@ public List listBackups() { } } } catch (IOException e) { - Logger.severe("[Backup] Failed to list backups: %s", e.getMessage()); + ErrorHandler.report("[Backup] Failed to list backups", e); } // Sort by timestamp, newest first diff --git a/src/main/java/com/hyperfactions/chat/PublicChatListener.java b/src/main/java/com/hyperfactions/chat/PublicChatListener.java index 1e23996..49d4009 100644 --- a/src/main/java/com/hyperfactions/chat/PublicChatListener.java +++ b/src/main/java/com/hyperfactions/chat/PublicChatListener.java @@ -2,6 +2,7 @@ import com.hyperfactions.HyperFactions; import com.hyperfactions.config.ConfigManager; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.event.EventPriority; import com.hypixel.hytale.server.core.event.events.player.PlayerChatEvent; @@ -65,7 +66,7 @@ public CompletableFuture onPlayerChatAsync( try { return handleChatEvent(event); } catch (Exception e) { - Logger.severe("Error handling chat event", e); + ErrorHandler.report("Error handling chat event", e); return event; } }); diff --git a/src/main/java/com/hyperfactions/command/admin/handler/AdminEconomyHandler.java b/src/main/java/com/hyperfactions/command/admin/handler/AdminEconomyHandler.java index 7e66072..87d86eb 100644 --- a/src/main/java/com/hyperfactions/command/admin/handler/AdminEconomyHandler.java +++ b/src/main/java/com/hyperfactions/command/admin/handler/AdminEconomyHandler.java @@ -8,6 +8,7 @@ import com.hyperfactions.manager.EconomyManager; import com.hyperfactions.util.CommandHelp; import com.hyperfactions.util.HelpFormatter; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.command.system.CommandContext; @@ -156,7 +157,7 @@ private void handleSet(CommandContext ctx, EconomyManager econ, UUID senderUuid, ctx.sendMessage(prefix().insert(msg("Failed: " + result.name(), COLOR_RED))); } }).exceptionally(ex -> { - Logger.severe("Admin economy set balance failed for %s", ex, faction.name()); + ErrorHandler.report(String.format("Admin economy set balance failed for %s", faction.name()), ex); ctx.sendMessage(prefix().insert(msg("An error occurred.", COLOR_RED))); return null; }); @@ -198,7 +199,7 @@ private void handleAdd(CommandContext ctx, EconomyManager econ, UUID senderUuid, ctx.sendMessage(prefix().insert(msg("Failed: " + result.name(), COLOR_RED))); } }).exceptionally(ex -> { - Logger.severe("Admin economy add failed for %s", ex, faction.name()); + ErrorHandler.report(String.format("Admin economy add failed for %s", faction.name()), ex); ctx.sendMessage(prefix().insert(msg("An error occurred.", COLOR_RED))); return null; }); @@ -240,7 +241,7 @@ private void handleTake(CommandContext ctx, EconomyManager econ, UUID senderUuid ctx.sendMessage(prefix().insert(msg("Failed: " + result.name(), COLOR_RED))); } }).exceptionally(ex -> { - Logger.severe("Admin economy take failed for %s", ex, faction.name()); + ErrorHandler.report(String.format("Admin economy take failed for %s", faction.name()), ex); ctx.sendMessage(prefix().insert(msg("An error occurred.", COLOR_RED))); return null; }); @@ -287,7 +288,7 @@ private void handleReset(CommandContext ctx, EconomyManager econ, UUID senderUui ctx.sendMessage(prefix().insert(msg("Failed: " + result.name(), COLOR_RED))); } }).exceptionally(ex -> { - Logger.severe("Admin economy reset failed for %s", ex, faction.name()); + ErrorHandler.report(String.format("Admin economy reset failed for %s", faction.name()), ex); ctx.sendMessage(prefix().insert(msg("An error occurred.", COLOR_RED))); return null; }); diff --git a/src/main/java/com/hyperfactions/config/ConfigFile.java b/src/main/java/com/hyperfactions/config/ConfigFile.java index b1b0a65..cc2cf88 100644 --- a/src/main/java/com/hyperfactions/config/ConfigFile.java +++ b/src/main/java/com/hyperfactions/config/ConfigFile.java @@ -6,6 +6,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.io.IOException; import java.nio.file.Files; @@ -93,7 +94,7 @@ public void load() { save(); } } catch (Exception e) { - Logger.severe("[Config] Failed to load %s: %s", filePath.getFileName(), e.getMessage()); + ErrorHandler.report(String.format("[Config] Failed to load %s", filePath.getFileName()), e); createDefaults(); } } @@ -109,7 +110,7 @@ public void save() { needsSave = false; Logger.debug("[Config] Saved: %s", filePath.getFileName()); } catch (IOException e) { - Logger.severe("[Config] Failed to save %s: %s", filePath.getFileName(), e.getMessage()); + ErrorHandler.report(String.format("[Config] Failed to save %s", filePath.getFileName()), e); } } diff --git a/src/main/java/com/hyperfactions/gui/AdminPageOpener.java b/src/main/java/com/hyperfactions/gui/AdminPageOpener.java index f752ca8..e9a0b04 100644 --- a/src/main/java/com/hyperfactions/gui/AdminPageOpener.java +++ b/src/main/java/com/hyperfactions/gui/AdminPageOpener.java @@ -15,6 +15,7 @@ import com.hyperfactions.gui.test.ButtonTestPage; import com.hyperfactions.manager.*; import com.hyperfactions.storage.PlayerStorage; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; @@ -56,8 +57,7 @@ public void openAdminTagModal(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] Admin TagModalPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open admin TagModalPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open admin TagModalPage", e); } } @@ -80,8 +80,7 @@ public void openAdminDescriptionModal(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] Admin DescriptionModalPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open admin DescriptionModalPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open admin DescriptionModalPage", e); } } @@ -105,8 +104,7 @@ public void openAdminRenameModal(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] Admin RenameModalPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open admin RenameModalPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open admin RenameModalPage", e); } } @@ -149,8 +147,7 @@ public void openAdminDashboard(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminDashboardPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminDashboardPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminDashboardPage", e); } } @@ -175,8 +172,7 @@ public void openAdminActions(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminActionsPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminActionsPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminActionsPage", e); } } @@ -203,8 +199,7 @@ public void openAdminFactions(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminFactionsPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminFactionsPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminFactionsPage", e); } } @@ -232,8 +227,7 @@ public void openAdminPlayers(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminPlayersPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminPlayersPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminPlayersPage", e); } } @@ -264,8 +258,7 @@ public void openAdminDisbandConfirm(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminDisbandConfirmPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminDisbandConfirmPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminDisbandConfirmPage", e); } } @@ -297,8 +290,7 @@ public void openAdminFactionInfo(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminFactionInfoPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminFactionInfoPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminFactionInfoPage", e); } } @@ -330,8 +322,7 @@ public void openAdminEconomy(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminEconomyPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminEconomyPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminEconomyPage", e); } } @@ -365,8 +356,7 @@ public void openAdminEconomyAdjust(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminEconomyAdjustPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminEconomyAdjustPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminEconomyAdjustPage", e); } } @@ -396,8 +386,7 @@ public void openAdminFactionMembers(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminFactionMembersPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminFactionMembersPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminFactionMembersPage", e); } } @@ -434,8 +423,7 @@ public void openAdminPlayerInfo(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminPlayerInfoPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminPlayerInfoPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminPlayerInfoPage", e); } } @@ -465,8 +453,7 @@ public void openAdminFactionRelations(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminFactionRelationsPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminFactionRelationsPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminFactionRelationsPage", e); } } @@ -495,8 +482,7 @@ public void openAdminFactionSettings(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminFactionSettingsPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminFactionSettingsPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminFactionSettingsPage", e); } } @@ -530,8 +516,7 @@ public void openAdminUnclaimAllConfirm(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminUnclaimAllConfirmPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminUnclaimAllConfirmPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminUnclaimAllConfirmPage", e); } } @@ -566,8 +551,7 @@ public void openAdminZone(Player player, Ref ref, pageManager.openCustomPage(ref, store, zonePage); Logger.debug("[GUI] AdminZonePage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminZonePage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminZonePage", e); } } @@ -589,8 +573,7 @@ public void openAdminConfig(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminConfigPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminConfigPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminConfigPage", e); } } @@ -612,8 +595,7 @@ public void openAdminBackups(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminBackupsPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminBackupsPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminBackupsPage", e); } } @@ -639,8 +621,7 @@ public void openAdminActivityLog(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminActivityLogPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminActivityLogPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminActivityLogPage", e); } } @@ -662,8 +643,7 @@ public void openAdminUpdates(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminUpdatesPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminUpdatesPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminUpdatesPage", e); } } @@ -685,8 +665,7 @@ public void openAdminHelp(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminHelpPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminHelpPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminHelpPage", e); } } @@ -707,8 +686,7 @@ public void openAdminVersion(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminVersionPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminVersionPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminVersionPage", e); } } @@ -798,8 +776,7 @@ public void openCreateZoneWizard(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] CreateZoneWizardPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open CreateZoneWizardPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open CreateZoneWizardPage", e); } } @@ -846,8 +823,7 @@ public void openAdminZoneMap(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminZoneMapPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminZoneMapPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminZoneMapPage", e); } } @@ -879,8 +855,7 @@ public void openAdminZoneProperties(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminZonePropertiesPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminZonePropertiesPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminZonePropertiesPage", e); } } @@ -909,8 +884,7 @@ public void openAdminZoneSettings(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminZoneSettingsPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminZoneSettingsPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminZoneSettingsPage", e); } } @@ -947,8 +921,7 @@ public void openAdminZoneSettings(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminZoneSettingsPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminZoneSettingsPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminZoneSettingsPage", e); } } @@ -978,8 +951,7 @@ public void openAdminZoneIntegrationFlags(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminZoneIntegrationFlagsPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open AdminZoneIntegrationFlagsPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open AdminZoneIntegrationFlagsPage", e); } } @@ -1011,8 +983,7 @@ public void openZoneRenameModal(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] ZoneRenameModalPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open ZoneRenameModalPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open ZoneRenameModalPage", e); } } @@ -1065,8 +1036,7 @@ public void openZoneChangeTypeModal(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] ZoneChangeTypeModalPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open ZoneChangeTypeModalPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open ZoneChangeTypeModalPage", e); } } diff --git a/src/main/java/com/hyperfactions/gui/FactionPageOpener.java b/src/main/java/com/hyperfactions/gui/FactionPageOpener.java index 0e490e9..c05c1a4 100644 --- a/src/main/java/com/hyperfactions/gui/FactionPageOpener.java +++ b/src/main/java/com/hyperfactions/gui/FactionPageOpener.java @@ -15,6 +15,7 @@ import com.hyperfactions.gui.test.ButtonTestPage; import com.hyperfactions.manager.*; import com.hyperfactions.storage.PlayerStorage; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; @@ -58,8 +59,7 @@ public void openMainMenu(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] MainMenuPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open MainMenuPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open MainMenuPage", e); } } @@ -97,8 +97,7 @@ public void openFactionMain(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] FactionDashboardPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open FactionDashboardPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open FactionDashboardPage", e); } } else { // Player has no faction - redirect to new player browse page @@ -133,8 +132,7 @@ public void openFactionMembers(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] FactionMembersPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open FactionMembersPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open FactionMembersPage", e); } } @@ -162,8 +160,7 @@ public void openChunkMap(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] ChunkMapPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open ChunkMapPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open ChunkMapPage", e); } } @@ -209,8 +206,7 @@ public void openFactionRelations(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] FactionRelationsPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open FactionRelationsPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open FactionRelationsPage", e); } } @@ -250,8 +246,7 @@ public void openFactionBrowser(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] FactionBrowserPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open FactionBrowserPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open FactionBrowserPage", e); } } @@ -280,8 +275,7 @@ public void openLeaderboard(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] FactionLeaderboardPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open FactionLeaderboardPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open FactionLeaderboardPage", e); } } @@ -314,8 +308,7 @@ public void openFactionInvites(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] FactionInvitesPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open FactionInvitesPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open FactionInvitesPage", e); } } @@ -346,8 +339,7 @@ public void openFactionSettings(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] FactionSettingsPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open FactionSettingsPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open FactionSettingsPage", e); } } @@ -396,8 +388,7 @@ public void openSetRelationModal(Player player, Ref ref, pageManager.openCustomPage(ref, store, modalPage); Logger.debug("[GUI] SetRelationModalPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open SetRelationModalPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open SetRelationModalPage", e); } } @@ -426,8 +417,7 @@ public void openTagModal(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] TagModalPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open TagModalPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open TagModalPage", e); } } @@ -455,8 +445,7 @@ public void openDescriptionModal(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] DescriptionModalPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open DescriptionModalPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open DescriptionModalPage", e); } } @@ -485,8 +474,7 @@ public void openRenameModal(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] RenameModalPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open RenameModalPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open RenameModalPage", e); } } @@ -514,8 +502,7 @@ public void openDisbandConfirm(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] DisbandConfirmPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open DisbandConfirmPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open DisbandConfirmPage", e); } } @@ -548,8 +535,7 @@ public void openTransferConfirm(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] TransferConfirmPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open TransferConfirmPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open TransferConfirmPage", e); } } @@ -582,8 +568,7 @@ public void openFactionDashboard(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] FactionDashboardPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open FactionDashboardPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open FactionDashboardPage", e); } } @@ -611,8 +596,7 @@ public void openLeaveConfirm(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] LeaveConfirmPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open LeaveConfirmPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open LeaveConfirmPage", e); } } @@ -641,8 +625,7 @@ public void openLeaderLeaveConfirm(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] LeaderLeaveConfirmPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open LeaderLeaveConfirmPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open LeaderLeaveConfirmPage", e); } } @@ -676,8 +659,7 @@ public void openFactionChat(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] FactionChatPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open FactionChatPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open FactionChatPage", e); } } @@ -706,8 +688,7 @@ public void openFactionModules(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] FactionModulesPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open FactionModulesPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open FactionModulesPage", e); } } @@ -742,8 +723,7 @@ public void openFactionTreasury(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] TreasuryPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open TreasuryPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open TreasuryPage", e); } } @@ -771,8 +751,7 @@ public void openTreasuryDepositModal(Player player, Ref ref, guiManager, guiManager.getPlugin().get(), faction, mode); player.getPageManager().openCustomPage(ref, store, page); } catch (Exception e) { - Logger.severe("[GUI] Failed to open TreasuryDepositModal: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open TreasuryDepositModal", e); } } @@ -793,8 +772,7 @@ public void openTreasuryTransferSearch(Player player, Ref ref, guiManager, guiManager.getPlugin().get(), faction); player.getPageManager().openCustomPage(ref, store, page); } catch (Exception e) { - Logger.severe("[GUI] Failed to open TreasuryTransferSearch: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open TreasuryTransferSearch", e); } } @@ -816,8 +794,7 @@ public void openTreasuryTransferConfirm(Player player, Ref ref, guiManager, guiManager.getPlugin().get(), faction, targetId, targetName, targetType); player.getPageManager().openCustomPage(ref, store, page); } catch (Exception e) { - Logger.severe("[GUI] Failed to open TreasuryTransferConfirm: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open TreasuryTransferConfirm", e); } } @@ -837,8 +814,7 @@ public void openTreasurySettings(Player player, Ref ref, var page = new TreasurySettingsPage(playerRef, guiManager.getFactionManager().get(), econ, guiManager, faction); player.getPageManager().openCustomPage(ref, store, page); } catch (Exception e) { - Logger.severe("[GUI] Failed to open TreasurySettings: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open TreasurySettings", e); } } @@ -866,8 +842,7 @@ public void openLogsViewer(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] LogsViewerPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open LogsViewerPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open LogsViewerPage", e); } } @@ -917,8 +892,7 @@ public void openFactionInfo(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] FactionInfoPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open FactionInfoPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open FactionInfoPage", e); } } @@ -960,8 +934,7 @@ public void openFactionInfoFromPlayerInfo(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] FactionInfoPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open FactionInfoPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open FactionInfoPage", e); } } @@ -1012,8 +985,7 @@ public void openPlayerInfo(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] PlayerInfoPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open PlayerInfoPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open PlayerInfoPage", e); } } @@ -1029,8 +1001,7 @@ public void openButtonTestPage(Player player, Ref ref, ButtonTestPage page = new ButtonTestPage(playerRef); pageManager.openCustomPage(ref, store, page); } catch (Exception e) { - Logger.severe("[GUI] Failed to open ButtonTestPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open ButtonTestPage", e); } } diff --git a/src/main/java/com/hyperfactions/gui/NewPlayerPageOpener.java b/src/main/java/com/hyperfactions/gui/NewPlayerPageOpener.java index 8da12a2..7a4ad60 100644 --- a/src/main/java/com/hyperfactions/gui/NewPlayerPageOpener.java +++ b/src/main/java/com/hyperfactions/gui/NewPlayerPageOpener.java @@ -15,6 +15,7 @@ import com.hyperfactions.gui.test.ButtonTestPage; import com.hyperfactions.manager.*; import com.hyperfactions.storage.PlayerStorage; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; @@ -96,8 +97,7 @@ public void openNewPlayerBrowse(Player player, Ref ref, pageManager.openCustomPage(ref, store, browsePage); Logger.debug("[GUI] NewPlayerBrowsePage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open NewPlayerBrowsePage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open NewPlayerBrowsePage", e); } } @@ -122,8 +122,7 @@ public void openCreateFactionWizard(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] CreateFactionPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open CreateFactionPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open CreateFactionPage", e); } } @@ -151,8 +150,7 @@ public void openInvitesPage(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] InvitesPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open InvitesPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open InvitesPage", e); } } @@ -180,8 +178,7 @@ public void openNewPlayerMap(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] NewPlayerMapPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open NewPlayerMapPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open NewPlayerMapPage", e); } } @@ -218,8 +215,7 @@ public void openHelp(Player player, Ref ref, pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] HelpMainPage opened successfully"); } catch (Exception e) { - Logger.severe("[GUI] Failed to open HelpMainPage: %s", e.getMessage()); - e.printStackTrace(); + ErrorHandler.report("[GUI] Failed to open HelpMainPage", e); } } diff --git a/src/main/java/com/hyperfactions/gui/admin/page/AdminActionsPage.java b/src/main/java/com/hyperfactions/gui/admin/page/AdminActionsPage.java index 8bad5f3..d7819f7 100644 --- a/src/main/java/com/hyperfactions/gui/admin/page/AdminActionsPage.java +++ b/src/main/java/com/hyperfactions/gui/admin/page/AdminActionsPage.java @@ -5,6 +5,7 @@ import com.hyperfactions.gui.admin.AdminNavBarHelper; import com.hyperfactions.gui.admin.data.AdminActionsData; import com.hyperfactions.storage.PlayerStorage; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hyperfactions.util.MessageUtil; import com.hypixel.hytale.component.Ref; @@ -120,7 +121,7 @@ public void handleDataEvent(Ref ref, Store store, playerRef.getUsername(), allUuids.size()); } catch (Exception e) { player.sendMessage(MessageUtil.adminError("Failed to reset K/D: " + e.getMessage())); - Logger.severe("[Admin] Global K/D reset failed: %s", e.getMessage()); + ErrorHandler.report("[Admin] Global K/D reset failed", e); } // Reopen page to reset state diff --git a/src/main/java/com/hyperfactions/gui/admin/page/AdminEconomyAdjustPage.java b/src/main/java/com/hyperfactions/gui/admin/page/AdminEconomyAdjustPage.java index eaf0f64..e3a799d 100644 --- a/src/main/java/com/hyperfactions/gui/admin/page/AdminEconomyAdjustPage.java +++ b/src/main/java/com/hyperfactions/gui/admin/page/AdminEconomyAdjustPage.java @@ -9,6 +9,7 @@ import com.hyperfactions.gui.admin.data.AdminEconomyAdjustData; import com.hyperfactions.manager.EconomyManager; import com.hyperfactions.manager.FactionManager; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; @@ -149,7 +150,7 @@ public void handleDataEvent(Ref ref, Store store, economyManager.adminAdjust(factionId, amount, playerRef.getUuid(), desc) .thenAccept(result -> handleResult(result, player, ref, store, playerRef)) .exceptionally(ex -> { - Logger.severe("Admin economy adjust failed for faction %s", ex, factionId); + ErrorHandler.report(String.format("Admin economy adjust failed for faction %s", factionId), ex); showError("An error occurred."); return null; }); @@ -172,7 +173,7 @@ public void handleDataEvent(Ref ref, Store store, economyManager.setBalance(factionId, newBalance, playerRef.getUuid()) .thenAccept(result -> handleResult(result, player, ref, store, playerRef)) .exceptionally(ex -> { - Logger.severe("Admin economy set balance failed for faction %s", ex, factionId); + ErrorHandler.report(String.format("Admin economy set balance failed for faction %s", factionId), ex); showError("An error occurred."); return null; }); diff --git a/src/main/java/com/hyperfactions/gui/faction/ChunkMapAsset.java b/src/main/java/com/hyperfactions/gui/faction/ChunkMapAsset.java index 4e3e206..9376a28 100644 --- a/src/main/java/com/hyperfactions/gui/faction/ChunkMapAsset.java +++ b/src/main/java/com/hyperfactions/gui/faction/ChunkMapAsset.java @@ -1,5 +1,6 @@ package com.hyperfactions.gui.faction; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.common.util.ArrayUtil; import com.hypixel.hytale.math.util.ChunkUtil; @@ -149,7 +150,7 @@ public static CompletableFuture generate(PlayerRef player, int ce ImageIO.write(image, "PNG", baos); return new ChunkMapAsset(baos.toByteArray()); } catch (IOException e) { - Logger.severe("Failed to encode terrain map PNG", e); + ErrorHandler.report("Failed to encode terrain map PNG", e); return null; } }); diff --git a/src/main/java/com/hyperfactions/integration/SentryIntegration.java b/src/main/java/com/hyperfactions/integration/SentryIntegration.java index abfd72a..44631b6 100644 --- a/src/main/java/com/hyperfactions/integration/SentryIntegration.java +++ b/src/main/java/com/hyperfactions/integration/SentryIntegration.java @@ -9,6 +9,8 @@ import com.hypixel.hytale.server.core.plugin.PluginBase; import io.sentry.Sentry; import io.sentry.SentryLevel; +import java.util.ArrayList; +import java.util.List; import org.jetbrains.annotations.NotNull; /** @@ -17,11 +19,19 @@ *

All operations are wrapped in try/catch to guarantee that Sentry issues * never crash the server. The Sentry SDK sends events asynchronously by default * (via {@code AsyncHttpTransport}), so event submission is non-blocking. + * + *

Errors that occur before Sentry is initialized (e.g., during config loading) + * are buffered and flushed once {@link #init} completes successfully. */ public final class SentryIntegration { private static boolean initialized = false; + /** Buffered errors from before Sentry was initialized. */ + private static final List preInitErrors = new ArrayList<>(); + + private record BufferedError(String message, Throwable throwable) {} + private SentryIntegration() {} /** @@ -151,6 +161,10 @@ public static void init(@NotNull SentryConfig config) { // Collect installed mods (may be incomplete during init — refreshed on boot) refreshInstalledMods(); + + // Flush any errors that occurred before Sentry was ready (e.g., during config loading) + // Done after refreshInstalledMods() so the mods context is attached to flushed events + flushPreInitErrors(); } catch (Exception e) { Logger.severe("Failed to initialize Sentry: %s", e.getMessage()); } @@ -164,10 +178,14 @@ public static void init(@NotNull SentryConfig config) { * @param throwable the exception to capture */ public static void captureException(@NotNull String message, @NotNull Throwable throwable) { - // Always log to console first - Logger.severe("%s: %s", message, throwable.getMessage()); + // Always log to console first (with full stack trace via withCause) + Logger.severe(message, throwable); if (!initialized) { + // Buffer for later — will be flushed after init() completes + synchronized (preInitErrors) { + preInitErrors.add(new BufferedError(message, throwable)); + } return; } @@ -287,6 +305,33 @@ public static void close() { } } + /** + * Flushes errors that were buffered before Sentry was initialized. + * Called once from {@link #init} after Sentry is ready. + */ + private static void flushPreInitErrors() { + List errors; + synchronized (preInitErrors) { + if (preInitErrors.isEmpty()) { + return; + } + errors = new ArrayList<>(preInitErrors); + preInitErrors.clear(); + } + + Logger.info("[Sentry] Flushing %d pre-init error(s) to Sentry", errors.size()); + for (BufferedError error : errors) { + try { + Sentry.captureException(error.throwable(), scope -> { + scope.setExtra("context", error.message()); + scope.setTag("pre_init", "true"); + }); + } catch (Exception e) { + Logger.debug("Sentry flush failed for pre-init error: %s", e.getMessage()); + } + } + } + /** * Checks whether Sentry has been initialized and is active. * diff --git a/src/main/java/com/hyperfactions/integration/protection/HyperProtectIntegration.java b/src/main/java/com/hyperfactions/integration/protection/HyperProtectIntegration.java index 3f5c3a2..32bc906 100644 --- a/src/main/java/com/hyperfactions/integration/protection/HyperProtectIntegration.java +++ b/src/main/java/com/hyperfactions/integration/protection/HyperProtectIntegration.java @@ -2,6 +2,7 @@ import com.hyperfactions.HyperFactions; import com.hyperfactions.protection.ProtectionChecker; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.entity.entities.Player; @@ -183,7 +184,7 @@ public static void registerAllHooks(@NotNull HyperFactions hf) { MethodType.methodType(Message.class, String.class)); bridge.set(SLOT_FORMAT_HANDLE, fmtHandle); } catch (Exception e) { - Logger.severe("Failed to register format handle at slot 15: %s", e.getMessage()); + ErrorHandler.report("Failed to register format handle at slot 15", e); } Logger.debug("Registered 20 HyperProtect hook(s) + format handle at bridge slots (unconditional)"); @@ -267,7 +268,7 @@ public static void registerUniqueHooks(@NotNull HyperFactions hf) { MethodType.methodType(Message.class, String.class)); bridge.set(SLOT_FORMAT_HANDLE, fmtHandle); } catch (Exception e) { - Logger.severe("Failed to register format handle at slot 15: %s", e.getMessage()); + ErrorHandler.report("Failed to register format handle at slot 15", e); } Logger.debug("Registered HP unique hooks (slots 0,9-12,15-22) for BOTH mode"); @@ -336,7 +337,7 @@ public int evaluate(UUID playerUuid, String worldName, int x, int y, int z) { } return verdict; } catch (Exception e) { - Logger.severe("Block break mixin hook error (fail-closed)", e); + ErrorHandler.report("Block break mixin hook error (fail-closed)", e); cachedReason.set("Protection error — action blocked for safety."); return DENY_WITH_MESSAGE; } @@ -369,7 +370,7 @@ public int evaluateExplosion(Object world, int x, int y, int z) { Logger.debugInteraction("[Mixin:Explosion] world=%s, pos=(%d,%d,%d), blocked=%b", worldName, x, y, z, blocked); return blocked ? DENY_SILENT : ALLOW; } catch (Exception e) { - Logger.severe("Explosion mixin hook error (fail-closed)", e); + ErrorHandler.report("Explosion mixin hook error (fail-closed)", e); return DENY_SILENT; } } @@ -412,7 +413,7 @@ public int evaluatePaste(UUID playerUuid, String worldName, int x, int y, int z) } return verdict; } catch (Exception e) { - Logger.severe("Builder tools mixin hook error (fail-closed)", e); + ErrorHandler.report("Builder tools mixin hook error (fail-closed)", e); cachedReason.set("Protection error — action blocked for safety."); return DENY_WITH_MESSAGE; } @@ -494,7 +495,7 @@ public int evaluateCrafting(UUID playerUuid, String worldName, int x, int y, int } return verdict; } catch (Exception e) { - Logger.severe("Container access mixin hook error (fail-closed)", e); + ErrorHandler.report("Container access mixin hook error (fail-closed)", e); cachedReason.set("Protection error — action blocked for safety."); return DENY_WITH_MESSAGE; } @@ -545,7 +546,7 @@ public int evaluateTeleporter(UUID playerUuid, String worldName, int x, int y, i } return verdict; } catch (Exception e) { - Logger.severe("Teleporter mixin hook error (fail-closed)", e); + ErrorHandler.report("Teleporter mixin hook error (fail-closed)", e); cachedReason.set("Protection error — action blocked for safety."); return DENY_WITH_MESSAGE; } @@ -580,7 +581,7 @@ public int evaluateGateway(UUID playerUuid, String worldName, int x, int y, int } return verdict; } catch (Exception e) { - Logger.severe("Portal mixin hook error (fail-closed)", e); + ErrorHandler.report("Portal mixin hook error (fail-closed)", e); cachedReason.set("Protection error — action blocked for safety."); return DENY_WITH_MESSAGE; } @@ -660,7 +661,7 @@ public int evaluateEntityDamage(UUID attackerUuid, UUID targetUuid, String world } return verdict; } catch (Exception e) { - Logger.severe("Entity damage mixin hook error (fail-closed)", e); + ErrorHandler.report("Entity damage mixin hook error (fail-closed)", e); cachedReason.set("Protection error — action blocked for safety."); return DENY_WITH_MESSAGE; } @@ -696,7 +697,7 @@ public int evaluateContainerOpen(UUID playerUuid, String worldName, int x, int y } return verdict; } catch (Exception e) { - Logger.severe("Container open mixin hook error (fail-closed)", e); + ErrorHandler.report("Container open mixin hook error (fail-closed)", e); cachedReason.set("Protection error — action blocked for safety."); return DENY_WITH_MESSAGE; } @@ -730,7 +731,7 @@ public int evaluateBlockPlace(UUID playerUuid, String worldName, int x, int y, i } return verdict; } catch (Exception e) { - Logger.severe("Block place mixin hook error (fail-closed)", e); + ErrorHandler.report("Block place mixin hook error (fail-closed)", e); cachedReason.set("Protection error — action blocked for safety."); return DENY_WITH_MESSAGE; } @@ -765,7 +766,7 @@ public int evaluateHammer(UUID playerUuid, String worldName, int x, int y, int z } return verdict; } catch (Exception e) { - Logger.severe("Hammer mixin hook error (fail-closed)", e); + ErrorHandler.report("Hammer mixin hook error (fail-closed)", e); cachedReason.set("Protection error — action blocked for safety."); return DENY_WITH_MESSAGE; } @@ -813,7 +814,7 @@ public int evaluateUse(UUID playerUuid, String worldName, int x, int y, int z) { } return verdict; } catch (Exception e) { - Logger.severe("Use mixin hook error (fail-closed)", e); + ErrorHandler.report("Use mixin hook error (fail-closed)", e); cachedReason.set("Protection error — action blocked for safety."); return DENY_WITH_MESSAGE; } @@ -848,7 +849,7 @@ public int evaluateSeat(UUID playerUuid, String worldName, int x, int y, int z) } return verdict; } catch (Exception e) { - Logger.severe("Seat mixin hook error (fail-closed)", e); + ErrorHandler.report("Seat mixin hook error (fail-closed)", e); cachedReason.set("Protection error — action blocked for safety."); return DENY_WITH_MESSAGE; } diff --git a/src/main/java/com/hyperfactions/lifecycle/MembershipHistoryHandler.java b/src/main/java/com/hyperfactions/lifecycle/MembershipHistoryHandler.java index 44621ac..fb0e341 100644 --- a/src/main/java/com/hyperfactions/lifecycle/MembershipHistoryHandler.java +++ b/src/main/java/com/hyperfactions/lifecycle/MembershipHistoryHandler.java @@ -10,6 +10,7 @@ import com.hyperfactions.data.PlayerData; import com.hyperfactions.manager.FactionManager; import com.hyperfactions.storage.PlayerStorage; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.util.UUID; import org.jetbrains.annotations.NotNull; @@ -72,7 +73,7 @@ public void handleMembershipHistory(@NotNull FactionMemberEvent event) { } playerStorage.savePlayerData(data); }).exceptionally(e -> { - Logger.severe("Failed to record membership history for %s", e, playerUuid); + ErrorHandler.report(String.format("Failed to record membership history for %s", playerUuid), e); return null; }); } @@ -91,7 +92,7 @@ public void handleDisbandHistory(@NotNull FactionDisbandEvent event) { playerStorage.savePlayerData(data); Logger.debug("Membership history: %s's faction %s disbanded", memberUuid, faction.name()); }).exceptionally(e -> { - Logger.severe("Failed to record disband history for %s", e, memberUuid); + ErrorHandler.report(String.format("Failed to record disband history for %s", memberUuid), e); return null; }); } @@ -140,7 +141,7 @@ public void migrateMembershipHistory() { playerStorage.savePlayerData(data).join(); migrated++; } catch (Exception e) { - Logger.severe("Failed to migrate membership history for %s", e, member.username()); + ErrorHandler.report(String.format("Failed to migrate membership history for %s", member.username()), e); } } } diff --git a/src/main/java/com/hyperfactions/manager/ChatHistoryManager.java b/src/main/java/com/hyperfactions/manager/ChatHistoryManager.java index dffbdf9..348ab04 100644 --- a/src/main/java/com/hyperfactions/manager/ChatHistoryManager.java +++ b/src/main/java/com/hyperfactions/manager/ChatHistoryManager.java @@ -6,6 +6,7 @@ import com.hyperfactions.data.FactionChatHistory; import com.hyperfactions.data.FactionRelation; import com.hyperfactions.storage.ChatHistoryStorage; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.util.ArrayList; import java.util.Comparator; @@ -189,22 +190,24 @@ public void pruneExpired() { } // Prune on-disk histories not in cache - storage.listAllFactionIds().thenAccept(ids -> { - for (UUID id : ids) { - if (!cache.containsKey(id)) { - storage.loadHistory(id).thenAccept(history -> { - FactionChatHistory pruned = history.pruneOlderThan(cutoff); - if (pruned != history) { - if (pruned.isEmpty()) { - storage.deleteHistory(id); - } else { - storage.saveHistory(pruned); - } - } - }); + ErrorHandler.guard("Chat history: prune expired on-disk histories", + storage.listAllFactionIds().thenAccept(ids -> { + for (UUID id : ids) { + if (!cache.containsKey(id)) { + ErrorHandler.guard("Chat history: prune faction " + id, + storage.loadHistory(id).thenAccept(history -> { + FactionChatHistory pruned = history.pruneOlderThan(cutoff); + if (pruned != history) { + if (pruned.isEmpty()) { + storage.deleteHistory(id); + } else { + storage.saveHistory(pruned); + } + } + })); + } } - } - }); + })); Logger.debug("Chat history retention cleanup completed (cutoff: %d days)", retentionDays); } @@ -250,13 +253,13 @@ public void shutdown() { private void scheduleDebouncedSave(@NotNull UUID factionId) { cancelPendingSave(factionId); - ScheduledFuture future = scheduler.schedule(() -> { + ScheduledFuture future = scheduler.schedule(ErrorHandler.wrapTask("Chat history debounced save", () -> { pendingSaves.remove(factionId); FactionChatHistory history = cache.get(factionId); if (history != null) { - storage.saveHistory(history); + ErrorHandler.guard("Chat history save for faction " + factionId, storage.saveHistory(history)); } - }, DEBOUNCE_DELAY_MS, TimeUnit.MILLISECONDS); + }), DEBOUNCE_DELAY_MS, TimeUnit.MILLISECONDS); pendingSaves.put(factionId, future); } @@ -278,7 +281,7 @@ private void flushPendingSave(@NotNull UUID factionId) { try { storage.saveHistory(history).join(); } catch (Exception e) { - Logger.severe("Failed to flush chat history for %s: %s", factionId, e.getMessage()); + ErrorHandler.report(String.format("Failed to flush chat history for %s", factionId), e); } } } diff --git a/src/main/java/com/hyperfactions/manager/FactionKDCache.java b/src/main/java/com/hyperfactions/manager/FactionKDCache.java index 5edd7d9..8ffbfeb 100644 --- a/src/main/java/com/hyperfactions/manager/FactionKDCache.java +++ b/src/main/java/com/hyperfactions/manager/FactionKDCache.java @@ -3,6 +3,7 @@ import com.hyperfactions.data.Faction; import com.hyperfactions.data.PlayerData; import com.hyperfactions.storage.PlayerStorage; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.util.Map; import java.util.Optional; @@ -111,7 +112,7 @@ private void refresh() { Logger.debug("[KDCache] Refreshed K/D stats for %d factions", newCache.size()); } catch (Exception e) { - Logger.severe("[KDCache] Failed to refresh: %s", e.getMessage()); + ErrorHandler.report("[KDCache] Failed to refresh", e); } } } diff --git a/src/main/java/com/hyperfactions/manager/FactionManager.java b/src/main/java/com/hyperfactions/manager/FactionManager.java index c199bb5..d37b8cb 100644 --- a/src/main/java/com/hyperfactions/manager/FactionManager.java +++ b/src/main/java/com/hyperfactions/manager/FactionManager.java @@ -8,6 +8,7 @@ import com.hyperfactions.data.*; import com.hyperfactions.integration.PermissionManager; import com.hyperfactions.storage.FactionStorage; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -146,7 +147,7 @@ public CompletableFuture loadAll() { Logger.info("[Startup] Loaded %d factions with %d members indexed", factions.size(), playerToFaction.size()); }).exceptionally(ex -> { - Logger.severe("CRITICAL: Exception during faction loading - keeping existing data", (Throwable) ex); + ErrorHandler.report("CRITICAL: Exception during faction loading - keeping existing data", ex); return null; }); } diff --git a/src/main/java/com/hyperfactions/manager/InviteManager.java b/src/main/java/com/hyperfactions/manager/InviteManager.java index dd45d3c..bbbfa92 100644 --- a/src/main/java/com/hyperfactions/manager/InviteManager.java +++ b/src/main/java/com/hyperfactions/manager/InviteManager.java @@ -11,6 +11,7 @@ import com.hyperfactions.data.PendingInvite; import com.hyperfactions.integration.PermissionManager; import com.hyperfactions.storage.StorageUtils; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.io.IOException; import java.nio.file.Files; @@ -459,7 +460,7 @@ private void load() { Logger.info("[Storage] Loaded %d invites (%d expired and skipped)", loaded, expired); } catch (Exception e) { - Logger.severe("Failed to load invites", e); + ErrorHandler.report("Failed to load invites", e); } } @@ -470,7 +471,7 @@ private void save() { try { Files.createDirectories(dataFile.getParent()); } catch (IOException e) { - Logger.severe("Failed to create invites directory", e); + ErrorHandler.report("Failed to create invites directory", e); return; } @@ -485,7 +486,7 @@ private void save() { StorageUtils.WriteResult result = StorageUtils.writeAtomic(dataFile, gson.toJson(array)); if (result instanceof StorageUtils.WriteResult.Failure failure) { - Logger.severe("Failed to save invites: %s", failure.error()); + ErrorHandler.report(String.format("Failed to save invites: %s", failure.error()), failure.cause()); } } diff --git a/src/main/java/com/hyperfactions/manager/JoinRequestManager.java b/src/main/java/com/hyperfactions/manager/JoinRequestManager.java index 20075f7..53edaab 100644 --- a/src/main/java/com/hyperfactions/manager/JoinRequestManager.java +++ b/src/main/java/com/hyperfactions/manager/JoinRequestManager.java @@ -11,6 +11,7 @@ import com.hyperfactions.data.JoinRequest; import com.hyperfactions.integration.PermissionManager; import com.hyperfactions.storage.StorageUtils; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.io.IOException; import java.nio.file.Files; @@ -488,7 +489,7 @@ private void load() { Logger.info("[Storage] Loaded %d join requests (%d expired and skipped)", loaded, expired); } catch (Exception e) { - Logger.severe("Failed to load join requests", e); + ErrorHandler.report("Failed to load join requests", e); } } @@ -499,7 +500,7 @@ private void save() { try { Files.createDirectories(dataFile.getParent()); } catch (IOException e) { - Logger.severe("Failed to create join requests directory", e); + ErrorHandler.report("Failed to create join requests directory", e); return; } @@ -514,7 +515,7 @@ private void save() { StorageUtils.WriteResult result = StorageUtils.writeAtomic(dataFile, gson.toJson(array)); if (result instanceof StorageUtils.WriteResult.Failure failure) { - Logger.severe("Failed to save join requests: %s", failure.error()); + ErrorHandler.report(String.format("Failed to save join requests: %s", failure.error()), failure.cause()); } } diff --git a/src/main/java/com/hyperfactions/manager/PowerManager.java b/src/main/java/com/hyperfactions/manager/PowerManager.java index 250f1d6..22209ae 100644 --- a/src/main/java/com/hyperfactions/manager/PowerManager.java +++ b/src/main/java/com/hyperfactions/manager/PowerManager.java @@ -9,6 +9,7 @@ import com.hyperfactions.data.Faction; import com.hyperfactions.data.PlayerPower; import com.hyperfactions.storage.PlayerStorage; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.io.IOException; import java.nio.file.Files; @@ -79,7 +80,7 @@ public CompletableFuture loadAll() { Logger.info("[Startup] Loaded %d player power records", powerCache.size()); }).exceptionally(ex -> { - Logger.severe("CRITICAL: Exception during player power loading - keeping existing data", (Throwable) ex); + ErrorHandler.report("CRITICAL: Exception during player power loading - keeping existing data", ex); return null; }); } @@ -600,7 +601,7 @@ public CompletableFuture loadHardcorePowerData() { } Logger.info("[Startup] Loaded hardcore power for %d factions", hardcoreFactionPower.size()); } catch (Exception e) { - Logger.severe("Failed to load hardcore power data: %s", e.getMessage()); + ErrorHandler.report("Failed to load hardcore power data", e); } }); } @@ -619,7 +620,7 @@ public CompletableFuture saveHardcorePowerData() { Gson gson = new GsonBuilder().setPrettyPrinting().create(); Files.writeString(file, gson.toJson(obj)); } catch (IOException e) { - Logger.severe("Failed to save hardcore power data: %s", e.getMessage()); + ErrorHandler.report("Failed to save hardcore power data", e); } }); } diff --git a/src/main/java/com/hyperfactions/manager/ZoneManager.java b/src/main/java/com/hyperfactions/manager/ZoneManager.java index e315ab5..c353503 100644 --- a/src/main/java/com/hyperfactions/manager/ZoneManager.java +++ b/src/main/java/com/hyperfactions/manager/ZoneManager.java @@ -5,6 +5,7 @@ import com.hyperfactions.data.ZoneFlags; import com.hyperfactions.data.ZoneType; import com.hyperfactions.storage.ZoneStorage; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -179,7 +180,7 @@ public CompletableFuture loadAll() { Logger.info("[Startup] Loaded %d zones with %d total chunks", zonesById.size(), zoneIndex.size()); }).exceptionally(ex -> { - Logger.severe("CRITICAL: Exception during zone loading - keeping existing data", (Throwable) ex); + ErrorHandler.report("CRITICAL: Exception during zone loading - keeping existing data", ex); return null; }); } diff --git a/src/main/java/com/hyperfactions/migration/MigrationRunner.java b/src/main/java/com/hyperfactions/migration/MigrationRunner.java index 5ce8f7a..1f601fc 100644 --- a/src/main/java/com/hyperfactions/migration/MigrationRunner.java +++ b/src/main/java/com/hyperfactions/migration/MigrationRunner.java @@ -2,6 +2,7 @@ import com.hyperfactions.backup.BackupMetadata; import com.hyperfactions.backup.BackupType; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.io.FileOutputStream; import java.io.IOException; @@ -113,7 +114,7 @@ public MigrationResult runMigration(@NotNull Migration migration) { Logger.info("[Migration] Created backup at: %s", backupPath); } catch (IOException e) { Duration duration = Duration.between(startTime, Instant.now()); - Logger.severe("[Migration] Failed to create backup: %s", e.getMessage()); + ErrorHandler.report("[Migration] Failed to create backup", e); return MigrationResult.failure( migration.id(), migration.fromVersion(), @@ -148,7 +149,7 @@ public MigrationResult runMigration(@NotNull Migration migration) { duration ); } catch (IOException rollbackError) { - Logger.severe("[Migration] Rollback failed: %s", rollbackError.getMessage()); + ErrorHandler.report("[Migration] Rollback failed", rollbackError); Duration duration = Duration.between(startTime, Instant.now()); return MigrationResult.failure( migration.id(), @@ -165,7 +166,7 @@ public MigrationResult runMigration(@NotNull Migration migration) { return result; } catch (Exception e) { - Logger.severe("[Migration] Migration threw exception: %s", e.getMessage()); + ErrorHandler.report("[Migration] Migration threw exception", e); // Attempt rollback boolean rolledBack = false; @@ -174,7 +175,7 @@ public MigrationResult runMigration(@NotNull Migration migration) { rollback(migration, backupPath); rolledBack = true; } catch (IOException rollbackError) { - Logger.severe("[Migration] Rollback failed: %s", rollbackError.getMessage()); + ErrorHandler.report("[Migration] Rollback failed", rollbackError); } } diff --git a/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV1ToV2Migration.java b/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV1ToV2Migration.java index 8d8a924..4e27720 100644 --- a/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV1ToV2Migration.java +++ b/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV1ToV2Migration.java @@ -8,6 +8,7 @@ import com.hyperfactions.migration.MigrationOptions; import com.hyperfactions.migration.MigrationResult; import com.hyperfactions.migration.MigrationType; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.io.IOException; import java.nio.file.Files; @@ -165,7 +166,7 @@ public MigrationResult execute(@NotNull Path dataDir, @NotNull MigrationOptions } catch (Exception e) { Duration duration = Duration.between(startTime, Instant.now()); - Logger.severe("[Migration] Config migration failed: %s", e.getMessage()); + ErrorHandler.report("[Migration] Config migration failed", e); return MigrationResult.failure( id(), fromVersion(), diff --git a/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV2ToV3Migration.java b/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV2ToV3Migration.java index 1140504..ddeb161 100644 --- a/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV2ToV3Migration.java +++ b/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV2ToV3Migration.java @@ -8,6 +8,7 @@ import com.hyperfactions.migration.MigrationOptions; import com.hyperfactions.migration.MigrationResult; import com.hyperfactions.migration.MigrationType; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.io.IOException; import java.nio.file.Files; @@ -202,7 +203,7 @@ public MigrationResult execute(@NotNull Path dataDir, @NotNull MigrationOptions } catch (Exception e) { Duration duration = Duration.between(startTime, Instant.now()); - Logger.severe("[Migration] Config migration failed: %s", e.getMessage()); + ErrorHandler.report("[Migration] Config migration failed", e); return MigrationResult.failure( id(), fromVersion(), diff --git a/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV3ToV4Migration.java b/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV3ToV4Migration.java index 816dacd..8ccbd06 100644 --- a/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV3ToV4Migration.java +++ b/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV3ToV4Migration.java @@ -10,6 +10,7 @@ import com.hyperfactions.migration.MigrationOptions; import com.hyperfactions.migration.MigrationResult; import com.hyperfactions.migration.MigrationType; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.nio.file.Files; import java.nio.file.Path; @@ -161,7 +162,7 @@ public MigrationResult execute(@NotNull Path dataDir, @NotNull MigrationOptions } catch (Exception e) { Duration duration = Duration.between(startTime, Instant.now()); - Logger.severe("[Migration] Config migration v3->v4 failed: %s", e.getMessage()); + ErrorHandler.report("[Migration] Config migration v3->v4 failed", e); return MigrationResult.failure( id(), fromVersion(), diff --git a/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV4ToV5Migration.java b/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV4ToV5Migration.java index 1c6d81d..fbb2a92 100644 --- a/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV4ToV5Migration.java +++ b/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV4ToV5Migration.java @@ -8,6 +8,7 @@ import com.hyperfactions.migration.MigrationOptions; import com.hyperfactions.migration.MigrationResult; import com.hyperfactions.migration.MigrationType; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.nio.file.Files; import java.nio.file.Path; @@ -148,7 +149,7 @@ public MigrationResult execute(@NotNull Path dataDir, @NotNull MigrationOptions } catch (Exception e) { Duration duration = Duration.between(startTime, Instant.now()); - Logger.severe("[Migration] Config migration v4->v5 failed: %s", e.getMessage()); + ErrorHandler.report("[Migration] Config migration v4->v5 failed", e); return MigrationResult.failure( id(), fromVersion(), diff --git a/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV5ToV6Migration.java b/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV5ToV6Migration.java index bbed6ee..c72a336 100644 --- a/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV5ToV6Migration.java +++ b/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV5ToV6Migration.java @@ -8,6 +8,7 @@ import com.hyperfactions.migration.MigrationOptions; import com.hyperfactions.migration.MigrationResult; import com.hyperfactions.migration.MigrationType; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.nio.file.Files; import java.nio.file.Path; @@ -204,7 +205,7 @@ public MigrationResult execute(@NotNull Path dataDir, @NotNull MigrationOptions } catch (Exception e) { Duration duration = Duration.between(startTime, Instant.now()); - Logger.severe("[Migration] Config migration v5->v6 failed: %s", e.getMessage()); + ErrorHandler.report("[Migration] Config migration v5->v6 failed", e); return MigrationResult.failure( id(), fromVersion(), diff --git a/src/main/java/com/hyperfactions/migration/migrations/data/DataV0ToV1Migration.java b/src/main/java/com/hyperfactions/migration/migrations/data/DataV0ToV1Migration.java index 1f6c89d..27c32a4 100644 --- a/src/main/java/com/hyperfactions/migration/migrations/data/DataV0ToV1Migration.java +++ b/src/main/java/com/hyperfactions/migration/migrations/data/DataV0ToV1Migration.java @@ -4,6 +4,7 @@ import com.hyperfactions.migration.MigrationOptions; import com.hyperfactions.migration.MigrationResult; import com.hyperfactions.migration.MigrationType; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.io.IOException; import java.nio.file.*; @@ -164,7 +165,7 @@ public MigrationResult execute(@NotNull Path dataDir, @NotNull MigrationOptions } catch (Exception e) { Duration duration = Duration.between(startTime, Instant.now()); - Logger.severe("[Migration] Data migration v0→v1 failed: %s", e.getMessage()); + ErrorHandler.report("[Migration] Data migration v0→v1 failed", e); return MigrationResult.failure( id(), fromVersion(), toVersion(), options.backupPath(), e.getMessage(), false, duration diff --git a/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java b/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java index b8fbd7c..d67cb45 100644 --- a/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java +++ b/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java @@ -14,6 +14,7 @@ import com.hyperfactions.protection.interactions.HyperFactionsHarvestCropInteraction; import com.hyperfactions.protection.interactions.HyperFactionsPlaceFluidInteraction; import com.hyperfactions.protection.interactions.HyperFactionsRefillContainerInteraction; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.server.core.event.events.BootEvent; import com.hypixel.hytale.server.core.modules.interaction.interaction.config.Interaction; @@ -178,29 +179,35 @@ protected void start() { @Override protected void shutdown() { // Stop periodic tasks - stopPeriodicTasks(); + ErrorHandler.runSafely("Shutdown: stopPeriodicTasks", this::stopPeriodicTasks); // Handle combat logout for all tagged players - for (UUID playerUuid : trackedPlayers.keySet()) { - hyperFactions.getCombatTagManager().handleDisconnect(playerUuid); - } + ErrorHandler.runSafely("Shutdown: combat logout processing", () -> { + for (UUID playerUuid : trackedPlayers.keySet()) { + hyperFactions.getCombatTagManager().handleDisconnect(playerUuid); + } + }); // Unregister all mixin hooks (HP or OG) - ProtectionMixinBridge.unregisterAllHooks(); + ErrorHandler.runSafely("Shutdown: unregister mixin hooks", ProtectionMixinBridge::unregisterAllHooks); // Clean up territory ticking system - if (eventRegistration != null) { - eventRegistration.shutdownTerritory(); - } + ErrorHandler.runSafely("Shutdown: territory system cleanup", () -> { + if (eventRegistration != null) { + eventRegistration.shutdownTerritory(); + } + }); // Clear instances instance = null; HyperFactionsAPI.setInstance(null); // Disable core - if (hyperFactions != null) { - hyperFactions.disable(); - } + ErrorHandler.runSafely("Shutdown: HyperFactions core disable", () -> { + if (hyperFactions != null) { + hyperFactions.disable(); + } + }); // Flush pending Sentry events and close SentryIntegration.close(); @@ -217,7 +224,7 @@ protected void shutdown() { private void configurePlatformCallbacks() { // Async executor hyperFactions.setAsyncExecutor(task -> { - java.util.concurrent.CompletableFuture.runAsync(task); + java.util.concurrent.CompletableFuture.runAsync(ErrorHandler.wrapTask("Async executor task", task)); }); // Task scheduler (for one-shot delayed tasks) @@ -225,12 +232,13 @@ private void configurePlatformCallbacks() { int id = taskIdCounter.incrementAndGet(); java.util.Timer timer = new java.util.Timer(); long delayMs = delayTicks * 50L; + Runnable wrapped = ErrorHandler.wrapTask("Scheduled task (delay=" + delayTicks + ")", task); timer.schedule(new java.util.TimerTask() { /** Runs the task. */ @Override public void run() { scheduledTasks.remove(id); - task.run(); + wrapped.run(); } }, delayMs); scheduledTasks.put(id, timer); @@ -243,11 +251,12 @@ public void run() { java.util.Timer timer = new java.util.Timer(); long delayMs = delayTicks * 50L; long periodMs = periodTicks * 50L; + Runnable wrapped = ErrorHandler.wrapTask("Repeating task (period=" + periodTicks + ")", task); timer.scheduleAtFixedRate(new java.util.TimerTask() { /** Runs the task. */ @Override public void run() { - task.run(); + wrapped.run(); } }, delayMs, periodMs); scheduledTasks.put(id, timer); @@ -354,7 +363,7 @@ private void startPeriodicTasks() { try { hyperFactions.getPowerManager().tickPowerRegen(); } catch (Exception e) { - Logger.severe("Error in power regen tick", e); + ErrorHandler.report("Error in power regen tick", e); } }, 60, 60, TimeUnit.SECONDS @@ -366,7 +375,7 @@ private void startPeriodicTasks() { try { hyperFactions.getCombatTagManager().tickDecay(); } catch (Exception e) { - Logger.severe("Error in combat tag tick", e); + ErrorHandler.report("Error in combat tag tick", e); } }, 1, 1, TimeUnit.SECONDS @@ -379,7 +388,7 @@ private void startPeriodicTasks() { try { hyperFactions.getClaimManager().tickClaimDecay(); } catch (Exception e) { - Logger.severe("Error in claim decay tick", e); + ErrorHandler.report("Error in claim decay tick", e); } }, 1, 1, TimeUnit.HOURS // Initial delay of 1 hour, then every hour @@ -394,7 +403,7 @@ private void startPeriodicTasks() { try { com.hyperfactions.protection.ProtectionMessageDebounce.cleanup(); } catch (Exception e) { - Logger.severe("Error in debounce cleanup tick", e); + ErrorHandler.report("Error in debounce cleanup tick", e); } }, 30, 30, TimeUnit.SECONDS diff --git a/src/main/java/com/hyperfactions/platform/PlayerConnectionHandler.java b/src/main/java/com/hyperfactions/platform/PlayerConnectionHandler.java index 8e860d3..1929235 100644 --- a/src/main/java/com/hyperfactions/platform/PlayerConnectionHandler.java +++ b/src/main/java/com/hyperfactions/platform/PlayerConnectionHandler.java @@ -3,6 +3,7 @@ import com.hyperfactions.HyperFactions; import com.hyperfactions.Permissions; import com.hyperfactions.integration.PermissionManager; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.server.core.event.events.player.PlayerChatEvent; import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; @@ -44,37 +45,39 @@ public void onPlayerConnect(PlayerConnectEvent event) { trackedPlayers.size(), uuid, trackedPlayers.containsKey(uuid)); // Cache username, track first join and last online - hyperFactions.getPlayerStorage().loadPlayerData(uuid).thenAccept(opt -> { - com.hyperfactions.data.PlayerData data = opt.orElseGet(() -> new com.hyperfactions.data.PlayerData(uuid)); - data.setUsername(username); - long now = System.currentTimeMillis(); - if (data.getFirstJoined() == 0) { - data.setFirstJoined(now); - } - data.setLastOnline(now); - hyperFactions.getPlayerStorage().savePlayerData(data); - }); + ErrorHandler.guard("Player connect: load/save player data for " + username, + hyperFactions.getPlayerStorage().loadPlayerData(uuid).thenAccept(opt -> { + com.hyperfactions.data.PlayerData data = opt.orElseGet(() -> new com.hyperfactions.data.PlayerData(uuid)); + data.setUsername(username); + long now = System.currentTimeMillis(); + if (data.getFirstJoined() == 0) { + data.setFirstJoined(now); + } + data.setLastOnline(now); + hyperFactions.getPlayerStorage().savePlayerData(data); + })); // Load player power hyperFactions.getPowerManager().playerOnline(uuid); // Restore persistent admin bypass if saved and player still has permission - hyperFactions.getPlayerStorage().loadPlayerData(uuid).thenAccept(opt -> { - if (opt.isPresent()) { - com.hyperfactions.data.PlayerData data = opt.get(); - if (data.isAdminBypassEnabled()) { - if (PermissionManager.get().hasPermission(uuid, Permissions.ADMIN)) { - hyperFactions.setAdminBypass(uuid, true); - Logger.debug("Restored admin bypass for %s", username); - } else { - // Player lost admin permission — clear the persisted flag - data.setAdminBypassEnabled(false); - hyperFactions.getPlayerStorage().savePlayerData(data); - Logger.debug("Cleared stale admin bypass for %s (no permission)", username); + ErrorHandler.guard("Player connect: restore admin bypass for " + username, + hyperFactions.getPlayerStorage().loadPlayerData(uuid).thenAccept(opt -> { + if (opt.isPresent()) { + com.hyperfactions.data.PlayerData data = opt.get(); + if (data.isAdminBypassEnabled()) { + if (PermissionManager.get().hasPermission(uuid, Permissions.ADMIN)) { + hyperFactions.setAdminBypass(uuid, true); + Logger.debug("Restored admin bypass for %s", username); + } else { + // Player lost admin permission — clear the persisted flag + data.setAdminBypassEnabled(false); + hyperFactions.getPlayerStorage().savePlayerData(data); + Logger.debug("Cleared stale admin bypass for %s (no permission)", username); + } } } - } - }); + })); // Update faction member last online hyperFactions.getFactionManager().updateLastOnline(uuid); @@ -131,13 +134,14 @@ public void onPlayerDisconnect(PlayerDisconnectEvent event) { hyperFactions.getPowerManager().playerOffline(uuid); // Update last online timestamp - hyperFactions.getPlayerStorage().loadPlayerData(uuid).thenAccept(opt -> { - if (opt.isPresent()) { - com.hyperfactions.data.PlayerData data = opt.get(); - data.setLastOnline(System.currentTimeMillis()); - hyperFactions.getPlayerStorage().savePlayerData(data); - } - }); + ErrorHandler.guard("Player disconnect: update last online for " + username, + hyperFactions.getPlayerStorage().loadPlayerData(uuid).thenAccept(opt -> { + if (opt.isPresent()) { + com.hyperfactions.data.PlayerData data = opt.get(); + data.setLastOnline(System.currentTimeMillis()); + hyperFactions.getPlayerStorage().savePlayerData(data); + } + })); // Update faction member last online hyperFactions.getFactionManager().updateLastOnline(uuid); @@ -174,18 +178,22 @@ public void onPlayerDisconnect(PlayerDisconnectEvent event) { public CompletableFuture onPlayerChatAsync( CompletableFuture futureEvent) { return futureEvent.thenApply(event -> { - if (event.isCancelled()) { - return event; - } + try { + if (event.isCancelled()) { + return event; + } - PlayerRef sender = event.getSender(); - String message = event.getContent(); + PlayerRef sender = event.getSender(); + String message = event.getContent(); - // Check if player is in faction/ally chat mode - boolean handled = hyperFactions.getChatManager().processChatMessage(sender, message); - if (handled) { - // Cancel the normal chat broadcast - event.setCancelled(true); + // Check if player is in faction/ally chat mode + boolean handled = hyperFactions.getChatManager().processChatMessage(sender, message); + if (handled) { + // Cancel the normal chat broadcast + event.setCancelled(true); + } + } catch (Exception e) { + ErrorHandler.report("Player chat: faction/ally chat processing", e); } return event; }); diff --git a/src/main/java/com/hyperfactions/platform/WorldSetup.java b/src/main/java/com/hyperfactions/platform/WorldSetup.java index f215c76..09a7d5e 100644 --- a/src/main/java/com/hyperfactions/platform/WorldSetup.java +++ b/src/main/java/com/hyperfactions/platform/WorldSetup.java @@ -3,6 +3,7 @@ import com.hyperfactions.HyperFactions; import com.hyperfactions.config.ConfigManager; import com.hyperfactions.integration.protection.OrbisGuardIntegration; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hyperfactions.worldmap.HyperFactionsWorldMapProvider; import com.hypixel.hytale.server.core.universe.Universe; @@ -200,7 +201,7 @@ public void onWorldAdd(AddWorldEvent event) { } catch (Exception e) { plugin.getLogger().at(Level.WARNING).log("Error in AddWorldEvent handler for %s: %s", world.getName(), e.getMessage()); - Logger.severe("AddWorldEvent error for %s", e, world.getName()); + ErrorHandler.report(String.format("AddWorldEvent error for %s", world.getName()), e); } } diff --git a/src/main/java/com/hyperfactions/protection/ProtectionChecker.java b/src/main/java/com/hyperfactions/protection/ProtectionChecker.java index 240a949..5fa28e6 100644 --- a/src/main/java/com/hyperfactions/protection/ProtectionChecker.java +++ b/src/main/java/com/hyperfactions/protection/ProtectionChecker.java @@ -14,6 +14,7 @@ import com.hyperfactions.integration.protection.OrbisMixinsIntegration; import com.hyperfactions.manager.*; import com.hyperfactions.util.ChunkUtil; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.util.UUID; import java.util.function.Supplier; @@ -307,8 +308,8 @@ public ProtectionResult canInteractChunk(@NotNull UUID playerUuid, @NotNull Stri return ProtectionResult.DENIED_NEUTRAL_CLAIM; } catch (Exception e) { // Fail-closed: deny on any exception to prevent unauthorized actions - Logger.severe("Protection check error (fail-closed) for player %s at %s/%d/%d type=%s", - e, playerUuid, world, chunkX, chunkZ, type); + ErrorHandler.report(String.format("Protection check error (fail-closed) for player %s at %s/%d/%d type=%s", + playerUuid, world, chunkX, chunkZ, type), e); return ProtectionResult.DENIED_NO_PERMISSION; } } @@ -814,8 +815,8 @@ private String checkMixinProtection(@NotNull UUID playerUuid, @NotNull String wo return "You cannot do that in claimed territory."; } catch (Exception e) { // Fail-closed: deny on any exception to prevent unauthorized actions - Logger.severe("Protection check error (fail-closed) for player %s at %s/%d/%d/%d type=%s", - e, playerUuid, worldName, x, y, z, factionType); + ErrorHandler.report(String.format("Protection check error (fail-closed) for player %s at %s/%d/%d/%d type=%s", + playerUuid, worldName, x, y, z, factionType), e); return "Protection error — action blocked for safety."; } } diff --git a/src/main/java/com/hyperfactions/protection/ecs/BlockBreakProtectionSystem.java b/src/main/java/com/hyperfactions/protection/ecs/BlockBreakProtectionSystem.java index 72eee07..efe859c 100644 --- a/src/main/java/com/hyperfactions/protection/ecs/BlockBreakProtectionSystem.java +++ b/src/main/java/com/hyperfactions/protection/ecs/BlockBreakProtectionSystem.java @@ -4,6 +4,7 @@ import com.hyperfactions.protection.ProtectionChecker; import com.hyperfactions.protection.ProtectionListener; import com.hyperfactions.protection.ProtectionMessageDebounce; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.component.Archetype; import com.hypixel.hytale.component.ArchetypeChunk; @@ -101,7 +102,7 @@ public void handle(int entityIndex, ArchetypeChunk chunk, } catch (Exception e) { // Fail-closed: cancel on any exception to prevent unauthorized block breaks event.setCancelled(true); - Logger.severe("Block break cancelled due to protection error (fail-closed)", e); + ErrorHandler.report("Block break cancelled due to protection error (fail-closed)", e); } } diff --git a/src/main/java/com/hyperfactions/protection/ecs/BlockPlaceProtectionSystem.java b/src/main/java/com/hyperfactions/protection/ecs/BlockPlaceProtectionSystem.java index 5b7fca4..127741f 100644 --- a/src/main/java/com/hyperfactions/protection/ecs/BlockPlaceProtectionSystem.java +++ b/src/main/java/com/hyperfactions/protection/ecs/BlockPlaceProtectionSystem.java @@ -4,6 +4,7 @@ import com.hyperfactions.protection.ProtectionChecker; import com.hyperfactions.protection.ProtectionListener; import com.hyperfactions.protection.ProtectionMessageDebounce; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.component.Archetype; import com.hypixel.hytale.component.ArchetypeChunk; @@ -95,7 +96,7 @@ public void handle(int entityIndex, ArchetypeChunk chunk, } catch (Exception e) { // Fail-closed: cancel on any exception to prevent unauthorized block placement event.setCancelled(true); - Logger.severe("Block place cancelled due to protection error (fail-closed)", e); + ErrorHandler.report("Block place cancelled due to protection error (fail-closed)", e); } } diff --git a/src/main/java/com/hyperfactions/protection/ecs/BlockUseProtectionSystem.java b/src/main/java/com/hyperfactions/protection/ecs/BlockUseProtectionSystem.java index b4fb50c..e2970e0 100644 --- a/src/main/java/com/hyperfactions/protection/ecs/BlockUseProtectionSystem.java +++ b/src/main/java/com/hyperfactions/protection/ecs/BlockUseProtectionSystem.java @@ -5,6 +5,7 @@ import com.hyperfactions.protection.ProtectionListener; import com.hyperfactions.protection.ProtectionMessageDebounce; import com.hyperfactions.protection.zone.ZoneInteractionProtection; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hyperfactions.util.MessageUtil; import com.hypixel.hytale.component.Archetype; @@ -173,7 +174,7 @@ public void handle(int entityIndex, ArchetypeChunk chunk, } catch (Exception e) { // Fail-closed: cancel on any exception to prevent unauthorized block interaction event.setCancelled(true); - Logger.severe("Block use cancelled due to protection error (fail-closed)", e); + ErrorHandler.report("Block use cancelled due to protection error (fail-closed)", e); } } diff --git a/src/main/java/com/hyperfactions/protection/ecs/DamageProtectionSystem.java b/src/main/java/com/hyperfactions/protection/ecs/DamageProtectionSystem.java index 0dd6da3..125172e 100644 --- a/src/main/java/com/hyperfactions/protection/ecs/DamageProtectionSystem.java +++ b/src/main/java/com/hyperfactions/protection/ecs/DamageProtectionSystem.java @@ -3,6 +3,7 @@ import com.hyperfactions.HyperFactions; import com.hyperfactions.protection.ProtectionListener; import com.hyperfactions.protection.damage.DamageProtectionHandler; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.component.Archetype; import com.hypixel.hytale.component.ArchetypeChunk; @@ -93,7 +94,7 @@ public void handle(int entityIndex, ArchetypeChunk chunk, } catch (Exception e) { // Fail-closed: cancel on any exception to prevent unauthorized damage event.setCancelled(true); - Logger.severe("Damage protection cancelled due to error (fail-closed)", e); + ErrorHandler.report("Damage protection cancelled due to error (fail-closed)", e); } } diff --git a/src/main/java/com/hyperfactions/protection/ecs/HarvestPickupProtectionSystem.java b/src/main/java/com/hyperfactions/protection/ecs/HarvestPickupProtectionSystem.java index 4226573..a817a5e 100644 --- a/src/main/java/com/hyperfactions/protection/ecs/HarvestPickupProtectionSystem.java +++ b/src/main/java/com/hyperfactions/protection/ecs/HarvestPickupProtectionSystem.java @@ -5,6 +5,7 @@ import com.hyperfactions.protection.ProtectionListener; import com.hyperfactions.protection.ProtectionMessageDebounce; import com.hyperfactions.protection.zone.ZoneInteractionProtection; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hyperfactions.util.MessageUtil; import com.hypixel.hytale.component.Archetype; @@ -152,7 +153,7 @@ public void handle(int entityIndex, ArchetypeChunk chunk, } catch (Exception e) { // Fail-closed: cancel on any exception to prevent unauthorized harvest pickup event.setCancelled(true); - Logger.severe("Harvest pickup cancelled due to protection error (fail-closed)", e); + ErrorHandler.report("Harvest pickup cancelled due to protection error (fail-closed)", e); } } diff --git a/src/main/java/com/hyperfactions/protection/ecs/ItemDropProtectionSystem.java b/src/main/java/com/hyperfactions/protection/ecs/ItemDropProtectionSystem.java index 885a3cf..64146e7 100644 --- a/src/main/java/com/hyperfactions/protection/ecs/ItemDropProtectionSystem.java +++ b/src/main/java/com/hyperfactions/protection/ecs/ItemDropProtectionSystem.java @@ -4,6 +4,7 @@ import com.hyperfactions.config.ConfigManager; import com.hyperfactions.protection.zone.ZoneInteractionProtection; import com.hyperfactions.util.ChunkUtil; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hyperfactions.util.MessageUtil; import com.hypixel.hytale.component.Archetype; @@ -113,7 +114,7 @@ public void handle(int entityIndex, ArchetypeChunk chunk, } catch (Exception e) { // Fail-closed: cancel on any exception to prevent unauthorized item drop event.setCancelled(true); - Logger.severe("Item drop cancelled due to protection error (fail-closed)", e); + ErrorHandler.report("Item drop cancelled due to protection error (fail-closed)", e); } } diff --git a/src/main/java/com/hyperfactions/protection/ecs/ItemPickupProtectionSystem.java b/src/main/java/com/hyperfactions/protection/ecs/ItemPickupProtectionSystem.java index 6499cbb..d1a0318 100644 --- a/src/main/java/com/hyperfactions/protection/ecs/ItemPickupProtectionSystem.java +++ b/src/main/java/com/hyperfactions/protection/ecs/ItemPickupProtectionSystem.java @@ -3,6 +3,7 @@ import com.hyperfactions.HyperFactions; import com.hyperfactions.protection.ProtectionListener; import com.hyperfactions.protection.zone.ZoneInteractionProtection; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.component.ArchetypeChunk; import com.hypixel.hytale.component.CommandBuffer; @@ -98,7 +99,7 @@ public void handle(int entityIndex, ArchetypeChunk chunk, } catch (Exception e) { // Fail-closed: cancel on any exception to prevent unauthorized item pickup event.setCancelled(true); - Logger.severe("Item pickup cancelled due to protection error (fail-closed)", e); + ErrorHandler.report("Item pickup cancelled due to protection error (fail-closed)", e); } } } diff --git a/src/main/java/com/hyperfactions/protection/ecs/PlayerDeathSystem.java b/src/main/java/com/hyperfactions/protection/ecs/PlayerDeathSystem.java index 1987420..80909b0 100644 --- a/src/main/java/com/hyperfactions/protection/ecs/PlayerDeathSystem.java +++ b/src/main/java/com/hyperfactions/protection/ecs/PlayerDeathSystem.java @@ -12,6 +12,7 @@ import com.hyperfactions.manager.CombatTagManager; import com.hyperfactions.manager.PowerManager; import com.hyperfactions.util.ChunkUtil; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.component.CommandBuffer; import com.hypixel.hytale.component.ComponentType; @@ -190,7 +191,7 @@ public void onComponentAdded(@NotNull Ref ref, // Death location announcement for gravestone integration announceDeathLocation(victimUuid, playerRef, store, commandBuffer, ref); } catch (Exception e) { - Logger.severe("Error handling player death in ECS system", e); + ErrorHandler.report("Error handling player death in ECS system", e); } } diff --git a/src/main/java/com/hyperfactions/protection/ecs/PlayerRespawnSystem.java b/src/main/java/com/hyperfactions/protection/ecs/PlayerRespawnSystem.java index ffcd6e0..dee936a 100644 --- a/src/main/java/com/hyperfactions/protection/ecs/PlayerRespawnSystem.java +++ b/src/main/java/com/hyperfactions/protection/ecs/PlayerRespawnSystem.java @@ -3,6 +3,7 @@ import com.hyperfactions.HyperFactions; import com.hyperfactions.config.ConfigManager; import com.hyperfactions.util.ChunkUtil; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.component.CommandBuffer; import com.hypixel.hytale.component.ComponentType; @@ -120,7 +121,7 @@ public void onComponentRemoved(@NotNull Ref ref, playerRef.getUuid()); } } catch (Exception e) { - Logger.severe("Error handling player respawn in ECS system", e); + ErrorHandler.report("Error handling player respawn in ECS system", e); } } } diff --git a/src/main/java/com/hyperfactions/protection/ecs/TeleportCancelOnDamageSystem.java b/src/main/java/com/hyperfactions/protection/ecs/TeleportCancelOnDamageSystem.java index a972cda..16f3dbe 100644 --- a/src/main/java/com/hyperfactions/protection/ecs/TeleportCancelOnDamageSystem.java +++ b/src/main/java/com/hyperfactions/protection/ecs/TeleportCancelOnDamageSystem.java @@ -1,6 +1,7 @@ package com.hyperfactions.protection.ecs; import com.hyperfactions.HyperFactions; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.component.Archetype; import com.hypixel.hytale.component.ArchetypeChunk; @@ -56,7 +57,7 @@ public void handle(int entityIndex, ArchetypeChunk chunk, player::sendMessage ); } catch (Exception e) { - Logger.severe("Error processing damage event for teleport cancellation", e); + ErrorHandler.report("Error processing damage event for teleport cancellation", e); } } } diff --git a/src/main/java/com/hyperfactions/storage/JsonEconomyStorage.java b/src/main/java/com/hyperfactions/storage/JsonEconomyStorage.java index 23c77e9..481bbcf 100644 --- a/src/main/java/com/hyperfactions/storage/JsonEconomyStorage.java +++ b/src/main/java/com/hyperfactions/storage/JsonEconomyStorage.java @@ -4,6 +4,7 @@ import com.hyperfactions.api.EconomyAPI; import com.hyperfactions.data.FactionEconomy.TreasuryLimits; import com.hyperfactions.data.FactionEconomy; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.io.IOException; import java.math.BigDecimal; @@ -44,7 +45,7 @@ public CompletableFuture init() { StorageUtils.cleanupOrphanedFiles(economyDir); Logger.info("[Storage] Economy storage initialized at %s", economyDir); } catch (IOException e) { - Logger.severe("Failed to create economy directory", e); + ErrorHandler.report("Failed to create economy directory", e); } }); } @@ -71,12 +72,11 @@ public CompletableFuture> loadAll() { JsonObject obj = JsonParser.parseString(json).getAsJsonObject(); economies.put(factionId, deserialize(obj)); } catch (Exception e) { - Logger.severe("Failed to load economy file %s: %s", - file.getFileName(), e.getMessage()); + ErrorHandler.report(String.format("Failed to load economy file %s", file.getFileName()), e); } } } catch (IOException e) { - Logger.severe("Failed to read economy directory", e); + ErrorHandler.report("Failed to read economy directory", e); } Logger.info("[Storage] Loaded economy data for %d factions", economies.size()); @@ -97,7 +97,7 @@ public CompletableFuture save(@NotNull UUID factionId, @NotNull FactionEco String content = gson.toJson(obj); StorageUtils.WriteResult result = StorageUtils.writeAtomic(file, content); if (result instanceof StorageUtils.WriteResult.Failure failure) { - Logger.severe("Failed to save economy for faction %s: %s", factionId, failure.error()); + ErrorHandler.report(String.format("Failed to save economy for faction %s: %s", factionId, failure.error()), failure.cause()); } }); } @@ -115,8 +115,7 @@ public CompletableFuture saveAll(@NotNull Map econom String content = gson.toJson(obj); StorageUtils.WriteResult result = StorageUtils.writeAtomic(file, content); if (result instanceof StorageUtils.WriteResult.Failure failure) { - Logger.severe("Failed to save economy for faction %s: %s", - entry.getKey(), failure.error()); + ErrorHandler.report(String.format("Failed to save economy for faction %s: %s", entry.getKey(), failure.error()), failure.cause()); } } Logger.debug("Saved economy data for %d factions", economies.size()); diff --git a/src/main/java/com/hyperfactions/storage/StorageUtils.java b/src/main/java/com/hyperfactions/storage/StorageUtils.java index fbd2393..29804a2 100644 --- a/src/main/java/com/hyperfactions/storage/StorageUtils.java +++ b/src/main/java/com/hyperfactions/storage/StorageUtils.java @@ -1,5 +1,6 @@ package com.hyperfactions.storage; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.io.IOException; import java.nio.file.Files; @@ -134,7 +135,7 @@ public static WriteResult writeAtomic(@NotNull Path targetFile, @NotNull String } catch (IOException ignored) {} String error = "I/O error during atomic write: " + e.getMessage(); - Logger.severe("[Storage] %s for %s", error, targetFile); + ErrorHandler.report(String.format("[Storage] %s for %s", error, targetFile), e); return new WriteResult.Failure(targetFile, error, e); } catch (Exception e) { @@ -144,7 +145,7 @@ public static WriteResult writeAtomic(@NotNull Path targetFile, @NotNull String } catch (IOException ignored) {} String error = "Unexpected error during atomic write: " + e.getMessage(); - Logger.severe("[Storage] %s for %s", error, targetFile); + ErrorHandler.report(String.format("[Storage] %s for %s", error, targetFile), e); return new WriteResult.Failure(targetFile, error, e); } } @@ -248,7 +249,7 @@ public static boolean recoverFromBackup(@NotNull Path targetFile) { return true; } catch (IOException e) { - Logger.severe("[Storage] Failed to recover %s from backup: %s", targetFile, e.getMessage()); + ErrorHandler.report(String.format("[Storage] Failed to recover %s from backup", targetFile), e); return false; } } diff --git a/src/main/java/com/hyperfactions/storage/json/JsonChatHistoryStorage.java b/src/main/java/com/hyperfactions/storage/json/JsonChatHistoryStorage.java index 3c9ae55..e5a4a5e 100644 --- a/src/main/java/com/hyperfactions/storage/json/JsonChatHistoryStorage.java +++ b/src/main/java/com/hyperfactions/storage/json/JsonChatHistoryStorage.java @@ -11,6 +11,7 @@ import com.hyperfactions.storage.ChatHistoryStorage; import com.hyperfactions.storage.StorageHealth; import com.hyperfactions.storage.StorageUtils; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hyperfactions.util.UuidUtil; import java.io.IOException; @@ -51,7 +52,7 @@ public CompletableFuture init() { StorageUtils.cleanupOrphanedFiles(chatDir); Logger.info("[Storage] Chat history storage initialized at %s", chatDir); } catch (IOException e) { - Logger.severe("Failed to create chat history directory", e); + ErrorHandler.report("Failed to create chat history directory", e); } }); } @@ -83,14 +84,14 @@ public CompletableFuture loadHistory(@NotNull UUID factionId JsonObject obj = JsonParser.parseString(json).getAsJsonObject(); return deserializeHistory(factionId, obj); } catch (Exception e) { - Logger.severe("Failed to load chat history for %s, attempting backup recovery", e, factionId); + ErrorHandler.report(String.format("Failed to load chat history for %s, attempting backup recovery", factionId), e); if (StorageUtils.recoverFromBackup(file)) { try { String json = Files.readString(file); JsonObject obj = JsonParser.parseString(json).getAsJsonObject(); return deserializeHistory(factionId, obj); } catch (Exception e2) { - Logger.severe("Backup recovery failed for chat history %s", e2, factionId); + ErrorHandler.report(String.format("Backup recovery failed for chat history %s", factionId), e2); } } return FactionChatHistory.empty(factionId); @@ -116,11 +117,11 @@ public CompletableFuture saveHistory(@NotNull FactionChatHistory history) Logger.debug("Saved chat history for %s (%d messages)", history.factionId(), history.size()); } else if (result instanceof StorageUtils.WriteResult.Failure failure) { StorageHealth.get().recordFailure(filePath, failure.error()); - Logger.severe("Failed to save chat history for %s: %s", history.factionId(), failure.error()); + ErrorHandler.report(String.format("Failed to save chat history for %s: %s", history.factionId(), failure.error()), failure.cause()); } } catch (Exception e) { StorageHealth.get().recordFailure(filePath, e.getMessage()); - Logger.severe("Failed to save chat history for %s", e, history.factionId()); + ErrorHandler.report(String.format("Failed to save chat history for %s", history.factionId()), e); } }); } @@ -156,7 +157,7 @@ public CompletableFuture> listAllFactionIds() { } } } catch (IOException e) { - Logger.severe("Failed to list chat history files", e); + ErrorHandler.report("Failed to list chat history files", e); } return ids; diff --git a/src/main/java/com/hyperfactions/storage/json/JsonFactionStorage.java b/src/main/java/com/hyperfactions/storage/json/JsonFactionStorage.java index 48d090f..3aeeb64 100644 --- a/src/main/java/com/hyperfactions/storage/json/JsonFactionStorage.java +++ b/src/main/java/com/hyperfactions/storage/json/JsonFactionStorage.java @@ -10,6 +10,7 @@ import com.hyperfactions.storage.FactionStorage; import com.hyperfactions.storage.StorageHealth; import com.hyperfactions.storage.StorageUtils; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.io.IOException; import java.nio.file.DirectoryStream; @@ -51,7 +52,7 @@ public CompletableFuture init() { StorageUtils.cleanupOrphanedFiles(factionsDir); Logger.info("[Storage] Faction storage initialized at %s", factionsDir); } catch (IOException e) { - Logger.severe("Failed to create factions directory", e); + ErrorHandler.report("Failed to create factions directory", e); } }); } @@ -86,7 +87,7 @@ public CompletableFuture> loadFaction(@NotNull UUID factionId) JsonObject obj = JsonParser.parseString(json).getAsJsonObject(); return Optional.of(deserializeFaction(obj)); } catch (Exception e) { - Logger.severe("Failed to load faction %s, attempting backup recovery", e, factionId); + ErrorHandler.report(String.format("Failed to load faction %s, attempting backup recovery", factionId), e); // Attempt backup recovery on parse failure if (StorageUtils.recoverFromBackup(file)) { try { @@ -95,7 +96,7 @@ public CompletableFuture> loadFaction(@NotNull UUID factionId) Logger.info("[Storage] Successfully loaded faction %s from recovered backup", factionId); return Optional.of(deserializeFaction(obj)); } catch (Exception e2) { - Logger.severe("Backup recovery failed for faction %s", e2, factionId); + ErrorHandler.report(String.format("Backup recovery failed for faction %s", factionId), e2); } } return Optional.empty(); @@ -122,11 +123,11 @@ public CompletableFuture saveFaction(@NotNull Faction faction) { Logger.debug("Saved faction %s (checksum: %s)", faction.name(), success.checksum().substring(0, 8)); } else if (result instanceof StorageUtils.WriteResult.Failure failure) { StorageHealth.get().recordFailure(filePath, failure.error()); - Logger.severe("Failed to save faction %s: %s", faction.name(), failure.error()); + ErrorHandler.report(String.format("Failed to save faction %s: %s", faction.name(), failure.error()), failure.cause()); } } catch (Exception e) { StorageHealth.get().recordFailure(filePath, e.getMessage()); - Logger.severe("Failed to save faction %s", e, faction.name()); + ErrorHandler.report(String.format("Failed to save faction %s", faction.name()), e); } }); } @@ -163,13 +164,13 @@ public CompletableFuture> loadAllFactions() { factions.add(deserializeFaction(obj)); } catch (Exception e) { failedFiles.add(file.getFileName().toString()); - Logger.severe("Failed to load faction file %s: %s", file.getFileName(), e.getMessage()); + ErrorHandler.report(String.format("Failed to load faction file %s", file.getFileName()), e); // Log full stack trace for debugging Logger.debug("Stack trace for %s: %s", file.getFileName(), e.toString()); } } } catch (IOException e) { - Logger.severe("CRITICAL: Failed to read factions directory - data may be lost!", e); + ErrorHandler.report("CRITICAL: Failed to read factions directory - data may be lost!", e); throw new RuntimeException("Failed to read factions directory", e); } diff --git a/src/main/java/com/hyperfactions/storage/json/JsonPlayerStorage.java b/src/main/java/com/hyperfactions/storage/json/JsonPlayerStorage.java index 3997669..43fe0e6 100644 --- a/src/main/java/com/hyperfactions/storage/json/JsonPlayerStorage.java +++ b/src/main/java/com/hyperfactions/storage/json/JsonPlayerStorage.java @@ -12,6 +12,7 @@ import com.hyperfactions.storage.PlayerStorage; import com.hyperfactions.storage.StorageHealth; import com.hyperfactions.storage.StorageUtils; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hyperfactions.util.UuidUtil; import java.io.IOException; @@ -63,7 +64,7 @@ public CompletableFuture init() { StorageUtils.cleanupOrphanedFiles(playersDir); Logger.info("[Storage] Player storage initialized at %s", playersDir); } catch (IOException e) { - Logger.severe("Failed to create players directory", e); + ErrorHandler.report("Failed to create players directory", e); } }); } @@ -98,7 +99,7 @@ public CompletableFuture savePlayerPower(@NotNull PlayerPower power) { } catch (Exception e) { Path file = playersDir.resolve(power.uuid() + ".json"); StorageHealth.get().recordFailure(file.toString(), e.getMessage()); - Logger.severe("Failed to save player power %s", e, power.uuid()); + ErrorHandler.report(String.format("Failed to save player power %s", power.uuid()), e); } finally { lock.unlock(); } @@ -137,11 +138,11 @@ public CompletableFuture> loadAllPlayerPower() { powers.add(deserializePlayerData(obj).toPower()); } catch (Exception e) { failedFiles.add(file.getFileName().toString()); - Logger.severe("Failed to load player file %s: %s", file.getFileName(), e.getMessage()); + ErrorHandler.report(String.format("Failed to load player file %s", file.getFileName()), e); } } } catch (IOException e) { - Logger.severe("CRITICAL: Failed to read players directory - data may be lost!", e); + ErrorHandler.report("CRITICAL: Failed to read players directory - data may be lost!", e); throw new RuntimeException("Failed to read players directory", e); } @@ -176,7 +177,7 @@ public CompletableFuture> getAllPlayerUuids() { // Skip non-UUID filenames } } catch (IOException e) { - Logger.severe("Failed to list player files: %s", e.getMessage()); + ErrorHandler.report("Failed to list player files", e); } return uuids; }); @@ -242,11 +243,11 @@ private void savePlayerDataSync(@NotNull PlayerData data) { StorageHealth.get().recordSuccess(filePath); } else if (result instanceof StorageUtils.WriteResult.Failure failure) { StorageHealth.get().recordFailure(filePath, failure.error()); - Logger.severe("Failed to save player data %s: %s", data.getUuid(), failure.error()); + ErrorHandler.report(String.format("Failed to save player data %s: %s", data.getUuid(), failure.error()), failure.cause()); } } catch (Exception e) { StorageHealth.get().recordFailure(filePath, e.getMessage()); - Logger.severe("Failed to save player data %s", e, data.getUuid()); + ErrorHandler.report(String.format("Failed to save player data %s", data.getUuid()), e); } } @@ -272,7 +273,7 @@ private PlayerData loadPlayerDataSync(@NotNull UUID uuid) { JsonObject obj = JsonParser.parseString(json).getAsJsonObject(); return deserializePlayerData(obj); } catch (Exception e) { - Logger.severe("Failed to load player data %s, attempting backup recovery", e, uuid); + ErrorHandler.report(String.format("Failed to load player data %s, attempting backup recovery", uuid), e); if (StorageUtils.recoverFromBackup(file)) { try { String json = Files.readString(file); @@ -280,7 +281,7 @@ private PlayerData loadPlayerDataSync(@NotNull UUID uuid) { Logger.info("[Storage] Successfully loaded player data %s from recovered backup", uuid); return deserializePlayerData(obj); } catch (Exception e2) { - Logger.severe("Backup recovery failed for player data %s", e2, uuid); + ErrorHandler.report(String.format("Backup recovery failed for player data %s", uuid), e2); } } return null; diff --git a/src/main/java/com/hyperfactions/storage/json/JsonZoneStorage.java b/src/main/java/com/hyperfactions/storage/json/JsonZoneStorage.java index 53e8de3..47692a3 100644 --- a/src/main/java/com/hyperfactions/storage/json/JsonZoneStorage.java +++ b/src/main/java/com/hyperfactions/storage/json/JsonZoneStorage.java @@ -13,6 +13,7 @@ import com.hyperfactions.storage.StorageHealth; import com.hyperfactions.storage.StorageUtils; import com.hyperfactions.storage.ZoneStorage; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.io.IOException; import java.nio.file.Files; @@ -55,7 +56,7 @@ public CompletableFuture init() { StorageUtils.cleanupOrphanedFiles(dataDir); Logger.info("[Storage] Zone storage initialized"); } catch (IOException e) { - Logger.severe("Failed to create data directory", e); + ErrorHandler.report("Failed to create data directory", e); } }); } @@ -106,7 +107,7 @@ public CompletableFuture> loadAllZones() { } } catch (Exception ignored) {} failedZones.add(zoneName); - Logger.severe("Failed to parse zone '%s': %s", zoneName, e.getMessage()); + ErrorHandler.report(String.format("Failed to parse zone '%s'", zoneName), e); } } @@ -126,7 +127,7 @@ public CompletableFuture> loadAllZones() { } } catch (Exception e) { - Logger.severe("CRITICAL: Failed to load zones file, attempting backup recovery", e); + ErrorHandler.report("CRITICAL: Failed to load zones file, attempting backup recovery", e); // Attempt backup recovery on parse failure if (StorageUtils.recoverFromBackup(zonesFile)) { try { @@ -140,7 +141,7 @@ public CompletableFuture> loadAllZones() { Logger.info("[Storage] Successfully loaded %d zones from recovered backup", zones.size()); return zones; } catch (Exception e2) { - Logger.severe("Backup recovery failed for zones file", e2); + ErrorHandler.report("Backup recovery failed for zones file", e2); } } throw new RuntimeException("Failed to load zones file", e); @@ -176,7 +177,7 @@ public CompletableFuture saveAllZones(@NotNull Collection zones) { } } catch (Exception e) { String error = "Pre-write validation failed: generated JSON is not valid: " + e.getMessage(); - Logger.severe("[ZoneStorage] %s", error); + ErrorHandler.report(String.format("[ZoneStorage] %s", error), e); StorageHealth.get().recordFailure(filePath, error); return; } @@ -189,11 +190,11 @@ public CompletableFuture saveAllZones(@NotNull Collection zones) { Logger.debug("Saved %d zones (checksum: %s)", zones.size(), success.checksum().substring(0, 8)); } else if (result instanceof StorageUtils.WriteResult.Failure failure) { StorageHealth.get().recordFailure(filePath, failure.error()); - Logger.severe("Failed to save zones: %s", failure.error()); + ErrorHandler.report(String.format("Failed to save zones: %s", failure.error()), failure.cause()); } } catch (Exception e) { StorageHealth.get().recordFailure(filePath, e.getMessage()); - Logger.severe("Failed to save zones", e); + ErrorHandler.report("Failed to save zones", e); } }); } diff --git a/src/main/java/com/hyperfactions/update/UpdateChecker.java b/src/main/java/com/hyperfactions/update/UpdateChecker.java index 95dfb9d..1608cb0 100644 --- a/src/main/java/com/hyperfactions/update/UpdateChecker.java +++ b/src/main/java/com/hyperfactions/update/UpdateChecker.java @@ -3,6 +3,7 @@ import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import java.io.*; import java.net.HttpURLConnection; @@ -622,7 +623,7 @@ public RollbackResult performRollback() { return new RollbackResult(true, backupVersion, info != null ? info.toVersion() : null, null); } catch (IOException e) { - Logger.severe("[Update:%s] Rollback failed: %s", artifactName, e.getMessage()); + ErrorHandler.report(String.format("[Update:%s] Rollback failed", artifactName), e); return new RollbackResult(false, backupVersion, null, "Failed to rollback: " + e.getMessage()); } } diff --git a/src/main/java/com/hyperfactions/util/ErrorHandler.java b/src/main/java/com/hyperfactions/util/ErrorHandler.java new file mode 100644 index 0000000..6b55ade --- /dev/null +++ b/src/main/java/com/hyperfactions/util/ErrorHandler.java @@ -0,0 +1,119 @@ +package com.hyperfactions.util; + +import com.hyperfactions.integration.SentryIntegration; +import io.sentry.SentryLevel; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Centralized error handling utility that logs to console and reports to Sentry. + * + *

All methods are safe to call regardless of whether Sentry is initialized — + * {@link SentryIntegration#captureException(String, Throwable)} handles both + * console logging and Sentry reporting internally. + */ +public final class ErrorHandler { + + private ErrorHandler() {} + + /** + * Reports an exception with context. Use in existing catch blocks + * that currently only call {@code Logger.severe()}. + * + * @param context description of what was happening when the error occurred + * @param throwable the exception + */ + public static void report(@NotNull String context, @NotNull Throwable throwable) { + SentryIntegration.captureException(context, throwable); + } + + /** + * Reports a failure that may or may not have an exception cause. + * If a cause is present, reports it as an exception to Sentry. + * If no cause, sends a message event so the failure is still visible in Sentry. + * Always logs to console via {@code Logger.severe()}. + * + * @param context description of what failed + * @param cause the exception, or null if only a string error is available + */ + public static void report(@NotNull String context, @Nullable Exception cause) { + if (cause != null) { + SentryIntegration.captureException(context, cause); + } else { + Logger.severe(context); + SentryIntegration.captureMessage(context, SentryLevel.ERROR); + } + } + + /** + * Wraps a {@link Runnable} so that any exception is caught, logged, and reported. + * Use for scheduled/timer tasks to prevent exceptions from killing scheduler threads. + * + * @param context description of the task for error reports + * @param task the runnable to wrap + * @return a wrapped runnable that never throws + */ + @NotNull + public static Runnable wrapTask(@NotNull String context, @NotNull Runnable task) { + return () -> { + try { + task.run(); + } catch (Exception e) { + SentryIntegration.captureException(context, e); + } + }; + } + + /** + * Adds exception handling to a {@link CompletableFuture} chain. + * Logs and reports any exception, then returns {@code null} to prevent propagation. + * + * @param context description of the async operation + * @param future the future to guard + * @param the future's result type + * @return the same future with an {@code exceptionally} handler attached + */ + @NotNull + public static CompletableFuture guard(@NotNull String context, @NotNull CompletableFuture future) { + return future.exceptionally(e -> { + SentryIntegration.captureException(context, e); + return null; + }); + } + + /** + * Runs an action in isolation, catching and reporting any exception. + * Use for shutdown/cleanup sequences where each step should run independently. + * + * @param context description of the action + * @param action the action to run + */ + public static void runSafely(@NotNull String context, @NotNull Runnable action) { + try { + action.run(); + } catch (Exception e) { + SentryIntegration.captureException(context, e); + } + } + + /** + * Runs a supplier in isolation, returning a default value on failure. + * Use for operations that need a fallback value when they fail. + * + * @param context description of the operation + * @param defaultValue the value to return on failure + * @param supplier the supplier to run + * @param the return type + * @return the supplier's result, or {@code defaultValue} on exception + */ + public static T runSafely(@NotNull String context, T defaultValue, @NotNull Supplier supplier) { + try { + return supplier.get(); + } catch (Exception e) { + SentryIntegration.captureException(context, e); + return defaultValue; + } + } +} diff --git a/src/main/java/com/hyperfactions/worldmap/WorldMapRefreshScheduler.java b/src/main/java/com/hyperfactions/worldmap/WorldMapRefreshScheduler.java index b64586f..7b3d886 100644 --- a/src/main/java/com/hyperfactions/worldmap/WorldMapRefreshScheduler.java +++ b/src/main/java/com/hyperfactions/worldmap/WorldMapRefreshScheduler.java @@ -4,6 +4,7 @@ import com.hyperfactions.config.modules.WorldMapConfig; import com.hyperfactions.data.ChunkKey; import com.hyperfactions.util.ChunkUtil; +import com.hyperfactions.util.ErrorHandler; import com.hyperfactions.util.Logger; import com.hypixel.hytale.math.vector.Vector3d; import com.hypixel.hytale.server.core.entity.entities.Player; @@ -105,7 +106,7 @@ public void start() { long intervalMs = (intervalTicks * 1000L) / 30; batchTask = scheduler.scheduleAtFixedRate( - this::processBatch, + ErrorHandler.wrapTask("World map batch processing", this::processBatch), intervalMs, intervalMs, TimeUnit.MILLISECONDS @@ -433,7 +434,7 @@ private void scheduleDebounceRefresh() { int delaySeconds = config.getDebouncedDelaySeconds(); - debounceTask = scheduler.schedule(() -> { + debounceTask = scheduler.schedule(ErrorHandler.wrapTask("World map debounce refresh", () -> { // Check if enough time has passed since last change long elapsed = System.currentTimeMillis() - lastChangeTime; if (elapsed >= delaySeconds * 1000L) { @@ -443,7 +444,7 @@ private void scheduleDebounceRefresh() { // Reschedule scheduleDebounceRefresh(); } - }, delaySeconds, TimeUnit.SECONDS); + }), delaySeconds, TimeUnit.SECONDS); } /** From 57e1b544ad10fc20c7bffaf6e1f1df5e1ba73e8a Mon Sep 17 00:00:00 2001 From: derrickmehaffy Date: Sun, 1 Mar 2026 17:50:32 -0800 Subject: [PATCH 10/10] refactor: merge SentryConfig into DebugConfig, add sentry admin command Move sentry configuration from standalone config/sentry.json into a nested "sentry" section within config/debug.json. Auto-migration reads existing sentry.json values on first load and deletes the old file. Add /f admin sentry command for server owners to view status, opt out, or opt back in to error reporting at runtime without editing config files. Remove SentryConfig class, update ConfigManager, SentryIntegration, and all callers to use DebugConfig for sentry settings. --- CHANGELOG.md | 4 +- .../command/admin/AdminSubCommand.java | 53 ++++++- .../hyperfactions/config/ConfigManager.java | 22 --- .../config/modules/DebugConfig.java | 144 ++++++++++++++++++ .../config/modules/SentryConfig.java | 108 ------------- .../integration/SentryIntegration.java | 20 +-- .../platform/HyperFactionsPlugin.java | 2 +- 7 files changed, 210 insertions(+), 143 deletions(-) delete mode 100644 src/main/java/com/hyperfactions/config/modules/SentryConfig.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e047e99..6264152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Sentry SDK (v8.33.0) bundled for automatic error reporting to Sentry dashboard - Non-blocking async event delivery — Sentry never impacts server performance - All Sentry operations wrapped in try/catch — failures never crash the server -- New config module `config/sentry.json` with settings: enabled, dsn, environment, debug, tracesSampleRate +- Sentry config nested under `config/debug.json` → `"sentry"` section (no separate file) +- Auto-migration: existing `config/sentry.json` values are read into `debug.json` on first load, old file deleted - DSN pre-configured with default — works out of the box - Source context upload via Sentry Gradle plugin (stack traces show source code in Sentry) - HyperFactions frames highlighted in stack traces via `addInAppInclude` +- New admin command: `/f admin sentry` — view status, enable/disable error reporting at runtime - New admin command: `/f admin sentrytest` — sends a test error with stack trace to verify integration - Sentry cleanly flushes pending events on server shutdown (2s timeout) - Auth token stored in `.sentry-auth-token` file (gitignored) with env var fallback diff --git a/src/main/java/com/hyperfactions/command/admin/AdminSubCommand.java b/src/main/java/com/hyperfactions/command/admin/AdminSubCommand.java index 2de569d..7cc070d 100644 --- a/src/main/java/com/hyperfactions/command/admin/AdminSubCommand.java +++ b/src/main/java/com/hyperfactions/command/admin/AdminSubCommand.java @@ -4,6 +4,7 @@ import com.hyperfactions.Permissions; import com.hyperfactions.command.admin.handler.AdminBackupHandler; import com.hyperfactions.command.admin.handler.AdminDebugHandler; +import com.hyperfactions.config.ConfigManager; import com.hyperfactions.integration.SentryIntegration; import com.hyperfactions.command.admin.handler.AdminEconomyHandler; import com.hyperfactions.command.admin.handler.AdminImportHandler; @@ -285,6 +286,7 @@ private void dispatchCommand(@NotNull CommandContext ctx, @Nullable Store economyHandler.handleAdminEconomy(ctx, player, senderUuid, subArgs); case "world", "worlds" -> worldHandler.handleAdminWorld(ctx, player, subArgs); case "version" -> handleVersion(ctx, store, ref, player, isPlayer); + case "sentry" -> handleSentry(ctx, subArgs); case "sentrytest" -> handleSentryTest(ctx); case "log", "logs", "activitylog" -> { if (!requirePlayer(ctx, isPlayer)) { @@ -377,6 +379,9 @@ private void showAdminHelp(CommandContext ctx) { commands.add(new CommandHelp("/f admin log", "View global activity log")); commands.add(new CommandHelp("/f admin world", "Per-world settings management")); commands.add(new CommandHelp("/f admin version", "View mod version and integration status")); + commands.add(new CommandHelp("/f admin sentry", "View Sentry status")); + commands.add(new CommandHelp("/f admin sentry disable", "Opt out of Sentry error reporting")); + commands.add(new CommandHelp("/f admin sentry enable", "Opt in to Sentry error reporting")); commands.add(new CommandHelp("/f admin sentrytest", "Send a test error to Sentry")); ctx.sendMessage(HelpFormatter.buildHelp("Admin Commands", "Server administration", commands, null)); } @@ -405,10 +410,56 @@ private void handleVersion(CommandContext ctx, @Nullable Store stor } } + // === Sentry === + private void handleSentry(CommandContext ctx, String[] args) { + var debugConfig = ConfigManager.get().debug(); + + if (args.length == 0) { + // Show status + boolean configEnabled = debugConfig.isSentryEnabled(); + boolean running = SentryIntegration.isInitialized(); + ctx.sendMessage(prefix().insert(msg("Sentry Error Reporting", COLOR_CYAN))); + ctx.sendMessage(msg(" Config: " + (configEnabled ? "enabled" : "disabled"), + configEnabled ? COLOR_GREEN : COLOR_GRAY)); + ctx.sendMessage(msg(" Status: " + (running ? "active" : "inactive"), + running ? COLOR_GREEN : COLOR_GRAY)); + ctx.sendMessage(msg(" DSN: " + debugConfig.getSentryDsn(), COLOR_GRAY)); + ctx.sendMessage(msg(" Environment: " + debugConfig.getSentryEnvironment(), COLOR_GRAY)); + return; + } + + switch (args[0].toLowerCase()) { + case "disable", "optout", "off" -> { + if (!debugConfig.isSentryEnabled()) { + ctx.sendMessage(prefix().insert(msg("Sentry is already disabled.", COLOR_YELLOW))); + return; + } + debugConfig.setSentryEnabled(false); + debugConfig.save(); + SentryIntegration.close(); + ctx.sendMessage(prefix().insert(msg("Sentry disabled and config saved. Error reporting is now off.", COLOR_GREEN))); + } + case "enable", "optin", "on" -> { + if (debugConfig.isSentryEnabled()) { + ctx.sendMessage(prefix().insert(msg("Sentry is already enabled.", COLOR_YELLOW))); + return; + } + debugConfig.setSentryEnabled(true); + debugConfig.save(); + // Try to initialize now if not already running + if (!SentryIntegration.isInitialized()) { + SentryIntegration.init(debugConfig); + } + ctx.sendMessage(prefix().insert(msg("Sentry enabled and config saved. Error reporting is now on.", COLOR_GREEN))); + } + default -> ctx.sendMessage(prefix().insert(msg("Usage: /f admin sentry [disable|enable]", COLOR_RED))); + } + } + // === Sentry Test === private void handleSentryTest(CommandContext ctx) { if (!SentryIntegration.isInitialized()) { - ctx.sendMessage(prefix().insert(msg("Sentry is not initialized. Check config/sentry.json", COLOR_RED))); + ctx.sendMessage(prefix().insert(msg("Sentry is not initialized. Check config/debug.json", COLOR_RED))); return; } diff --git a/src/main/java/com/hyperfactions/config/ConfigManager.java b/src/main/java/com/hyperfactions/config/ConfigManager.java index 13fbba6..1df50f5 100644 --- a/src/main/java/com/hyperfactions/config/ConfigManager.java +++ b/src/main/java/com/hyperfactions/config/ConfigManager.java @@ -49,8 +49,6 @@ public class ConfigManager { private WorldsConfig worldsConfig; - private SentryConfig sentryConfig; - private final WorldSettingsResolver worldSettingsResolver = new WorldSettingsResolver(); private ConfigManager() {} @@ -135,9 +133,6 @@ public void loadAll(@NotNull Path dataDir) { worldsConfig = new WorldsConfig(configDir.resolve("worlds.json")); worldsConfig.load(); - sentryConfig = new SentryConfig(configDir.resolve("sentry.json")); - sentryConfig.load(); - // Build the world settings resolver from loaded config worldSettingsResolver.rebuild(worldsConfig); @@ -213,11 +208,6 @@ private void validateAll() { combined.merge(worldsConfig.getLastValidationResult()); } - sentryConfig.validateAndLog(); - if (sentryConfig.getLastValidationResult() != null) { - combined.merge(sentryConfig.getLastValidationResult()); - } - // Log summary if (combined.hasIssues()) { int warnings = combined.getWarnings().size(); @@ -263,7 +253,6 @@ public void reloadAll() { announcementConfig.reload(); gravestoneConfig.reload(); worldsConfig.reload(); - sentryConfig.reload(); // Rebuild world settings resolver worldSettingsResolver.rebuild(worldsConfig); @@ -289,7 +278,6 @@ public void saveAll() { announcementConfig.save(); gravestoneConfig.save(); worldsConfig.save(); - sentryConfig.save(); } // === Config Accessors === @@ -416,16 +404,6 @@ public WorldsConfig worlds() { return worldsConfig; } - /** - * Gets the sentry module configuration. - * - * @return sentry config - */ - @NotNull - public SentryConfig sentry() { - return sentryConfig; - } - /** * Gets the world settings resolver for per-world behavior queries. * diff --git a/src/main/java/com/hyperfactions/config/modules/DebugConfig.java b/src/main/java/com/hyperfactions/config/modules/DebugConfig.java index d8e1026..5ea5891 100644 --- a/src/main/java/com/hyperfactions/config/modules/DebugConfig.java +++ b/src/main/java/com/hyperfactions/config/modules/DebugConfig.java @@ -1,8 +1,10 @@ package com.hyperfactions.config.modules; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.hyperfactions.config.ModuleConfig; import com.hyperfactions.util.Logger; +import java.nio.file.Files; import java.nio.file.Path; import org.jetbrains.annotations.NotNull; @@ -42,6 +44,20 @@ public class DebugConfig extends ModuleConfig { private boolean economy = false; + // Sentry error tracking settings + private static final String DEFAULT_SENTRY_DSN = + "https://cc41f97749e8b8b1562defea6ba3de9c@o4510966614589440.ingest.us.sentry.io/4510966616162304"; + + private boolean sentryEnabled = true; + + private String sentryDsn = DEFAULT_SENTRY_DSN; + + private String sentryEnvironment = "production"; + + private boolean sentryDebug = false; + + private double sentryTracesSampleRate = 0.0; + /** * Creates a new debug config. * @@ -82,6 +98,11 @@ protected void createDefaults() { spawning = false; integration = false; economy = false; + sentryEnabled = true; + sentryDsn = DEFAULT_SENTRY_DSN; + sentryEnvironment = "production"; + sentryDebug = false; + sentryTracesSampleRate = 0.0; } /** Loads module settings. */ @@ -107,6 +128,19 @@ protected void loadModuleSettings(@NotNull JsonObject root) { economy = getBool(categories, "economy", false); } + // Load sentry settings (nested object) + if (hasSection(root, "sentry")) { + JsonObject sentry = root.getAsJsonObject("sentry"); + sentryEnabled = getBool(sentry, "enabled", sentryEnabled); + sentryDsn = getString(sentry, "dsn", sentryDsn); + sentryEnvironment = getString(sentry, "environment", sentryEnvironment); + sentryDebug = getBool(sentry, "debug", sentryDebug); + sentryTracesSampleRate = getDouble(sentry, "tracesSampleRate", sentryTracesSampleRate); + } else { + // Auto-migrate from old config/sentry.json if it exists + migrateLegacySentryConfig(); + } + // Apply settings to Logger applyToLogger(); } @@ -131,6 +165,14 @@ protected void writeModuleSettings(@NotNull JsonObject root) { categories.addProperty("integration", integration); categories.addProperty("economy", economy); root.add("categories", categories); + + JsonObject sentry = new JsonObject(); + sentry.addProperty("enabled", sentryEnabled); + sentry.addProperty("dsn", sentryDsn); + sentry.addProperty("environment", sentryEnvironment); + sentry.addProperty("debug", sentryDebug); + sentry.addProperty("tracesSampleRate", sentryTracesSampleRate); + root.add("sentry", sentry); } /** @@ -282,6 +324,65 @@ public boolean isEconomy() { return economy; } + // === Sentry Getters === + + /** + * Checks if Sentry error tracking is enabled. + * + * @return true if Sentry is enabled + */ + public boolean isSentryEnabled() { + return sentryEnabled; + } + + /** + * Gets the Sentry DSN (Data Source Name) URL. + * + * @return DSN string + */ + @NotNull + public String getSentryDsn() { + return sentryDsn; + } + + /** + * Gets the environment name sent to Sentry (e.g., "production", "development"). + * + * @return environment name + */ + @NotNull + public String getSentryEnvironment() { + return sentryEnvironment; + } + + /** + * Checks if Sentry debug logging is enabled. + * + * @return true if Sentry debug mode is on + */ + public boolean isSentryDebug() { + return sentryDebug; + } + + /** + * Gets the traces sample rate for Sentry performance monitoring. + * 0.0 = no performance traces, 1.0 = capture all. + * + * @return sample rate between 0.0 and 1.0 + */ + public double getSentryTracesSampleRate() { + return sentryTracesSampleRate; + } + + /** + * Sets whether Sentry error tracking is enabled. + * + * @param enabled true to enable + */ + public void setSentryEnabled(boolean enabled) { + this.sentryEnabled = enabled; + } + // === Setters (for runtime toggle) === /** @@ -443,4 +544,47 @@ public void disableAll() { economy = false; applyToLogger(); } + + /** + * Migrates sentry settings from the old config/sentry.json file into this config. + * Called when debug.json has no "sentry" section and we need to check for legacy data. + * Deletes the old file after successful migration. + */ + private void migrateLegacySentryConfig() { + Path sentryFile = filePath.getParent().resolve("sentry.json"); + if (!Files.exists(sentryFile)) { + return; + } + + try { + String json = Files.readString(sentryFile); + JsonObject root = JsonParser.parseString(json).getAsJsonObject(); + + // Read sentry values from the old file + if (root.has("enabled")) { + sentryEnabled = root.get("enabled").getAsBoolean(); + } + if (root.has("dsn")) { + sentryDsn = root.get("dsn").getAsString(); + } + if (root.has("environment")) { + sentryEnvironment = root.get("environment").getAsString(); + } + if (root.has("debug")) { + sentryDebug = root.get("debug").getAsBoolean(); + } + if (root.has("tracesSampleRate")) { + sentryTracesSampleRate = root.get("tracesSampleRate").getAsDouble(); + } + + needsSave = true; + Logger.info("[Config] Migrated sentry config from sentry.json into debug.json"); + + // Delete the old file + Files.delete(sentryFile); + Logger.info("[Config] Deleted old config/sentry.json"); + } catch (Exception e) { + Logger.warn("[Config] Failed to migrate sentry.json: %s", e.getMessage()); + } + } } diff --git a/src/main/java/com/hyperfactions/config/modules/SentryConfig.java b/src/main/java/com/hyperfactions/config/modules/SentryConfig.java deleted file mode 100644 index f933228..0000000 --- a/src/main/java/com/hyperfactions/config/modules/SentryConfig.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.hyperfactions.config.modules; - -import com.google.gson.JsonObject; -import com.hyperfactions.config.ModuleConfig; -import java.nio.file.Path; -import org.jetbrains.annotations.NotNull; - -/** - * Configuration for Sentry error tracking integration. - * Controls whether errors are reported to Sentry, the DSN, environment, and debug settings. - */ -public class SentryConfig extends ModuleConfig { - - private static final String DEFAULT_DSN = - "https://cc41f97749e8b8b1562defea6ba3de9c@o4510966614589440.ingest.us.sentry.io/4510966616162304"; - - private String dsn = DEFAULT_DSN; - - private String environment = "production"; - - private boolean debug = false; - - private double tracesSampleRate = 0.0; - - /** - * Creates a new sentry config. - * - * @param filePath path to config/sentry.json - */ - public SentryConfig(@NotNull Path filePath) { - super(filePath); - } - - @Override - @NotNull - public String getModuleName() { - return "sentry"; - } - - @Override - protected boolean getDefaultEnabled() { - return true; - } - - @Override - protected void createDefaults() { - enabled = true; - dsn = DEFAULT_DSN; - environment = "production"; - debug = false; - tracesSampleRate = 0.0; - } - - @Override - protected void loadModuleSettings(@NotNull JsonObject root) { - dsn = getString(root, "dsn", dsn); - environment = getString(root, "environment", environment); - debug = getBool(root, "debug", debug); - tracesSampleRate = getDouble(root, "tracesSampleRate", tracesSampleRate); - } - - @Override - protected void writeModuleSettings(@NotNull JsonObject root) { - root.addProperty("dsn", dsn); - root.addProperty("environment", environment); - root.addProperty("debug", debug); - root.addProperty("tracesSampleRate", tracesSampleRate); - } - - /** - * Gets the Sentry DSN (Data Source Name) URL. - * - * @return DSN string, empty if not configured - */ - @NotNull - public String getDsn() { - return dsn; - } - - /** - * Gets the environment name sent to Sentry (e.g., "production", "development"). - * - * @return environment name - */ - @NotNull - public String getEnvironment() { - return environment; - } - - /** - * Checks if Sentry debug logging is enabled. - * - * @return true if debug mode is on - */ - public boolean isDebug() { - return debug; - } - - /** - * Gets the traces sample rate for performance monitoring. - * 0.0 = no performance traces, 1.0 = capture all. - * - * @return sample rate between 0.0 and 1.0 - */ - public double getTracesSampleRate() { - return tracesSampleRate; - } -} \ No newline at end of file diff --git a/src/main/java/com/hyperfactions/integration/SentryIntegration.java b/src/main/java/com/hyperfactions/integration/SentryIntegration.java index 44631b6..bb6188e 100644 --- a/src/main/java/com/hyperfactions/integration/SentryIntegration.java +++ b/src/main/java/com/hyperfactions/integration/SentryIntegration.java @@ -1,7 +1,7 @@ package com.hyperfactions.integration; import com.hyperfactions.BuildInfo; -import com.hyperfactions.config.modules.SentryConfig; +import com.hyperfactions.config.modules.DebugConfig; import com.hyperfactions.util.Logger; import com.hypixel.hytale.common.util.java.ManifestUtil; import com.hypixel.hytale.server.core.HytaleServer; @@ -35,23 +35,23 @@ private record BufferedError(String message, Throwable throwable) {} private SentryIntegration() {} /** - * Initializes Sentry from the given config. + * Initializes Sentry from the debug config's sentry section. * Does nothing if Sentry is disabled or the DSN is empty. * Safe to call multiple times — subsequent calls are ignored. * - * @param config the sentry configuration + * @param config the debug configuration containing sentry settings */ - public static void init(@NotNull SentryConfig config) { + public static void init(@NotNull DebugConfig config) { if (initialized) { return; } - if (!config.isEnabled()) { + if (!config.isSentryEnabled()) { Logger.info("[Sentry] Sentry is disabled in config"); return; } - String dsn = config.getDsn(); + String dsn = config.getSentryDsn(); if (dsn == null || dsn.isBlank()) { Logger.info("[Sentry] No DSN configured — Sentry will not send events"); return; @@ -61,10 +61,10 @@ public static void init(@NotNull SentryConfig config) { Sentry.init(options -> { options.setDsn(dsn); options.setRelease("hyperfactions@" + BuildInfo.VERSION); - options.setDebug(config.isDebug()); + options.setDebug(config.isSentryDebug()); // Auto-detect environment from server patchline, fall back to config value - String environment = config.getEnvironment(); + String environment = config.getSentryEnvironment(); try { String patchline = ManifestUtil.getPatchline(); if (patchline != null && !patchline.isEmpty()) { @@ -81,7 +81,7 @@ public static void init(@NotNull SentryConfig config) { options.setEnvironment(environment); // Performance monitoring (0.0 = off by default) - double traceRate = config.getTracesSampleRate(); + double traceRate = config.getSentryTracesSampleRate(); if (traceRate > 0.0) { options.setTracesSampleRate(traceRate); } @@ -157,7 +157,7 @@ public static void init(@NotNull SentryConfig config) { }); initialized = true; - Logger.info("[Sentry] Initialized (env=%s, debug=%s)", config.getEnvironment(), config.isDebug()); + Logger.info("[Sentry] Initialized (env=%s, debug=%s)", config.getSentryEnvironment(), config.isSentryDebug()); // Collect installed mods (may be incomplete during init — refreshed on boot) refreshInstalledMods(); diff --git a/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java b/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java index d67cb45..f3fd820 100644 --- a/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java +++ b/src/main/java/com/hyperfactions/platform/HyperFactionsPlugin.java @@ -118,7 +118,7 @@ protected void start() { hyperFactions.enable(); // Initialize Sentry error tracking (after config is loaded, non-blocking, fail-safe) - SentryIntegration.init(ConfigManager.get().sentry()); + SentryIntegration.init(ConfigManager.get().debug()); // Initialize GravestonePlugin integration (v2 direct API — needs EventRegistry) hyperFactions.initGravestoneIntegration(getEventRegistry());