diff --git a/CHANGELOG.md b/CHANGELOG.md index c6de5a2..2a157cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,46 +1,9 @@ # Changelog -## v4.0.0 +## v4.1.0 -### Core Features -- First-launch **Welcome Wizard** for guided setup -- Custom **main menu styles** (Modern, Modern Minimal, Minimal) -- Built-in **performance profile selector** -- Visual customization pages for: - - Tab design - - Item background style - - Storage design -- Optional wizard pages for supported mods: - - **ScaleMe** sword block toggle - - **ScamScreener** alert + ping setup -- **Resource pack selection** during setup -- Final **review + apply** page with per-setting status - -### Config Pack System -- Automatic config pack detection and loading -- Resolution-based best-match config selection -- Safer update behavior (tracks applied pack + version) -- Restart-safe pending apply flow for full preset changes - -### Config Manager UI -- New in-game **modpack config screen** with tabs: - - Configuration - - Export - - Import - - Backups -- Browse config pack contents before applying -- Apply **selected files** or apply **entire preset** - -### Export / Import / Backups -- Export selected files as reusable config pack `.zip` -- Include metadata in exports (name, version, author, description, target resolution, GUI scale) -- Import external config packs from imports folder -- Create and restore backups from in-game UI - -### Commands & Utilities -- Command to reopen wizard: `/packcore wizard` -- Command to open config manager: `/packcore modpack_config` -- Update check commands: - - `/packcore update check` - - `/packcore update reset` -- Performance/design quick commands for advanced users +### Diagnostics +- Full diagnostics report logged on every startup (modpack info, config pack, settings, runtime, system) +- Crash reports now include a **PackCore Diagnostics** section with the same data as the startup log +- New `/packcore diagnose` command — shows a compact report in chat with a **click-to-copy** button for easy sharing when reporting issues +- New `/packcore crashtest` command — triggers a test crash to verify the crash report enrichment is working \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index e1721b8..797d7bf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ org.gradle.parallel=true org.gradle.configuration-cache=false # Mod properties -mod.version=4.0.0 +mod.version=4.1.0 mod.group=com.github.kd_gaming1 mod.id=packcore mod.name=Pack Core diff --git a/settings.gradle.kts b/settings.gradle.kts index ba504d5..2fad8c7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,7 +8,7 @@ pluginManagement { } plugins { - id("dev.kikugie.stonecutter") version "0.9-beta.1" + id("dev.kikugie.stonecutter") version "0.9" } stonecutter { diff --git a/src/main/java/com/github/kd_gaming1/packcore/PackCore.java b/src/main/java/com/github/kd_gaming1/packcore/PackCore.java index 6d159a7..d2c4106 100644 --- a/src/main/java/com/github/kd_gaming1/packcore/PackCore.java +++ b/src/main/java/com/github/kd_gaming1/packcore/PackCore.java @@ -2,6 +2,7 @@ import com.github.kd_gaming1.packcore.command.PackCoreCommands; import com.github.kd_gaming1.packcore.config.PackCoreConfig; +import com.github.kd_gaming1.packcore.util.diagnostics.DiagnosticsCollector; import com.github.kd_gaming1.packcore.gui.screen.PackCoreTitleScreen; import com.github.kd_gaming1.packcore.gui.screen.SBETitleScreen; import com.github.kd_gaming1.packcore.gui.screen.WelcomeWizardScreen; @@ -23,70 +24,65 @@ import java.nio.file.Path; public class PackCore implements ClientModInitializer { + public static final String MOD_ID = "packcore"; public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - - public static final Path PACKCORE_DIR = FabricLoader.getInstance().getGameDir().resolve("packcore"); + public static final Path PACKCORE_DIR = + FabricLoader.getInstance().getGameDir().resolve("packcore"); public static boolean migratedFromV3 = false; private static boolean replacingTitleScreen = false; @Override public void onInitializeClient() { - LOGGER.info("[PackCore] Initialized"); + LOGGER.info("[PackCore] Initialized\n{}", DiagnosticsCollector.buildFullReport()); RamWarningHelper.init(); UpdateChecker.checkAsync(); - ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> PackCoreCommands.register(dispatcher)); + ClientCommandRegistrationCallback.EVENT.register( + (dispatcher, registryAccess) -> PackCoreCommands.register(dispatcher)); ScreenEvents.BEFORE_INIT.register((client, screen, scaledWidth, scaledHeight) -> { - if (!(screen instanceof TitleScreen)) return; - if (screen instanceof PackCoreTitleScreen) return; - + if (!(screen instanceof TitleScreen) || screen instanceof PackCoreTitleScreen) return; RamWarningHelper.onMainMenu(); - if (PackCoreConfig.menuStyle != PackCoreConfig.MenuStyle.MINIMAL) { scheduleConfiguredTitleScreen(client, screen); } }); ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> { - if (!(screen instanceof TitleScreen)) return; - if (screen instanceof PackCoreTitleScreen) return; + if (!(screen instanceof TitleScreen) || screen instanceof PackCoreTitleScreen) return; if (!PackCoreConfig.successfulWelcomeWizard) return; if (PackCoreConfig.menuStyle != PackCoreConfig.MenuStyle.MINIMAL) return; - PackCoreTitleScreen.decorateExisting((TitleScreen) screen, scaledWidth, scaledHeight); }); - ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> client.execute(RamWarningHelper::onWorldJoin)); + ClientPlayConnectionEvents.JOIN.register( + (handler, sender, client) -> client.execute(RamWarningHelper::onWorldJoin)); ClientLifecycleEvents.CLIENT_STARTED.register(client -> PlaytimeTracker.onSessionStart()); ClientLifecycleEvents.CLIENT_STOPPING.register(client -> PlaytimeTracker.onSessionEnd()); } private static void scheduleConfiguredTitleScreen(Minecraft client, Screen screen) { - if (!(screen instanceof TitleScreen) || screen instanceof PackCoreTitleScreen || replacingTitleScreen) return; - + if (replacingTitleScreen) return; replacingTitleScreen = true; client.execute(() -> { try { if (client.screen != screen) return; - if (!PackCoreConfig.successfulWelcomeWizard) { client.setScreen(new WelcomeWizardScreen(screen)); return; } - switch (PackCoreConfig.menuStyle) { - case MODERN -> client.setScreen(new SBETitleScreen()); + case MODERN -> client.setScreen(new SBETitleScreen()); case MODERN_MINIMAL -> client.setScreen(new SBETitleScreen(false)); - case MINIMAL -> client.setScreen(new PackCoreTitleScreen()); + case MINIMAL -> client.setScreen(new PackCoreTitleScreen()); } } finally { replacingTitleScreen = false; } }); } -} +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/command/PackCoreCommands.java b/src/main/java/com/github/kd_gaming1/packcore/command/PackCoreCommands.java index 3898399..90328e1 100644 --- a/src/main/java/com/github/kd_gaming1/packcore/command/PackCoreCommands.java +++ b/src/main/java/com/github/kd_gaming1/packcore/command/PackCoreCommands.java @@ -1,5 +1,6 @@ package com.github.kd_gaming1.packcore.command; +import com.github.kd_gaming1.packcore.util.diagnostics.DiagnosticsCollector; import com.github.kd_gaming1.packcore.gui.screen.WelcomeWizardScreen; import com.github.kd_gaming1.packcore.gui.screen.config.ConfigScreen; import com.github.kd_gaming1.packcore.integration.ItemBackgroundManager; @@ -8,15 +9,16 @@ import com.github.kd_gaming1.packcore.integration.TabDesignManager; import com.github.kd_gaming1.packcore.update.UpdateCache; import com.github.kd_gaming1.packcore.update.UpdateChecker; -import com.github.kd_gaming1.packcore.update.UpdateStatus; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; +import java.util.Arrays; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.ChatFormatting; +import net.minecraft.CrashReport; import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; - -import java.util.Arrays; -import java.util.concurrent.CompletableFuture; +import net.minecraft.network.chat.Style; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; @@ -36,8 +38,7 @@ public static void register(CommandDispatcher dispatc .then(literal("reset").executes(ctx -> { resetUpdateCache(ctx.getSource()); return 1; - })) - ) + }))) .then(literal("performance") .then(argument("profile", StringArgumentType.word()) .suggests((ctx, builder) -> { @@ -47,12 +48,10 @@ public static void register(CommandDispatcher dispatc return builder.buildFuture(); }) .executes(ctx -> { - String id = StringArgumentType.getString(ctx, "profile"); - applyPerformanceProfile(ctx.getSource(), id); + applyPerformanceProfile( + ctx.getSource(), StringArgumentType.getString(ctx, "profile")); return 1; - }) - ) - ) + }))) .then(literal("tabdesign") .then(literal("compact").executes(ctx -> { applyTabDesign(ctx.getSource(), TabDesignManager.TabDesign.COMPACT); @@ -61,8 +60,7 @@ public static void register(CommandDispatcher dispatc .then(literal("fancy").executes(ctx -> { applyTabDesign(ctx.getSource(), TabDesignManager.TabDesign.FANCY); return 1; - })) - ) + }))) .then(literal("itembg") .then(literal("none").executes(ctx -> { applyItemBackground(ctx.getSource(), ItemBackgroundManager.ItemBackground.NONE); @@ -75,8 +73,7 @@ public static void register(CommandDispatcher dispatc .then(literal("square").executes(ctx -> { applyItemBackground(ctx.getSource(), ItemBackgroundManager.ItemBackground.SQUARE); return 1; - })) - ) + }))) .then(literal("storagedesign") .then(literal("overlay").executes(ctx -> { applyStorageDesign(ctx.getSource(), StorageDesignManager.StorageDesign.OVERLAY); @@ -85,48 +82,83 @@ public static void register(CommandDispatcher dispatc .then(literal("vanilla").executes(ctx -> { applyStorageDesign(ctx.getSource(), StorageDesignManager.StorageDesign.VANILLA); return 1; - })) - ) - .then(literal("wizard") - .executes(ctx -> { - Minecraft.getInstance().execute(() -> - Minecraft.getInstance().setScreen(new WelcomeWizardScreen(Minecraft.getInstance().screen)) - ); - return 1; - }) - ) - .then(literal("modpack_config") - .executes(ctx -> { - Minecraft.getInstance().execute(() -> - Minecraft.getInstance().setScreen(new ConfigScreen()) - ); - return 1; - }) - ) - ); + }))) + .then(literal("wizard").executes(ctx -> { + Minecraft.getInstance().execute(() -> + Minecraft.getInstance().setScreen( + new WelcomeWizardScreen(Minecraft.getInstance().screen))); + return 1; + })) + .then(literal("modpack_config").executes(ctx -> { + Minecraft.getInstance().execute(() -> + Minecraft.getInstance().setScreen(new ConfigScreen())); + return 1; + })) + .then(literal("diagnose").executes(ctx -> { + sendDiagnostics(ctx.getSource()); + return 1; + })) + .then(literal("crashtest").executes(ctx -> { + triggerTestCrash(); + return 1; + }))); } - private static void checkUpdate(FabricClientCommandSource source) { - send(source, "Checking for updates..."); + // --------------------------------------------------------------------------- + // Diagnostics + // --------------------------------------------------------------------------- + + private static void sendDiagnostics(FabricClientCommandSource source) { + String report = DiagnosticsCollector.buildCompactReport(); + + for (String line : report.split("\n")) { + source.sendFeedback(Component.literal(line).withStyle(ChatFormatting.GRAY)); + } - CompletableFuture future = UpdateChecker.checkAsync(); + source.sendFeedback( + Component.literal("[PackCore] ") + .withStyle(ChatFormatting.DARK_AQUA) + .append( + Component.literal(" [ Click to copy ] ") + .withStyle( + Style.EMPTY + .withColor(ChatFormatting.AQUA) + .withUnderlined(true) + .withClickEvent( + new ClickEvent.CopyToClipboard(report))))); + } - future.thenAccept(status -> Minecraft.getInstance().execute(() -> { - switch (status.state()) { - case UP_TO_DATE -> send(source, "You are up to date! Version: " + status.installedVersion()); - case UPDATE_AVAILABLE -> { - send(source, "Update available!"); - send(source, "Installed: " + status.installedVersion()); - send(source, "Latest: " + status.latestVersion()); + private static void triggerTestCrash() { + Minecraft.getInstance().execute(() -> + Minecraft.getInstance().delayCrash( + CrashReport.forThrowable( + new Throwable("PackCore crash report test"), + "PackCore crashtest command"))); + } - if (status.changelog() != null && !status.changelog().isBlank()) { - send(source, "Changelog:"); - send(source, status.changelog()); + // --------------------------------------------------------------------------- + // Handlers + // --------------------------------------------------------------------------- + + private static void checkUpdate(FabricClientCommandSource source) { + send(source, "Checking for updates..."); + UpdateChecker.checkAsync().thenAccept(status -> + Minecraft.getInstance().execute(() -> { + switch (status.state()) { + case UP_TO_DATE -> + send(source, "You are up to date! Version: " + status.installedVersion()); + case UPDATE_AVAILABLE -> { + send(source, "Update available!"); + send(source, "Installed: " + status.installedVersion()); + send(source, "Latest: " + status.latestVersion()); + if (status.changelog() != null && !status.changelog().isBlank()) { + send(source, "Changelog:"); + send(source, status.changelog()); + } + } + case UNKNOWN -> sendError(source, "Could not determine update status."); } - } - case UNKNOWN -> sendError(source, "Could not determine update status."); - } - })); + })); } private static void resetUpdateCache(FabricClientCommandSource source) { @@ -134,61 +166,71 @@ private static void resetUpdateCache(FabricClientCommandSource source) { send(source, "Update cache cleared. Next check will fetch from Modrinth."); } - private static void applyStorageDesign(FabricClientCommandSource source, StorageDesignManager.StorageDesign design) { - send(source, "Applying storage design: " + design.name().toLowerCase() + "..."); - boolean success = StorageDesignManager.apply(design); - if (success) { - send(source, "Storage design applied: " + design.name().toLowerCase() + private static void applyStorageDesign( + FabricClientCommandSource source, StorageDesignManager.StorageDesign design) { + String name = design.name().toLowerCase(); + send(source, "Applying storage design: " + name + "..."); + if (StorageDesignManager.apply(design)) { + send(source, "Storage design applied: " + name + ". If not in a world yet, the change will take effect on next world join."); } else { - sendError(source, "Failed to apply storage design: " + design.name().toLowerCase() + ". Firmament may not be loaded — check logs."); + sendError(source, "Failed to apply storage design: " + name + + ". Firmament may not be loaded — check logs."); } } - private static void applyItemBackground(FabricClientCommandSource source, ItemBackgroundManager.ItemBackground background) { - send(source, "Applying item background: " + background.name().toLowerCase() + "..."); - boolean success = ItemBackgroundManager.apply(background); - if (success) { - send(source, "Item background applied: " + background.name().toLowerCase()); + private static void applyItemBackground( + FabricClientCommandSource source, ItemBackgroundManager.ItemBackground background) { + String name = background.name().toLowerCase(); + send(source, "Applying item background: " + name + "..."); + if (ItemBackgroundManager.apply(background)) { + send(source, "Item background applied: " + name); } else { - sendError(source, "Failed to apply item background: " + background.name().toLowerCase() + ". Skyblocker may not be loaded — check logs."); + sendError(source, "Failed to apply item background: " + name + + ". Skyblocker may not be loaded — check logs."); } } - private static void applyTabDesign(FabricClientCommandSource source, TabDesignManager.TabDesign design) { - send(source, "Applying tab design: " + design.name().toLowerCase() + "..."); - boolean success = TabDesignManager.apply(design); - if (success) { - send(source, "Tab design applied: " + design.name().toLowerCase()); + private static void applyTabDesign( + FabricClientCommandSource source, TabDesignManager.TabDesign design) { + String name = design.name().toLowerCase(); + send(source, "Applying tab design: " + name + "..."); + if (TabDesignManager.apply(design)) { + send(source, "Tab design applied: " + name); } else { - sendError(source, "Failed to apply tab design: " + design.name().toLowerCase() + ". Check logs for details."); + sendError(source, "Failed to apply tab design: " + name + ". Check logs for details."); } } private static void applyPerformanceProfile(FabricClientCommandSource source, String id) { - PerformanceProfileService.PerformanceProfile profile = Arrays.stream(PerformanceProfileService.PerformanceProfile.values()) - .filter(p -> p.id().equals(id)) - .findFirst() - .orElse(null); + PerformanceProfileService.PerformanceProfile profile = + Arrays.stream(PerformanceProfileService.PerformanceProfile.values()) + .filter(p -> p.id().equals(id)) + .findFirst() + .orElse(null); if (profile == null) { - sendError(source, "Unknown performance profile: \"" + id + "\". Valid options: " - + Arrays.stream(PerformanceProfileService.PerformanceProfile.values()) + String valid = Arrays.stream(PerformanceProfileService.PerformanceProfile.values()) .map(PerformanceProfileService.PerformanceProfile::id) - .reduce((a, b) -> a + ", " + b).orElse("")); + .reduce((a, b) -> a + ", " + b) + .orElse(""); + sendError(source, "Unknown performance profile: \"" + id + "\". Valid options: " + valid); return; } send(source, "Applying performance profile: " + profile.getDisplayName() + "..."); - boolean success = PerformanceProfileService.applyAll(profile); - - if (success) { + if (PerformanceProfileService.applyAll(profile)) { send(source, "Performance profile applied: " + profile.getDisplayName()); } else { - sendError(source, "One or more integrations failed for profile: " + profile.getDisplayName() + ". Check logs for details."); + sendError(source, "One or more integrations failed for profile: " + + profile.getDisplayName() + ". Check logs for details."); } } + // --------------------------------------------------------------------------- + // Feedback helpers + // --------------------------------------------------------------------------- + private static void send(FabricClientCommandSource source, String message) { source.sendFeedback(Component.literal("[PackCore] " + message)); } diff --git a/src/main/java/com/github/kd_gaming1/packcore/mixin/CrashReportMixin.java b/src/main/java/com/github/kd_gaming1/packcore/mixin/CrashReportMixin.java new file mode 100644 index 0000000..7be764c --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/mixin/CrashReportMixin.java @@ -0,0 +1,18 @@ +package com.github.kd_gaming1.packcore.mixin; + +import com.github.kd_gaming1.packcore.util.diagnostics.CrashReportEnricher; +import net.minecraft.CrashReport; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** Injects PackCore diagnostics into every crash report automatically. */ +@Mixin(CrashReport.class) +public class CrashReportMixin { + + @Inject(method = "", at = @At("RETURN")) + private void packcore$enrichCrashReport(String message, Throwable cause, CallbackInfo ci) { + CrashReportEnricher.register((CrashReport) (Object) this); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/diagnostics/CrashReportEnricher.java b/src/main/java/com/github/kd_gaming1/packcore/util/diagnostics/CrashReportEnricher.java new file mode 100644 index 0000000..51541c8 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/util/diagnostics/CrashReportEnricher.java @@ -0,0 +1,25 @@ +package com.github.kd_gaming1.packcore.util.diagnostics; + +import com.github.kd_gaming1.packcore.mixin.CrashReportMixin; +import net.minecraft.CrashReport; +import net.minecraft.CrashReportCategory; + +/** + * Appends a "PackCore Diagnostics" section to Minecraft crash reports. + * + *

Invoked automatically via {@link CrashReportMixin}. The try/catch ensures + * this can never make a crash report worse. + */ +public final class CrashReportEnricher { + + private CrashReportEnricher() {} + + public static void register(CrashReport report) { + CrashReportCategory cat = report.addCategory("PackCore Diagnostics"); + try { + cat.setDetail("Report", DiagnosticsCollector.buildFullReport()); + } catch (Exception e) { + cat.setDetail("Error", "Failed to collect diagnostics: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/diagnostics/DiagnosticsCollector.java b/src/main/java/com/github/kd_gaming1/packcore/util/diagnostics/DiagnosticsCollector.java new file mode 100644 index 0000000..e872d4f --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/util/diagnostics/DiagnosticsCollector.java @@ -0,0 +1,148 @@ +package com.github.kd_gaming1.packcore.util.diagnostics; + +import com.github.kd_gaming1.packcore.config.PackCoreConfig; +import com.github.kd_gaming1.packcore.metadata.ModpackMetadata; +import com.github.kd_gaming1.packcore.update.UpdateChecker; +import com.github.kd_gaming1.packcore.update.UpdateStatus; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import net.fabricmc.loader.api.FabricLoader; + +/** + * Collects modpack, config, and runtime diagnostics. All reporting surfaces + * (startup log, crash reports, /packcore diagnose) delegate here so the data + * is always consistent. + */ +public final class DiagnosticsCollector { + + private static final DateTimeFormatter DATE_FMT = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.systemDefault()); + + private DiagnosticsCollector() {} + + // --------------------------------------------------------------------------- + // Public API + // --------------------------------------------------------------------------- + + /** Full multi-section report for the startup log and crash reports. */ + public static String buildFullReport() { + ModpackMetadata meta = ModpackMetadata.getInstance(); + Runtime rt = Runtime.getRuntime(); + MemoryMXBean mem = ManagementFactory.getMemoryMXBean(); + var os = ManagementFactory.getOperatingSystemMXBean(); + + return section("Modpack", new String[][] { + {"Name", meta.getModpackName()}, + {"Version", meta.getModpackVersion()}, + {"Minecraft Version", meta.getMinecraftVersion()}, + {"Author", meta.getAuthor()}, + {"Description", meta.getDescription()}, + {"Modrinth ID", meta.getModrinthProjectId()}, + }) + + section("Config Pack", new String[][] { + {"Last Applied Version", blankOr(PackCoreConfig.lastAppliedVersion)}, + {"Last Applied File", blankOr(PackCoreConfig.lastAppliedPackFile)}, + }) + + section("Settings", new String[][] { + {"Menu Style", PackCoreConfig.menuStyle.name()}, + {"Wizard Complete", String.valueOf(PackCoreConfig.successfulWelcomeWizard)}, + {"Auto Backup", PackCoreConfig.autoBackupEnabled + ? "enabled (every " + PackCoreConfig.autoBackupIntervalDays + " days)" + : "disabled"}, + {"Last Backup", formatEpoch(PackCoreConfig.lastBackupEpochMs)}, + }) + + section("Runtime", new String[][] { + {"RAM Allocated", mb(rt.maxMemory()) + " MB"}, + {"RAM Used", mb(rt.totalMemory() - rt.freeMemory()) + " MB"}, + {"Heap Used / Max", + mb(mem.getHeapMemoryUsage().getUsed()) + + " MB / " + + mb(mem.getHeapMemoryUsage().getMax()) + + " MB"}, + {"CPU Cores", String.valueOf(rt.availableProcessors())}, + {"Java Version", System.getProperty("java.version")}, + {"JVM", System.getProperty("java.vm.name")}, + {"Fabric Loader", + FabricLoader.getInstance() + .getModContainer("fabricloader") + .map(c -> c.getMetadata().getVersion().getFriendlyString()) + .orElse("unknown")}, + }) + + section("System", new String[][] { + {"OS", System.getProperty("os.name") + " " + System.getProperty("os.version")}, + {"Architecture", System.getProperty("os.arch")}, + {"CPU Load", String.format("%.1f%%", os.getSystemLoadAverage() * 100)}, + {"Physical RAM", physicalRam(os)}, + }); + } + + /** + * Compact report for /packcore diagnose — only what a developer needs when a + * player reports an issue. + */ + public static String buildCompactReport() { + ModpackMetadata meta = ModpackMetadata.getInstance(); + Runtime rt = Runtime.getRuntime(); + + return String.join( + "\n", + "=== PackCore Diagnostics ===", + "Modpack : " + meta.getModpackName() + " " + meta.getModpackVersion(), + "MC : " + meta.getMinecraftVersion(), + "Update : " + updateLine(), + "RAM : " + mb(rt.totalMemory() - rt.freeMemory()) + " MB used / " + mb(rt.maxMemory()) + " MB allocated", + "Config : " + blankOr(PackCoreConfig.lastAppliedVersion) + " (" + blankOr(PackCoreConfig.lastAppliedPackFile) + ")", + "Backup : " + formatEpoch(PackCoreConfig.lastBackupEpochMs), + "Menu : " + PackCoreConfig.menuStyle.name(), + "OS : " + System.getProperty("os.name") + " " + System.getProperty("os.arch"), + "Java : " + System.getProperty("java.version"), + "============================"); + } + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + private static String section(String title, String[][] fields) { + int keyWidth = Arrays.stream(fields).mapToInt(f -> f[0].length()).max().orElse(0); + StringBuilder sb = new StringBuilder(" -- ").append(title).append(" --\n"); + for (String[] f : fields) { + sb.append(String.format(" %-" + keyWidth + "s : %s%n", f[0], f[1])); + } + return sb.append('\n').toString(); + } + + private static long mb(long bytes) { + return bytes / 1024 / 1024; + } + + private static String physicalRam(java.lang.management.OperatingSystemMXBean os) { + if (os instanceof com.sun.management.OperatingSystemMXBean sunOs) { + return mb(sunOs.getTotalMemorySize()) + " MB"; + } + return "unavailable"; + } + + /** Returns the cached update status line, or "not yet checked" if unavailable. */ + private static String updateLine() { + UpdateStatus cached = UpdateChecker.getCachedStatus(); + return switch (cached.state()) { + case UP_TO_DATE -> "up to date (" + cached.installedVersion() + ")"; + case UPDATE_AVAILABLE -> "update available → " + cached.latestVersion(); + case UNKNOWN -> "not yet checked"; + }; + } + private static String formatEpoch(long epochMs) { + if (epochMs == 0) return "never"; + return DATE_FMT.format(Instant.ofEpochMilli(epochMs)); + } + + /** Returns the value if non-blank, otherwise {@code "none"}. */ + private static String blankOr(String value) { + return (value == null || value.isBlank()) ? "none" : value; + } +} \ No newline at end of file diff --git a/src/main/resources/packcore.mixins.json b/src/main/resources/packcore.mixins.json index 845cff4..be3a964 100644 --- a/src/main/resources/packcore.mixins.json +++ b/src/main/resources/packcore.mixins.json @@ -7,5 +7,6 @@ "defaultRequire": 1 }, "client": [ + "CrashReportMixin" ] }