diff --git a/docs/Network-Play.md b/docs/Network-Play.md index 7dcba1d95fe..3bd9be95286 100644 --- a/docs/Network-Play.md +++ b/docs/Network-Play.md @@ -258,7 +258,10 @@ Forge writes detailed network logs during online multiplayer games. These are se | **Windows** | `%APPDATA%/Forge/networklogs/` | | **macOS** | `~/Library/Application Support/Forge/networklogs/` | | **Linux** | `~/.forge/networklogs/` | +| **Android** | `Android/data/forge.app/files/Forge/networklogs/` (typically not browsable without a file manager — use the in-app export below) | -On desktop, you can open this folder directly from the Forge game menu: **Online > Open Network Logs**. +On **Desktop**, you can open this folder directly from the Forge game menu: **Online > Open Network Logs**. + +On **Mobile**, you can export logs in .zip file to your Downloads folder from **Settings > Files > Data Management > Export Network Logs**. By default, logs from the last 10 games are kept; older logs are automatically removed. \ No newline at end of file diff --git a/forge-gui-mobile/src/forge/screens/settings/FilesPage.java b/forge-gui-mobile/src/forge/screens/settings/FilesPage.java index ac517c92629..afedf58f131 100644 --- a/forge-gui-mobile/src/forge/screens/settings/FilesPage.java +++ b/forge-gui-mobile/src/forge/screens/settings/FilesPage.java @@ -1,8 +1,12 @@ package forge.screens.settings; import java.io.File; +import java.io.FilenameFilter; import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -89,6 +93,26 @@ public void select() { }); } }, 0); + //Export Game Log + lstItems.addItem(new Extra(Forge.getLocalizer().getMessage("lblExportGameLog"), Forge.getLocalizer().getMessage("lblExportGameLogDescription")) { + @Override + public void select() { + exportLogs("lblExportGameLog", + new File(ForgeProfileProperties.getUserDir()), + (dir, name) -> name.startsWith("forge") && name.endsWith(".log"), + "forge-logs"); + } + }, 0); + //Export Network Logs + lstItems.addItem(new Extra(Forge.getLocalizer().getMessage("lblExportNetworkLogs"), Forge.getLocalizer().getMessage("lblExportNetworkLogsDescription")) { + @Override + public void select() { + exportLogs("lblExportNetworkLogs", + new File(ForgeConstants.NETWORK_LOGS_DIR), + (dir, name) -> name.startsWith("network-debug-") && name.endsWith(".log"), + "forge-network-logs"); + } + }, 0); //Auditer lstItems.addItem(new Extra(Forge.getLocalizer().getMessage("btnListImageData"), Forge.getLocalizer().getMessage("lblListImageData")) { @Override @@ -226,6 +250,30 @@ protected void doLayout(float width, float height) { lstItems.setBounds(0, 0, width, height); } + private void exportLogs(String dialogTitleKey, File sourceDir, FilenameFilter filter, String outputPrefix) { + if (Forge.getDeviceAdapter().needFileAccess()) { + Forge.getDeviceAdapter().requestFileAcces(); + return; + } + final String dialogTitle = Forge.getLocalizer().getMessage(dialogTitleKey); + FThreads.invokeInEdtLater(() -> LoadingOverlay.show(Forge.getLocalizer().getMessage("lblExporting"), true, () -> { + try { + File[] matches = sourceDir.isDirectory() ? sourceDir.listFiles(filter) : null; + if (matches == null || matches.length == 0) { + FOptionPane.showMessageDialog(Forge.getLocalizer().getMessage("lblNoLogFilesFound"), dialogTitle, FOptionPane.INFORMATION_ICON); + return; + } + String stamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")); + File downloads = new FileHandle(Forge.getDeviceAdapter().getDownloadsDir()).file(); + File zipFile = new File(downloads, outputPrefix + "-" + stamp + ".zip"); + ZipUtil.zipFiles(Arrays.asList(matches), zipFile); + FOptionPane.showMessageDialog(Forge.getLocalizer().getMessage("lblSuccess") + "\n" + zipFile.getAbsolutePath(), dialogTitle, FOptionPane.INFORMATION_ICON); + } catch (IOException e) { + FOptionPane.showMessageDialog(e.toString(), Forge.getLocalizer().getMessage("lblError"), FOptionPane.ERROR_ICON); + } + })); + } + private abstract class FilesItem { protected String label; protected String description; diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 86b32cef4b0..61ffe0c1340 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -3401,6 +3401,12 @@ lblPrepareDatabase=Preparing database... lblLoadingGameResources=Loading game resources... lblBackupRestore=Backup and Restore lblBackupRestoreDescription=Backup or Restore Classic game mode data to/from Downloads folder +lblExportGameLog=Export Game Log +lblExportGameLogDescription=Export forge.log files to Downloads folder as a zip +lblExportNetworkLogs=Export Network Logs +lblExportNetworkLogsDescription=Export network debug logs to Downloads folder as a zip +lblExporting=Exporting... +lblNoLogFilesFound=No log files found lblDataManagement=Data Management lblPlsSelectActions=Please select options to perform action lblBackupMsg=Backing up files diff --git a/forge-gui/src/main/java/forge/util/ZipUtil.java b/forge-gui/src/main/java/forge/util/ZipUtil.java index eabe48747b3..000ad51dc62 100644 --- a/forge-gui/src/main/java/forge/util/ZipUtil.java +++ b/forge-gui/src/main/java/forge/util/ZipUtil.java @@ -5,6 +5,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; +import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; @@ -25,6 +26,29 @@ public static void zip(File source, File dest, String name) throws IOException { } } + /** + * Write the given files to a flat zip archive at {@code zipFile}. Each entry uses the + * source file's basename; no directory structure is preserved. Files that don't exist + * are skipped silently so callers don't have to pre-filter. + */ + public static void zipFiles(List files, File zipFile) throws IOException { + try (FileOutputStream fos = new FileOutputStream(zipFile); + ZipOutputStream zipOut = new ZipOutputStream(fos)) { + byte[] buffer = new byte[1024]; + for (File file : files) { + if (file == null || !file.isFile()) continue; + try (FileInputStream fis = new FileInputStream(file)) { + zipOut.putNextEntry(new ZipEntry(file.getName())); + int length; + while ((length = fis.read(buffer)) >= 0) { + zipOut.write(buffer, 0, length); + } + zipOut.closeEntry(); + } + } + } + } + private static void zipFile(File fileToZip, String fileName, ZipOutputStream zipOut) throws IOException { if (fileToZip.isHidden()) { return;