diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java index 3ace4df057d..b8bcdcd1f34 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java @@ -17,6 +17,8 @@ */ package org.jackhuang.hmcl.game; +import org.jackhuang.hmcl.addon.mod.LocalModFile; +import org.jackhuang.hmcl.addon.mod.ModManager; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.IOUtils; import org.jackhuang.hmcl.util.io.Zipper; @@ -32,7 +34,9 @@ import java.nio.file.PathMatcher; import java.util.ArrayList; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.concurrent.CompletableFuture; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -74,6 +78,100 @@ public static CompletableFuture exportLogs( zipper.putTextFile(logs, "minecraft.log"); zipper.putTextFile(Logger.filterForbiddenToken(launchScript), OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh"); + try { + ModManager modManager = gameRepository.getModManager(versionId); + modManager.refresh(); + + List activeMods = modManager.getLocalFiles().stream() + .filter(LocalModFile::isActive) + .sorted((m1, m2) -> String.CASE_INSENSITIVE_ORDER.compare(m1.getName(), m2.getName())) + .toList(); + + StringBuilder infoBuilder = new StringBuilder(); + + // Mods that exist both as a standalone file and inside another active mod's + // Jar-in-Jar payload. Such duplicates can conflict and crash the game, so list + // them at the very top of the report. + LinkedHashSet duplicates = new LinkedHashSet<>(); + for (LocalModFile host : activeMods) { + if (!host.hasBundledMods()) continue; + for (String bundled : host.getBundledMods()) { + String bundledName = bundled.contains("/") ? bundled.substring(bundled.lastIndexOf('/') + 1) : bundled; + String bundledLower = bundledName.toLowerCase(Locale.ROOT); + String bundledNorm = bundledLower.replace("-", "").replace("_", ""); + for (LocalModFile other : activeMods) { + if (other == host) continue; + String id = other.getId(); + if (id == null || id.length() < 3) continue; + String idLower = id.toLowerCase(Locale.ROOT); + if (bundledLower.contains(idLower) + || bundledNorm.contains(idLower.replace("-", "").replace("_", ""))) { + duplicates.add(other.getName() + " [" + other.getFileName() + "] <-> bundled in " + host.getName() + " (" + bundledName + ")"); + } + } + } + } + + infoBuilder.append("=== Potential Duplicate Mods (installed separately AND bundled via Jar-in-Jar) ===").append(System.lineSeparator()); + if (duplicates.isEmpty()) { + infoBuilder.append("None").append(System.lineSeparator()); + } else { + infoBuilder.append("These mods may conflict with their bundled copies and cause crashes:").append(System.lineSeparator()); + infoBuilder.append("(Heuristic: matched by mod id appearing in the bundled jar file name; may include false positives.)").append(System.lineSeparator()); + for (String line : duplicates) { + infoBuilder.append("\t|-> ").append(line).append(System.lineSeparator()); + } + } + infoBuilder.append(System.lineSeparator()) + .append("----------------------------").append(System.lineSeparator()) + .append(System.lineSeparator()); + + infoBuilder.append("=== Mod List ===").append(System.lineSeparator()); + infoBuilder.append("Filesystem structure of: ").append(runDirectory.resolve("mods")).append(System.lineSeparator()); + infoBuilder.append("|-> mods").append(System.lineSeparator()); + + for (LocalModFile mod : activeMods) { + infoBuilder.append("| |-> ").append(mod.getName()); + if (StringUtils.isNotBlank(mod.getVersion()) && !"${version}".equals(mod.getVersion())) { + infoBuilder.append(" (").append(mod.getVersion()).append(")"); + } + if (!mod.getName().equals(mod.getFileName())) { + infoBuilder.append(" [").append(mod.getFileName()).append("]"); + } + infoBuilder.append(System.lineSeparator()); + } + + infoBuilder.append(System.lineSeparator()) + .append("----------------------------").append(System.lineSeparator()) + .append(System.lineSeparator()) + .append("=== Jar-in-Jar Info List (active mods only) ===").append(System.lineSeparator()); + + boolean hasJij = false; + for (LocalModFile mod : activeMods) { + if (mod.hasBundledMods()) { + hasJij = true; + infoBuilder.append(mod.getName()); + if (!mod.getName().equals(mod.getFileName())) { + infoBuilder.append(" [").append(mod.getFileName()).append("]"); + } + infoBuilder.append(System.lineSeparator()); + for (String bundled : mod.getBundledMods()) { + String name = bundled.contains("/") ? bundled.substring(bundled.lastIndexOf('/') + 1) : bundled; + infoBuilder.append("\t|-> ").append(name).append(System.lineSeparator()); + } + infoBuilder.append(System.lineSeparator()); + } + } + + if (!hasJij) { + infoBuilder.append("No Jar-in-Jar info found").append(System.lineSeparator()); + } + + zipper.putTextFile(infoBuilder.toString(), "mods_info.txt"); + } catch (Exception e) { + LOG.warning("Failed to export mod info to crash report package", e); + } + for (String id : versions) { Path versionJson = baseDirectory.resolve("versions").resolve(id).resolve(id + ".json"); if (Files.exists(versionJson)) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index fb2c3b99858..5c7f4bf93e6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -113,6 +113,7 @@ public enum SVG { SELECT_ALL("M7 17V7H17V17H7ZM9 15H15V9H9V15ZM5 19V21Q4.175 21 3.5875 20.4125T3 19H5ZM3 17V15H5V17H3ZM3 13V11H5V13H3ZM3 9V7H5V9H3ZM5 5H3Q3 4.175 3.5875 3.5875T5 3V5ZM7 21V19H9V21H7ZM7 5V3H9V5H7ZM11 21V19H13V21H11ZM11 5V3H13V5H11ZM15 21V19H17V21H15ZM15 5V3H17V5H15ZM19 21V19H21Q21 19.825 20.4125 20.4125T19 21ZM19 17V15H21V17H19ZM19 13V11H21V13H19ZM19 9V7H21V9H19ZM19 5V3Q19.825 3 20.4125 3.5875T21 5H19Z"), SETTINGS("M19.43 12.98C19.47 12.66 19.5 12.34 19.5 12 19.5 11.66 19.47 11.34 19.43 11.02L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.57 5.11 19.4 5.02 19.22 5.02 19.16 5.02 19.1 5.03 19.05 5.05L16.56 6.05C16.04 5.65 15.48 5.32 14.87 5.07L14.49 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.51 2.42L9.13 5.07C8.52 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.89 5.03 4.83 5.02 4.77 5.02 4.6 5.02 4.43 5.11 4.34 5.27L2.34 8.73C2.21 8.95 2.27 9.22 2.46 9.37L4.57 11.02C4.53 11.34 4.5 11.67 4.5 12 4.5 12.33 4.53 12.66 4.57 12.98L2.46 14.63C2.27 14.78 2.22 15.05 2.34 15.27L4.34 18.73C4.43 18.89 4.6 18.98 4.78 18.98 4.84 18.98 4.9 18.97 4.95 18.95L7.44 17.95C7.96 18.35 8.52 18.68 9.13 18.93L9.51 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.49 21.58L14.87 18.93C15.48 18.68 16.04 18.34 16.56 17.95L19.05 18.95C19.11 18.97 19.17 18.98 19.23 18.98 19.4 18.98 19.57 18.89 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.98ZM17.45 11.27C17.49 11.58 17.5 11.79 17.5 12 17.5 12.21 17.48 12.43 17.45 12.73L17.31 13.86 18.2 14.56 19.28 15.4 18.58 16.61 17.31 16.1 16.27 15.68 15.37 16.36C14.94 16.68 14.53 16.92 14.12 17.09L13.06 17.52 12.9 18.65 12.7 20H11.3L11.11 18.65 10.95 17.52 9.89 17.09C9.46 16.91 9.06 16.68 8.66 16.38L7.75 15.68 6.69 16.11 5.42 16.62 4.72 15.41 5.8 14.57 6.69 13.87 6.55 12.74C6.52 12.43 6.5 12.2 6.5 12S6.52 11.57 6.55 11.27L6.69 10.14 5.8 9.44 4.72 8.6 5.42 7.39 6.69 7.9 7.73 8.32 8.63 7.64C9.06 7.32 9.47 7.08 9.88 6.91L10.94 6.48 11.1 5.35 11.3 4H12.69L12.88 5.35 13.04 6.48 14.1 6.91C14.53 7.09 14.93 7.32 15.33 7.62L16.24 8.32 17.3 7.89 18.57 7.38 19.27 8.59 18.2 9.44 17.31 10.14 17.45 11.27ZM12 8C9.79 8 8 9.79 8 12S9.79 16 12 16 16 14.21 16 12 14.21 8 12 8ZM12 14C10.9 14 10 13.1 10 12S10.9 10 12 10 14 10.9 14 12 13.1 14 12 14Z"), // Material Icons SETTINGS_FILL("M9.25 22l-.4-3.2q-.325-.125-.6125-.3t-.5625-.375L4.7 19.375l-2.75-4.75 2.575-1.95Q4.5 12.5 4.5 12.3375v-.675q0-.1625.025-.3375L1.95 9.375 4.7 4.625l2.975 1.25q.275-.2.575-.375t.6-.3L9.25 2h5.5l.4 3.2q.325.125.6125.3t.5625.375L19.3 4.625l2.75 4.75-2.575 1.95q.025.175.025.3375v.675q0 .1625-.05.3375l2.575 1.95-2.75 4.75-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2H9.25Zm2.8-6.5q1.45 0 2.475-1.025T15.55 12 14.525 9.525 12.05 8.5q-1.475 0-2.4875 1.025T8.55 12q0 1.45 1.0125 2.475T12.05 15.5Z"), // Material Icons + STACKS("M11.99 18.54l-7.37-5.73L3 14.07l9 7 9-7-1.63-1.27-7.38 5.74zM12 16l7.36-5.73L21 9l-9-7-9 7 1.63 1.27L12 16z"), STADIA_CONTROLLER("M4.725 20Q3.225 20 2.1625 18.925T1.05 16.325Q1.05 16.1 1.075 15.875T1.15 15.425L3.25 7.025Q3.6 5.675 4.675 4.8375T7.125 4H16.875Q18.25 4 19.325 4.8375T20.75 7.025L22.85 15.425Q22.9 15.65 22.9375 15.8875T22.975 16.35Q22.975 17.875 21.8875 18.9375T19.275 20Q18.225 20 17.325 19.45T15.975 17.95L15.275 16.5Q15.15 16.25 14.9 16.125T14.375 16H9.625Q9.35 16 9.1 16.125T8.725 16.5L8.025 17.95Q7.575 18.9 6.675 19.45T4.725 20ZM4.8 18Q5.275 18 5.6625 17.75T6.25 17.075L6.95 15.65Q7.325 14.875 8.05 14.4375T9.625 14H14.375Q15.225 14 15.95 14.45T17.075 15.65L17.775 17.075Q17.975 17.5 18.3625 17.75T19.225 18Q19.925 18 20.425 17.5375T20.95 16.375Q20.95 16.4 20.9 15.9L18.8 7.525Q18.625 6.85 18.1 6.425T16.875 6H7.125Q6.425 6 5.8875 6.425T5.2 7.525L3.1 15.9Q3.05 16.05 3.05 16.35 3.05 17.05 3.5625 17.525T4.8 18ZM13.5 11Q13.925 11 14.2125 10.7125T14.5 10Q14.5 9.575 14.2125 9.2875T13.5 9Q13.075 9 12.7875 9.2875T12.5 10Q12.5 10.425 12.7875 10.7125T13.5 11ZM15.5 9Q15.925 9 16.2125 8.7125T16.5 8Q16.5 7.575 16.2125 7.2875T15.5 7Q15.075 7 14.7875 7.2875T14.5 8Q14.5 8.425 14.7875 8.7125T15.5 9ZM15.5 13Q15.925 13 16.2125 12.7125T16.5 12Q16.5 11.575 16.2125 11.2875T15.5 11Q15.075 11 14.7875 11.2875T14.5 12Q14.5 12.425 14.7875 12.7125T15.5 13ZM17.5 11Q17.925 11 18.2125 10.7125T18.5 10Q18.5 9.575 18.2125 9.2875T17.5 9Q17.075 9 16.7875 9.2875T16.5 10Q16.5 10.425 16.7875 10.7125T17.5 11ZM8.5 12.5Q8.825 12.5 9.0375 12.2875T9.25 11.75V10.75H10.25Q10.575 10.75 10.7875 10.5375T11 10Q11 9.675 10.7875 9.4625T10.25 9.25H9.25V8.25Q9.25 7.925 9.0375 7.7125T8.5 7.5Q8.175 7.5 7.9625 7.7125T7.75 8.25V9.25H6.75Q6.425 9.25 6.2125 9.4625T6 10Q6 10.325 6.2125 10.5375T6.75 10.75H7.75V11.75Q7.75 12.075 7.9625 12.2875T8.5 12.5ZM12 12Z"), STADIA_CONTROLLER_FILL("M4.725 20q-1.5 0-2.5625-1.075T1.05 16.325q0-.225.025-.45t.075-.45l2.1-8.4q.35-1.35 1.425-2.1875T7.125 4h9.75q1.375 0 2.45.8375T20.75 7.025l2.1 8.4q.05.225.0875.4625t.0375.4625q0 1.525-1.0875 2.5875T19.275 20q-1.05 0-1.95-.55t-1.35-1.5l-.7-1.45q-.125-.25-.375-.375T14.375 16H9.625q-.275 0-.525.125t-.375.375l-.7 1.45q-.45.95-1.35 1.5T4.725 20ZM13.5 11q.425 0 .7125-.2875T14.5 10t-.2875-.7125T13.5 9t-.7125.2875T12.5 10t.2875.7125T13.5 11Zm2-2q.425 0 .7125-.2875T16.5 8q0-.425-.2875-.7125T15.5 7q-.425 0-.7125.2875T14.5 8t.2875.7125T15.5 9Zm0 4q.425 0 .7125-.2875T16.5 12q0-.425-.2875-.7125T15.5 11q-.425 0-.7125.2875T14.5 12t.2875.7125T15.5 13Zm2-2q.425 0 .7125-.2875T18.5 10q0-.425-.2875-.7125T17.5 9q-.425 0-.7125.2875T16.5 10q0 .425.2875.7125T17.5 11Zm-9 1.5q.325 0 .5375-.2125T9.25 11.75v-1h1q.325 0 .5375-.2125T11 10t-.2125-.5375T10.25 9.25h-1v-1q0-.325-.2125-.5375T8.5 7.5q-.325 0-.5375.2125T7.75 8.25v1h-1q-.325 0-.5375.2125T6 10q0 .325.2125.5375T6.75 10.75h1v1q0 .325.2125.5375T8.5 12.5Z"), STYLE("M3.975 19.8 3.125 19.45Q2.35 19.125 2.0875 18.325T2.175 16.75L3.975 12.85V19.8ZM7.975 22Q7.15 22 6.5625 21.4125T5.975 20V14L8.625 21.35Q8.7 21.525 8.775 21.6875T8.975 22H7.975ZM13.125 21.9Q12.325 22.2 11.575 21.825T10.525 20.65L6.075 8.45Q5.775 7.65 6.125 6.8875T7.275 5.85L14.825 3.1Q15.625 2.8 16.375 3.175T17.425 4.35L21.875 16.55Q22.175 17.35 21.825 18.1125T20.675 19.15L13.125 21.9ZM10.975 10Q11.4 10 11.6875 9.7125T11.975 9Q11.975 8.575 11.6875 8.2875T10.975 8Q10.55 8 10.2625 8.2875T9.975 9Q9.975 9.425 10.2625 9.7125T10.975 10ZM12.425 20 19.975 17.25 15.525 5 7.975 7.75 12.425 20ZM7.975 7.75 15.525 5 7.975 7.75Z"), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java index f9f90e95904..f8b81d8305b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java @@ -23,10 +23,13 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.css.PseudoClass; +import javafx.geometry.HPos; +import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.geometry.VPos; +import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.ui.FXUtils; @@ -46,9 +49,41 @@ public TwoLineListItem() { lblTitle = new Label(); lblTitle.getStyleClass().add("title"); - this.firstLine = new HBox(lblTitle); + // Custom layout for the title row: when there isn't enough room, the tags shrink (and clip) + // first; only once they are gone does the title itself start to ellipsize. The preferred + // width stays the natural title+tags width (no width bindings), so this works in any parent + // without circular layout dependencies. + this.firstLine = new HBox(lblTitle) { + @Override + protected void layoutChildren() { + var managed = getManagedChildren(); + if (managed.size() != 2) { + super.layoutChildren(); + return; + } + + Insets insets = getInsets(); + double x = insets.getLeft(); + double y = insets.getTop(); + double availableWidth = Math.max(0, getWidth() - x - insets.getRight()); + double availableHeight = Math.max(0, getHeight() - y - insets.getBottom()); + + Node title = managed.get(0); + Node tagsNode = managed.get(1); + double spacing = getSpacing(); + + double titleWidth = Math.min(title.prefWidth(availableHeight), availableWidth); + double tagsWidth = Math.min(tagsNode.prefWidth(availableHeight), + Math.max(0, availableWidth - titleWidth - spacing)); + + layoutInArea(title, x, y, titleWidth, availableHeight, 0, HPos.LEFT, VPos.CENTER); + layoutInArea(tagsNode, x + titleWidth + spacing, y, tagsWidth, availableHeight, 0, HPos.LEFT, VPos.CENTER); + } + }; firstLine.getStyleClass().add("first-line"); firstLine.setAlignment(Pos.CENTER_LEFT); + // Safety net: never let the row's content render past its own right edge. + FXUtils.setOverflowHidden(firstLine); this.getChildren().setAll(firstLine); } @@ -168,15 +203,13 @@ public ObservableList