From da88a4a5b6ff95e382e9de0f8c3b9fd21d84654f Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 20 May 2024 11:40:52 +0200 Subject: [PATCH 01/22] add processing of multiple fluff images --- .../ui/swing/util/FluffImageHelper.java | 99 ++++++++++++++++--- 1 file changed, 85 insertions(+), 14 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java b/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java index 71e15497e7b..12560afba8a 100644 --- a/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java +++ b/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java @@ -25,12 +25,21 @@ import megamek.common.Mech; import megamek.common.annotations.Nullable; import megamek.common.preference.PreferenceManager; +import org.apache.logging.log4j.LogManager; import javax.swing.*; import java.awt.*; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toList; /** * This class provides methods for retrieving fluff images, for use in MM, MML and MHQ; also @@ -76,6 +85,10 @@ public final class FluffImageHelper { return getFluffImage(unit, false); } + public static @Nullable List getFluffImages(@Nullable BTObject unit) { + return getFluffImageList(unit, false); + } + /** * Returns a fluff image for the given unit for the record sheet, with a fallback * file named "hud.png" if that is present in the right fluff directory, or null if nothing @@ -91,23 +104,44 @@ public final class FluffImageHelper { } private static @Nullable Image getFluffImage(@Nullable BTObject unit, boolean recordSheet) { - if (unit == null) { + List fluffImages = getFluffImageList(unit, recordSheet); + if (!fluffImages.isEmpty()) { + //return fluffImages.get(0); + // TEST --- just choose a random image from the available ones. Should be get(0) instead + int rndIndex = (int) (Math.random() * fluffImages.size()); + return fluffImages.get(rndIndex); + // --- + } else { return null; } + } + + /** + * Returns a list of available fluff images. If a fluff image is embedded in the unit file, + * only that image is returned, even if others are available from the fluff directories. The returned + * list may be empty, but not null. + * + * @param unit The unit + * @param recordSheet True if this image search is meant for a record sheet + * @return Available fluff images or the embedded fluff image + */ + private static List getFluffImageList(@Nullable BTObject unit, boolean recordSheet) { + if (unit == null) { + return new ArrayList<>(); + } Image embeddedFluffImage = unit.getFluffImage(); if (embeddedFluffImage != null) { - return embeddedFluffImage; + return List.of(embeddedFluffImage); } else { - File fluffImageFile = findFluffFile(unit, recordSheet); - if (fluffImageFile != null) { - return new ImageIcon(fluffImageFile.toString()).getImage(); - } else { - return null; - } + return findFluffFiles(unit, recordSheet).stream() + .map(File::toString) + .map(ImageIcon::new) + .map(ImageIcon::getImage) + .collect(Collectors.toList()); } } - private static @Nullable File findFluffFile(BTObject unit, boolean recordSheet) { + private static @Nullable List findFluffFiles(BTObject unit, boolean recordSheet) { List fileCandidates = new ArrayList<>(); var fluffDir = new File(Configuration.fluffImagesDir(), FluffImageHelper.getFluffPath(unit)); var rsFluffSuperDir = new File(Configuration.fluffImagesDir(), "rs"); @@ -130,11 +164,14 @@ public final class FluffImageHelper { } } } + for (String nameCandidate : nameCandidates) { for (String ext : EXTENSIONS_FLUFF_IMAGE_FORMATS) { fileCandidates.add(new File(fluffUserDir, nameCandidate + ext)); } } + + fileCandidates.addAll(getFluffInChassisDir(unit, fluffUserDir)); } // Internal fluff path matches @@ -149,12 +186,46 @@ public final class FluffImageHelper { fileCandidates.add(new File(fluffDir, "hud.png")); } - for (File possibleFile : fileCandidates) { - if (possibleFile.exists() && !possibleFile.isDirectory()) { - return possibleFile; - } + fileCandidates.addAll(getFluffInChassisDir(unit, fluffDir)); + + fileCandidates.removeIf(f -> !f.exists() || f.isDirectory()); + return fileCandidates; + } + + /** + * With the addition of multiple fluff images, file matching depends on the directory a file is in. + *
- In the main fluff/[unittype]/ directory the old rules apply, i.e. a file is valid if it + * matches the model exactly or if the filename is only the chassis and matches the unit's chassis. + * The filename may now contain additional information after an underscore (atlas_xyz.jpg matches for + * any Atlas mek). + *
- In a chassis subdirectory fluff/[unittype]/[chassis], all files match if [chassis] + * matches the unit's chassis (even if the filename has the wrong model) AND if there is no + * [model] subdirectory matching the unit's model. Empty models match the directory "---empty---". + * The filename doesn't matter for matching. + *
- In a model subdirectory fluff/[unittype]/[chassis]/[model], all files match if the + * unit's chassis and model match [chassis] and [model]. The filename doesn't matter for matching. + */ + private static List getFluffInChassisDir(BTObject unit, File unitTypeFluffDir) { + List result = new ArrayList<>(); + String sanitizedChassis = sanitize(unit.generalName()); + var chassisDir = new File(unitTypeFluffDir, sanitizedChassis); + if (!chassisDir.exists()) { + return result; + } + + String sanitizedModel = sanitize(unit.specificName()); + if (sanitizedModel.isBlank()) { + sanitizedModel = "---empty---"; + } + var modelDir = new File(chassisDir, sanitizedModel); + File correctFluffDir = modelDir.exists() ? modelDir : chassisDir; + + try (Stream entries = Files.walk(correctFluffDir.toPath())) { + result.addAll(entries.map(Objects::toString).map(File::new).collect(toList())); + } catch (IOException e) { + LogManager.getLogger().warn("Error while reading files from " + correctFluffDir, e); } - return null; + return result; } private static String sanitize(String original) { From bd2d0fac6b11449909f29981ae8cfff0aa579f96 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 20 May 2024 12:15:41 +0200 Subject: [PATCH 02/22] allow clicking the fluff in the unit view to advance to the next image --- .../client/ui/swing/MechViewPanel.java | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/MechViewPanel.java b/megamek/src/megamek/client/ui/swing/MechViewPanel.java index baba8fd74e7..364f6c9d134 100644 --- a/megamek/src/megamek/client/ui/swing/MechViewPanel.java +++ b/megamek/src/megamek/client/ui/swing/MechViewPanel.java @@ -28,12 +28,13 @@ import megamek.common.templates.TROView; import org.apache.logging.log4j.LogManager; +import java.util.List; import javax.swing.*; import javax.swing.border.EmptyBorder; import javax.swing.event.HyperlinkEvent; import java.awt.*; -import java.awt.event.MouseWheelEvent; -import java.awt.event.MouseWheelListener; +import java.awt.event.*; +import java.util.ArrayList; /** * @author Jay Lawson @@ -46,6 +47,8 @@ public class MechViewPanel extends JPanel { private JTextPane txtMek = new JTextPane(); private JLabel lblMek = new JLabel(); private JScrollPane scrMek; + private final List fluffImageList = new ArrayList<>(); + private int imageIndex = 0; public static final int DEFAULT_WIDTH = 360; public static final int DEFAULT_HEIGHT = 600; @@ -92,6 +95,8 @@ public MechViewPanel(int width, int height, boolean noBorder) { setLayout(new BorderLayout()); add(sp); addMouseWheelListener(wheelForwarder); + + lblMek.addMouseListener(mouseListener); } public void setMech(Entity entity, MechView mechView) { @@ -123,12 +128,18 @@ public void setMech(Entity entity, String fontName) { } private void setFluffImage(Entity entity) { - Image image = FluffImageHelper.getFluffImage(entity); + fluffImageList.clear(); + fluffImageList.addAll(FluffImageHelper.getFluffImages(entity)); + imageIndex = 0; + setNextFluffImage(); + } + + private void setFluffImage(Image image) { // Scale down to the default width if the image is wider than that if (null != image) { if (image.getWidth(this) > DEFAULT_WIDTH) { image = image.getScaledInstance(DEFAULT_WIDTH, -1, Image.SCALE_SMOOTH); - } + } lblMek.setIcon(new ImageIcon(image)); } else { lblMek.setIcon(null); @@ -147,4 +158,23 @@ public void reset() { listener.mouseWheelMoved(converted); } }; + + private final MouseListener mouseListener = new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + setNextFluffImage(); + } + }; + + private void setNextFluffImage() { + imageIndex++; + if (imageIndex >= fluffImageList.size()) { + imageIndex = 0; + } + if (imageIndex < fluffImageList.size()) { + setFluffImage(fluffImageList.get(imageIndex)); + } else { + setFluffImage((Image) null); + } + } } \ No newline at end of file From f26549af1de7f8b06dec352b3c817eb199fc2d38 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 20 May 2024 15:34:31 +0200 Subject: [PATCH 03/22] cleanup --- .../client/ui/swing/MechViewPanel.java | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/MechViewPanel.java b/megamek/src/megamek/client/ui/swing/MechViewPanel.java index 364f6c9d134..4512207d7ec 100644 --- a/megamek/src/megamek/client/ui/swing/MechViewPanel.java +++ b/megamek/src/megamek/client/ui/swing/MechViewPanel.java @@ -26,7 +26,6 @@ import megamek.common.MechView; import megamek.common.Report; import megamek.common.templates.TROView; -import org.apache.logging.log4j.LogManager; import java.util.List; import javax.swing.*; @@ -44,11 +43,12 @@ public class MechViewPanel extends JPanel { private static final long serialVersionUID = 2438490306644271135L; - private JTextPane txtMek = new JTextPane(); - private JLabel lblMek = new JLabel(); + private final JTextPane txtMek = new JTextPane(); private JScrollPane scrMek; + + private final JLabel fluffImageLabel = new JLabel(); private final List fluffImageList = new ArrayList<>(); - private int imageIndex = 0; + private int fluffImageIndex = 0; public static final int DEFAULT_WIDTH = 360; public static final int DEFAULT_HEIGHT = 600; @@ -84,7 +84,7 @@ public MechViewPanel(int width, int height, boolean noBorder) { var fluffPanel = new FixedXPanel(); fluffPanel.setMinimumSize(new Dimension(width, height)); fluffPanel.setPreferredSize(new Dimension(width, height)); - fluffPanel.add(lblMek); + fluffPanel.add(fluffImageLabel); JPanel p = new JPanel(); p.setLayout(new BoxLayout(p, BoxLayout.LINE_AXIS)); @@ -96,7 +96,7 @@ public MechViewPanel(int width, int height, boolean noBorder) { add(sp); addMouseWheelListener(wheelForwarder); - lblMek.addMouseListener(mouseListener); + fluffImageLabel.addMouseListener(mouseListener); } public void setMech(Entity entity, MechView mechView) { @@ -130,7 +130,7 @@ public void setMech(Entity entity, String fontName) { private void setFluffImage(Entity entity) { fluffImageList.clear(); fluffImageList.addAll(FluffImageHelper.getFluffImages(entity)); - imageIndex = 0; + fluffImageIndex = 0; setNextFluffImage(); } @@ -140,19 +140,19 @@ private void setFluffImage(Image image) { if (image.getWidth(this) > DEFAULT_WIDTH) { image = image.getScaledInstance(DEFAULT_WIDTH, -1, Image.SCALE_SMOOTH); } - lblMek.setIcon(new ImageIcon(image)); + fluffImageLabel.setIcon(new ImageIcon(image)); } else { - lblMek.setIcon(null); + fluffImageLabel.setIcon(null); } } public void reset() { txtMek.setText(""); - lblMek.setIcon(null); + fluffImageLabel.setIcon(null); } /** Forwards a mouse wheel scroll on the fluff image or free space to the TRO entry. */ - MouseWheelListener wheelForwarder = e -> { + private final MouseWheelListener wheelForwarder = e -> { MouseWheelEvent converted = (MouseWheelEvent) SwingUtilities.convertMouseEvent(MechViewPanel.this, e, scrMek); for (MouseWheelListener listener : scrMek.getMouseWheelListeners()) { listener.mouseWheelMoved(converted); @@ -167,12 +167,12 @@ public void mouseClicked(MouseEvent e) { }; private void setNextFluffImage() { - imageIndex++; - if (imageIndex >= fluffImageList.size()) { - imageIndex = 0; + fluffImageIndex++; + if (fluffImageIndex >= fluffImageList.size()) { + fluffImageIndex = 0; } - if (imageIndex < fluffImageList.size()) { - setFluffImage(fluffImageList.get(imageIndex)); + if (fluffImageIndex < fluffImageList.size()) { + setFluffImage(fluffImageList.get(fluffImageIndex)); } else { setFluffImage((Image) null); } From 2c6663f1e5302b117a6c6e829be0c2884e86762d Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 21 May 2024 19:22:23 +0200 Subject: [PATCH 04/22] allow clan mek chassis variants, require chassis in model subdir name --- .../ui/swing/util/FluffImageHelper.java | 56 +++++++++++++++---- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java b/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java index 12560afba8a..822328b8b89 100644 --- a/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java +++ b/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java @@ -171,7 +171,7 @@ private static List getFluffImageList(@Nullable BTObject unit, boolean re } } - fileCandidates.addAll(getFluffInChassisDir(unit, fluffUserDir)); + fileCandidates.addAll(getFluffInChassisDirs(unit, fluffUserDir)); } // Internal fluff path matches @@ -186,7 +186,7 @@ private static List getFluffImageList(@Nullable BTObject unit, boolean re fileCandidates.add(new File(fluffDir, "hud.png")); } - fileCandidates.addAll(getFluffInChassisDir(unit, fluffDir)); + fileCandidates.addAll(getFluffInChassisDirs(unit, fluffDir)); fileCandidates.removeIf(f -> !f.exists() || f.isDirectory()); return fileCandidates; @@ -205,25 +205,61 @@ private static List getFluffImageList(@Nullable BTObject unit, boolean re *
- In a model subdirectory fluff/[unittype]/[chassis]/[model], all files match if the * unit's chassis and model match [chassis] and [model]. The filename doesn't matter for matching. */ - private static List getFluffInChassisDir(BTObject unit, File unitTypeFluffDir) { + private static List getFluffInChassisDirs(BTObject unit, File unitTypeFluffDir) { List result = new ArrayList<>(); + for (String nameCandidate : chassisNameCandidates(unit)) { + var chassisDir = new File(unitTypeFluffDir, nameCandidate); + if (chassisDir.exists()) { + result.addAll(getFluffInChassisDir(unit, chassisDir)); + } + } + return result; + } + + /** + * @return For the unit, returns the possible chassis lookup strings, which is simply the chassis + * (the list has only one entry) for all units except Clan Meks with a double name, where the list + * includes the four variations on Timber Wolf (Mad Cat), Mad Cat (Timber Wolf), Mad Cat and + * Timber Wolf. Note that a few units have X (Y) chassis that are not clan double names. Those + * will return only the full chassis X (Y). + */ + private static List chassisNameCandidates(BTObject unit) { + List result = new ArrayList<>(); String sanitizedChassis = sanitize(unit.generalName()); - var chassisDir = new File(unitTypeFluffDir, sanitizedChassis); - if (!chassisDir.exists()) { - return result; + result.add(sanitizedChassis); + if ((unit instanceof Mech) && !((Mech) unit).getClanChassisName().isBlank()) { + String sanitizedClanChassis = sanitize(((Mech) unit).getClanChassisName()); + result.add(sanitizedClanChassis + " (" + sanitizedChassis + ")"); + result.add(sanitizedChassis + " (" + sanitizedClanChassis + ")"); + result.add(sanitizedClanChassis); } + return result; + } + private static List getFluffInChassisDir(BTObject unit, File chassisDir) { String sanitizedModel = sanitize(unit.specificName()); if (sanitizedModel.isBlank()) { sanitizedModel = "---empty---"; } - var modelDir = new File(chassisDir, sanitizedModel); - File correctFluffDir = modelDir.exists() ? modelDir : chassisDir; + List result = new ArrayList<>(); + for (String chassisNameCandidate : chassisNameCandidates(unit)) { + var modelDir = new File(chassisDir, chassisNameCandidate + " " + sanitizedModel); + if (modelDir.exists()) { + result.addAll(getFluffInDir(modelDir)); + } + } + if (result.isEmpty()) { + result.addAll(getFluffInDir(chassisDir)); + } + return result; + } - try (Stream entries = Files.walk(correctFluffDir.toPath())) { + private static List getFluffInDir(File dir) { + List result = new ArrayList<>(); + try (Stream entries = Files.walk(dir.toPath())) { result.addAll(entries.map(Objects::toString).map(File::new).collect(toList())); } catch (IOException e) { - LogManager.getLogger().warn("Error while reading files from " + correctFluffDir, e); + LogManager.getLogger().warn("Error while reading files from " + dir, e); } return result; } From 8fa601d220753f414e11a9cc6b18c9b89dfcfd8c Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 22 May 2024 19:03:48 +0200 Subject: [PATCH 05/22] clean up chassis name generation --- .../ui/swing/util/FluffImageHelper.java | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java b/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java index 822328b8b89..4a26d768eb3 100644 --- a/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java +++ b/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java @@ -85,6 +85,26 @@ public final class FluffImageHelper { return getFluffImage(unit, false); } + /** + * Returns a list of all fluff images for the given unit/object to be shown e.g. in the + * unit summary. + * + *

If a fluff image is stored in the unit/object itself, e.g. if it was part of the + * unit's file or is created by the unit itself, only this is returned. Note that this + * is not used for canon units, but may be used in custom ones by adding a fluff + * image to the unit in MML. + * + *

Otherwise, the fluff image directories are searched. First searches the user dir, + * then the internal dir. Tries to match the image by chassis + model or chassis alone. + * Chassis and model names are cleaned from " and / characters before matching. For + * Meks with clan names, both names and the combinations are searched. The model + * alone is not used to search. + * + * Returns null if no fluff image can be found. + * + * @param unit The unit + * @return a fluff image or null, if no match is found + */ public static @Nullable List getFluffImages(@Nullable BTObject unit) { return getFluffImageList(unit, false); } @@ -144,26 +164,13 @@ private static List getFluffImageList(@Nullable BTObject unit, boolean re private static @Nullable List findFluffFiles(BTObject unit, boolean recordSheet) { List fileCandidates = new ArrayList<>(); var fluffDir = new File(Configuration.fluffImagesDir(), FluffImageHelper.getFluffPath(unit)); - var rsFluffSuperDir = new File(Configuration.fluffImagesDir(), "rs"); - var rsFluffDir = new File(rsFluffSuperDir, FluffImageHelper.getFluffPath(unit)); List nameCandidates = nameCandidates(unit); // UserDir matches - // For internal use: in [user dir]/data/images/fluff/rs/ images for record sheets can be placed - // These will be preferentially loaded when the recordSheet paremeter is true String userDir = PreferenceManager.getClientPreferences().getUserDir(); if (!userDir.isBlank() && new File(userDir).isDirectory()) { var fluffUserDir = new File(userDir, fluffDir.toString()); - var rsFluffUserDir = new File(userDir, rsFluffDir.toString()); - - if (recordSheet) { - for (String nameCandidate : nameCandidates) { - for (String ext : EXTENSIONS_FLUFF_IMAGE_FORMATS) { - fileCandidates.add(new File(rsFluffUserDir, nameCandidate + ext)); - } - } - } for (String nameCandidate : nameCandidates) { for (String ext : EXTENSIONS_FLUFF_IMAGE_FORMATS) { @@ -270,20 +277,11 @@ private static String sanitize(String original) { private static List nameCandidates(BTObject unit) { List nameCandidates = new ArrayList<>(); - - String sanitizedChassis = sanitize(unit.generalName()); String sanitizedModel = sanitize(unit.specificName()); - nameCandidates.add((sanitizedChassis + " " + sanitizedModel).trim()); - if (unit instanceof Mech && !((Mech) unit).getClanChassisName().isBlank()) { - Mech mek = (Mech) unit; - String fullChassis = sanitize(mek.getFullChassis()); - nameCandidates.add((fullChassis + " " + sanitizedModel).trim()); - String clanChassis = sanitize(mek.getClanChassisName()); - nameCandidates.add((clanChassis + " " + sanitizedModel).trim()); - nameCandidates.add(fullChassis); - nameCandidates.add(clanChassis); + for (String chassisNameCandidate : chassisNameCandidates(unit)) { + nameCandidates.add((chassisNameCandidate + " " + sanitizedModel.trim())); } - nameCandidates.add(sanitizedChassis); + nameCandidates.addAll(chassisNameCandidates(unit)); return nameCandidates; } From 494ca0de35de4a7397e0bf75a626651d26af6fc4 Mon Sep 17 00:00:00 2001 From: HammerGS Date: Sat, 25 May 2024 11:02:45 -0600 Subject: [PATCH 06/22] Merging PR's MegaMek + Fix 5476: Update SV armor slots to 2 or 1 for Rating E or F respectively MegaMekLab + Fix ##1514: Fix Atrocious table shading render speed MekHQ + PR #4106: Use tabs in customize scenario dialog + PR #4105: Updated Post-Scenario Logging for Prisoners + PR #4104: Fixed Post-Scenario Tracking System's Handling of Multiple Personnel in autoAwards + PR #4102: Fixed Award Tier Count Calculations in PersonViewPanel + PR #4093: Added Negotiation and Scrounge Skill Settings for Administrator Personnel + PR #4054: Added Life Paths Campaign Options Tab, Added Education Module Very Important to read the documentation on this feature (See Docs folder) --- megamek/docs/history.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/megamek/docs/history.txt b/megamek/docs/history.txt index 5df7809a0b0..2d11240fc1c 100644 --- a/megamek/docs/history.txt +++ b/megamek/docs/history.txt @@ -44,6 +44,7 @@ MEGAMEK VERSION HISTORY: + PR #5481: Name Changes Updates + PR #5466: Move Field of Fire Sprites out of BoardView + PR #5472: Use FlatLaf exclusively ++ Fix 5476: Update SV armor slots to 2 or 1 for Rating E or F respectively 0.49.19.1 (2024-05-14 1800 UTC) + Milestone Release. Backported fixes. From 487870522c436b7e23a07044fff7c6933c0cd0c0 Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 3 Aug 2024 09:56:45 +0200 Subject: [PATCH 07/22] show next/prev buttons for fluff images, show filename (WIP) --- .../client/ui/swing/MechViewPanel.java | 65 ++++++++++++----- .../ui/swing/util/FluffImageHelper.java | 70 ++++++++++++++++--- 2 files changed, 107 insertions(+), 28 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/MechViewPanel.java b/megamek/src/megamek/client/ui/swing/MechViewPanel.java index 4512207d7ec..bc96080e688 100644 --- a/megamek/src/megamek/client/ui/swing/MechViewPanel.java +++ b/megamek/src/megamek/client/ui/swing/MechViewPanel.java @@ -1,6 +1,6 @@ /* * MCopyright (C) 2000-2004 Ben Mazur (bmazur@sev.org) - * Copyright (c) 2021 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2021, 2024 - The MegaMek Team. All Rights Reserved. * * This file is part of MegaMek. * @@ -21,7 +21,6 @@ import megamek.client.ui.swing.util.FluffImageHelper; import megamek.client.ui.swing.util.UIUtil; -import megamek.client.ui.swing.util.UIUtil.FixedXPanel; import megamek.common.Entity; import megamek.common.MechView; import megamek.common.Report; @@ -37,18 +36,18 @@ /** * @author Jay Lawson - * @since November 2, 2009 */ public class MechViewPanel extends JPanel { - private static final long serialVersionUID = 2438490306644271135L; - private final JTextPane txtMek = new JTextPane(); private JScrollPane scrMek; private final JLabel fluffImageLabel = new JLabel(); - private final List fluffImageList = new ArrayList<>(); + private final List fluffImageList = new ArrayList<>(); private int fluffImageIndex = 0; + private final JButton nextImageButton = new JButton(" > "); + private final JButton prevImageButton = new JButton(" < "); + private final JLabel imageInfoLabel = new JLabel("", JLabel.CENTER); public static final int DEFAULT_WIDTH = 360; public static final int DEFAULT_HEIGHT = 600; @@ -77,17 +76,26 @@ public MechViewPanel(int width, int height, boolean noBorder) { scrMek.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); var textPanel = new JPanel(new GridLayout(1, 1)); + textPanel.setAlignmentY(0); textPanel.setMinimumSize(new Dimension(width, height)); textPanel.setPreferredSize(new Dimension(width, height)); textPanel.add(scrMek); - var fluffPanel = new FixedXPanel(); - fluffPanel.setMinimumSize(new Dimension(width, height)); - fluffPanel.setPreferredSize(new Dimension(width, height)); + var imageControlsPanel = new UIUtil.FixedYPanel(new FlowLayout()); + imageControlsPanel.add(prevImageButton); + imageControlsPanel.add(nextImageButton); + + imageControlsPanel.setAlignmentX(0.5f); + fluffImageLabel.setAlignmentX(0.5f); + imageInfoLabel.setAlignmentX(0.5f); + + Box fluffPanel = Box.createVerticalBox(); + fluffPanel.setAlignmentY(0); + fluffPanel.add(imageControlsPanel); fluffPanel.add(fluffImageLabel); + fluffPanel.add(imageInfoLabel); - JPanel p = new JPanel(); - p.setLayout(new BoxLayout(p, BoxLayout.LINE_AXIS)); + Box p = Box.createHorizontalBox(); p.add(textPanel); p.add(fluffPanel); p.add(Box.createHorizontalGlue()); @@ -97,6 +105,8 @@ public MechViewPanel(int width, int height, boolean noBorder) { addMouseWheelListener(wheelForwarder); fluffImageLabel.addMouseListener(mouseListener); + nextImageButton.addActionListener(e -> showNextFluffImage()); + prevImageButton.addActionListener(e -> showPrevFluffImage()); } public void setMech(Entity entity, MechView mechView) { @@ -117,6 +127,7 @@ public void setMech(Entity entity, TROView troView) { setFluffImage(entity); } + @SuppressWarnings("unused") // Used in MHQ public void setMech(Entity entity, boolean useAlternateCost) { MechView mechView = new MechView(entity, false, useAlternateCost); setMech(entity, mechView); @@ -129,9 +140,11 @@ public void setMech(Entity entity, String fontName) { private void setFluffImage(Entity entity) { fluffImageList.clear(); - fluffImageList.addAll(FluffImageHelper.getFluffImages(entity)); + fluffImageList.addAll(FluffImageHelper.getFluffRecords(entity)); fluffImageIndex = 0; - setNextFluffImage(); + nextImageButton.setEnabled(fluffImageList.size() > 1); + prevImageButton.setEnabled(fluffImageList.size() > 1); + showNextFluffImage(); } private void setFluffImage(Image image) { @@ -148,7 +161,8 @@ private void setFluffImage(Image image) { public void reset() { txtMek.setText(""); - fluffImageLabel.setIcon(null); + fluffImageList.clear(); + setFluffImage((Entity) null); } /** Forwards a mouse wheel scroll on the fluff image or free space to the TRO entry. */ @@ -162,19 +176,32 @@ public void reset() { private final MouseListener mouseListener = new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { - setNextFluffImage(); + showNextFluffImage(); } }; - private void setNextFluffImage() { - fluffImageIndex++; + private void showNextFluffImage() { + changeFluffImageIndex(1); + } + + private void showPrevFluffImage() { + changeFluffImageIndex(-1); + } + + private void changeFluffImageIndex(int delta) { + fluffImageIndex += delta; if (fluffImageIndex >= fluffImageList.size()) { fluffImageIndex = 0; } - if (fluffImageIndex < fluffImageList.size()) { - setFluffImage(fluffImageList.get(fluffImageIndex)); + if (fluffImageIndex < 0) { + fluffImageIndex = fluffImageList.size() - 1; + } + if ((fluffImageIndex >= 0) && (fluffImageIndex < fluffImageList.size())) { + setFluffImage(fluffImageList.get(fluffImageIndex).image()); + imageInfoLabel.setText(fluffImageList.get(fluffImageIndex).fileName()); } else { setFluffImage((Image) null); + imageInfoLabel.setText(""); } } } \ No newline at end of file diff --git a/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java b/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java index 4a26d768eb3..00b53892b74 100644 --- a/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java +++ b/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java @@ -33,14 +33,11 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; +import java.util.*; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; -import static java.util.stream.Collectors.toList; - /** * This class provides methods for retrieving fluff images, for use in MM, MML and MHQ; also * for record sheets (where the fallback image "hud.png" is used). @@ -105,10 +102,34 @@ public final class FluffImageHelper { * @param unit The unit * @return a fluff image or null, if no match is found */ - public static @Nullable List getFluffImages(@Nullable BTObject unit) { + public static List getFluffImages(@Nullable BTObject unit) { return getFluffImageList(unit, false); } + /** + * Returns a list of all fluff images for the given unit/object to be shown e.g. in the + * unit summary. + * + *

If a fluff image is stored in the unit/object itself, e.g. if it was part of the + * unit's file or is created by the unit itself, only this is returned. Note that this + * is not used for canon units, but may be used in custom ones by adding a fluff + * image to the unit in MML. + * + *

Otherwise, the fluff image directories are searched. First searches the user dir, + * then the internal dir. Tries to match the image by chassis + model or chassis alone. + * Chassis and model names are cleaned from " and / characters before matching. For + * Meks with clan names, both names and the combinations are searched. The model + * alone is not used to search. + * + * Returns null if no fluff image can be found. + * + * @param unit The unit + * @return a fluff image or null, if no match is found + */ + public static List getFluffRecords(@Nullable BTObject unit) { + return getFluffImageRecords(unit, false); + } + /** * Returns a fluff image for the given unit for the record sheet, with a fallback * file named "hud.png" if that is present in the right fluff directory, or null if nothing @@ -136,6 +157,27 @@ public final class FluffImageHelper { } } + /** + * Returns a list of available fluff images. If a fluff image is embedded in the unit file, + * only that image is returned, even if others are available from the fluff directories. The returned + * list may be empty, but not null. + * + * @param unit The unit + * @param recordSheet True if this image search is meant for a record sheet + * @return Available fluff images or the embedded fluff image + */ + private static List getFluffImageRecords(@Nullable BTObject unit, boolean recordSheet) { + if (unit == null) { + return new ArrayList<>(); + } + Image embeddedFluffImage = unit.getFluffImage(); + if (embeddedFluffImage != null) { + return List.of(new FluffImageRecord(embeddedFluffImage, "")); + } else { + return findFluffFiles(unit, recordSheet).stream().map(FluffImageRecord::toRecord).toList(); + } + } + /** * Returns a list of available fluff images. If a fluff image is embedded in the unit file, * only that image is returned, even if others are available from the fluff directories. The returned @@ -161,8 +203,8 @@ private static List getFluffImageList(@Nullable BTObject unit, boolean re } } - private static @Nullable List findFluffFiles(BTObject unit, boolean recordSheet) { - List fileCandidates = new ArrayList<>(); + private static Set findFluffFiles(BTObject unit, boolean recordSheet) { + Set fileCandidates = new HashSet<>(); var fluffDir = new File(Configuration.fluffImagesDir(), FluffImageHelper.getFluffPath(unit)); List nameCandidates = nameCandidates(unit); @@ -194,7 +236,6 @@ private static List getFluffImageList(@Nullable BTObject unit, boolean re } fileCandidates.addAll(getFluffInChassisDirs(unit, fluffDir)); - fileCandidates.removeIf(f -> !f.exists() || f.isDirectory()); return fileCandidates; } @@ -264,7 +305,7 @@ private static List getFluffInChassisDir(BTObject unit, File chassisDir) { private static List getFluffInDir(File dir) { List result = new ArrayList<>(); try (Stream entries = Files.walk(dir.toPath())) { - result.addAll(entries.map(Objects::toString).map(File::new).collect(toList())); + result.addAll(entries.map(Objects::toString).map(File::new).toList()); } catch (IOException e) { LogManager.getLogger().warn("Error while reading files from " + dir, e); } @@ -321,4 +362,15 @@ public static String getFluffPath(BTObject unit) { return DIR_NAME_MECH; } } + + public record FluffImageRecord(Image image, String fileName) { + + public static FluffImageRecord toRecord(File file) { + return toRecord(file.toString()); + } + + public static FluffImageRecord toRecord(String fileName) { + return new FluffImageRecord(new ImageIcon(fileName).getImage(), fileName); + } + } } \ No newline at end of file From 890ac0f629417ebda8f1512e3ee226d29cc2c2c1 Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 3 Aug 2024 10:04:26 +0200 Subject: [PATCH 08/22] remove image click mouse listener --- megamek/src/megamek/client/ui/swing/MechViewPanel.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/MechViewPanel.java b/megamek/src/megamek/client/ui/swing/MechViewPanel.java index bc96080e688..5a225922de7 100644 --- a/megamek/src/megamek/client/ui/swing/MechViewPanel.java +++ b/megamek/src/megamek/client/ui/swing/MechViewPanel.java @@ -104,7 +104,6 @@ public MechViewPanel(int width, int height, boolean noBorder) { add(sp); addMouseWheelListener(wheelForwarder); - fluffImageLabel.addMouseListener(mouseListener); nextImageButton.addActionListener(e -> showNextFluffImage()); prevImageButton.addActionListener(e -> showPrevFluffImage()); } @@ -173,13 +172,6 @@ public void reset() { } }; - private final MouseListener mouseListener = new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - showNextFluffImage(); - } - }; - private void showNextFluffImage() { changeFluffImageIndex(1); } From 60b69b66dc5617642ca7ad260e2afe960ec5a485 Mon Sep 17 00:00:00 2001 From: HammerGS Date: Mon, 5 Aug 2024 21:29:53 -0600 Subject: [PATCH 09/22] Fixing Destrier has Sponson Equipment in Inconsistent Location #5865 --- .../3145/Davion/Destrier Siege Vehicle.blk | 65 +++++++++++++------ 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/megamek/data/mechfiles/vehicles/3145/Davion/Destrier Siege Vehicle.blk b/megamek/data/mechfiles/vehicles/3145/Davion/Destrier Siege Vehicle.blk index 9d7516ceb82..624e2321bc1 100644 --- a/megamek/data/mechfiles/vehicles/3145/Davion/Destrier Siege Vehicle.blk +++ b/megamek/data/mechfiles/vehicles/3145/Davion/Destrier Siege Vehicle.blk @@ -1,13 +1,4 @@ -#building block data file - -1 - - -# Write the version number just in case... - -MAM0 - - +#Saved from version 0.50.0-SNAPSHOT on 2024-08-05 Tank @@ -16,9 +7,9 @@ Tank Destrier Siege Vehicle - + - + 6329 @@ -28,6 +19,10 @@ Destrier Siege Vehicle 3100 + +3100 + + IS Level 3 @@ -36,6 +31,13 @@ IS Level 3 Sniper + +imp_target_long +oversized +non_standard +poor_performance + + Tracked @@ -71,8 +73,9 @@ troopspace:4.0 +SponsonTurret ISCASE -Communications Equipment (1 ton) +Communications Equipment:SIZE:1.0 ISGuardianECMSuite ISC3SlaveUnit ISAMS Ammo @@ -97,7 +100,6 @@ IS Light Machine Gun Ammo - Full ISAntiMissileSystem(ST) Light Machine Gun(ST) Light Machine Gun(ST) -ISSponsonTurret @@ -127,6 +129,34 @@ ISERMediumLaser ISAntiMissileSystem + +Twice the mass of a conventional assault tank, wrapped in almost forty tons of hardened armor, and armed with a pair of turreted Long Tom assault cannons, the Destrier excels at reducing enemy fortifications to dust within minutes of its arrival. Heavy defensive weaponry and thick armor just barely compensate for its slow speed, however. + + + +Despite the formation of its light combat teams and its public declarations of defensive doctrine, the adoption of the Destrier Siege Vehicle at the turn of the thirty-second century proved that the AFFS hadn’t completely rejected offensive warfare. The Federated Suns' involvement in the Victoria War, where it acted as the aggressor, demonstrated to the Inner Sphere that it had not forgotten Hanse Davion’s lessons. + + + +Given the relative scarcity of AFFS invasions, the most notable engagement to feature the Destrier came on Spica in 3104. The Eighth Avalon Hussars light combat team, the world’s garrison, was attacked by the Third McCarron’s Armored Cavalry. Unable to stand against the Third directly, the Eighth planned an ambush with the company of Destriers assigned to it. Using her ’Mechs as bait and her hovertanks as harassers, General Stasi Ivanova led the Capellan vanguard into a valley and destroyed it with concentrated Long Tom fire. As missile artillery began to fall from the Destriers’ attached Ballista trailers, the Hussars ’Mechs counterattacked and drove the Capellans into retreat. +

Several times during the Kuritan invasion, Destriers and their crews have been sacrificed—voluntarily or not—as forlorn hopes to let battered Davion units escape. Too slow to retreat and too large to be easily evacuated, the Destriers make fine last-line holders. On Barlow’s End, for instance, a platoon of Destriers set itself outside the gates of the Princess Melissa spaceport. As the DCMS advanced, the Second New Ivaarsen Chasseurs withdrew to their DropShips. A small group of lottery-chosen VTOLs and hovertanks remained to designate targets for the Ballistas’ Arrow IV missiles, while the Destriers hid themselves among the spaceport buildings. As the lead ’Mechs of Ryuken-roku advanced, the Destriers fired from cover, causing enough confusion that the DropShips could escape. None of the Destrier crews survived to be captured.

+ + + +General Motors + + + +Salem + + + +ENGINE:GM SuperLoad 400 XL Fusion +ARMOR:Johnston Bastion Plate 530 Hardened with CASE +COMMUNICATIONS:Electronics HICS-17 with C3 Slave and Guardian ECM Suite +TARGETING:Federated Bombard Mk. II + + TRO: 3145 Federated Suns @@ -135,10 +165,3 @@ TRO: 3145 Federated Suns 200.0 - -imp_target_long -oversized -poor_performance -non_standard - - From 0985559d5688bac1835f7582eeeff1b263603f56 Mon Sep 17 00:00:00 2001 From: HammerGS Date: Fri, 9 Aug 2024 09:20:17 -0600 Subject: [PATCH 10/22] Add a doc on AI usage (not Princess related) to MegaMek --- ...and MegaMek Suite (Not Princess Stuff).txt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 megamek/docs/1-Readme/AI and MegaMek Suite (Not Princess Stuff).txt diff --git a/megamek/docs/1-Readme/AI and MegaMek Suite (Not Princess Stuff).txt b/megamek/docs/1-Readme/AI and MegaMek Suite (Not Princess Stuff).txt new file mode 100644 index 00000000000..38f6bf39a92 --- /dev/null +++ b/megamek/docs/1-Readme/AI and MegaMek Suite (Not Princess Stuff).txt @@ -0,0 +1,20 @@ +Disclaimer +Version 1.0 | Date: August 9th, 2024 + +Content Creation Notice + +Some of the lore and narrative content within MegaMek has been generated using OpenAI's ChatGPT. This AI-assisted content creation aims to enhance the game's depth and provide players with a richer experience. + +Important Points to Note: + +Accuracy and Consistency: While we strive to maintain the accuracy and consistency of the BattleTech universe, there may be occasional deviations or errors in the AI-generated content. We appreciate your understanding and welcome feedback to improve these elements. + +Intellectual Property: All AI-generated content is created with respect to the intellectual property holders and those that license the Tabletop properties of the BattleTech universe. However, it is important to note that this content is unofficial and not directly endorsed by these entities. Additionally, MechWarrior, BattleMech, ‘Mech, and AeroTech are registered trademarks of The Topps Company, Inc. All Rights Reserved. + +Ethical Use: We ensure that all AI-generated content adheres to ethical standards and does not contain inappropriate or sensitive material. Our team reviews and moderates this content to ensure it aligns with the community's expectations. + +Use of AI: We use AI specifically for certain art in splash screens, editing fluff text within the programs, and developing news articles for MekHQ. + +Feedback and Contributions: We value the input of our community. If you notice any issues or have suggestions for improving the AI-generated content, please reach out to us. Your feedback helps us refine and enhance the game. + +Thank you for being a part of the MegaMek community and for supporting our efforts to bring a more immersive experience to the BattleTech universe. \ No newline at end of file From 4f4c794b4407b8ec2e10c68ec3400c87345d65bc Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 10 Aug 2024 14:11:33 +0200 Subject: [PATCH 11/22] only take image files as fluff images --- .../client/ui/swing/MechViewPanel.java | 13 ++++++++++- .../ui/swing/util/FluffImageHelper.java | 23 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/MechViewPanel.java b/megamek/src/megamek/client/ui/swing/MechViewPanel.java index 5a225922de7..b9e30a97811 100644 --- a/megamek/src/megamek/client/ui/swing/MechViewPanel.java +++ b/megamek/src/megamek/client/ui/swing/MechViewPanel.java @@ -190,10 +190,21 @@ private void changeFluffImageIndex(int delta) { } if ((fluffImageIndex >= 0) && (fluffImageIndex < fluffImageList.size())) { setFluffImage(fluffImageList.get(fluffImageIndex).image()); - imageInfoLabel.setText(fluffImageList.get(fluffImageIndex).fileName()); + imageInfoLabel.setText(prepareLabelText(fluffImageList.get(fluffImageIndex).fileName())); } else { setFluffImage((Image) null); imageInfoLabel.setText(""); } } + + private String prepareLabelText(String labelInfo) { + String labelText = ""; + if (labelInfo.contains("__")) { + labelText = labelInfo.substring(labelInfo.lastIndexOf("__") + 2); + } + if (labelText.contains(".")) { + labelText = labelText.substring(0, labelText.lastIndexOf(".")); + } + return labelText; + } } \ No newline at end of file diff --git a/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java b/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java index 00b53892b74..c8a7eb3d9e1 100644 --- a/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java +++ b/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java @@ -56,8 +56,8 @@ public final class FluffImageHelper { public static final String DIR_NAME_SPACESTATION = "Space Station"; public static final String DIR_NAME_VEHICLE = "Vehicle"; public static final String DIR_NAME_WARSHIP = "WarShip"; - public static final String[] EXTENSIONS_FLUFF_IMAGE_FORMATS = { ".PNG", ".png", ".JPG", - ".JPEG", ".jpg", ".jpeg", ".GIF", ".gif" }; + public static final List EXTENSIONS_FLUFF_IMAGE_FORMATS = List.of(".PNG", ".png", ".JPG", + ".JPEG", ".jpg", ".jpeg", ".GIF", ".gif"); /** * Returns a fluff image for the given unit/object to be shown e.g. in the unit summary. @@ -306,6 +306,7 @@ private static List getFluffInDir(File dir) { List result = new ArrayList<>(); try (Stream entries = Files.walk(dir.toPath())) { result.addAll(entries.map(Objects::toString).map(File::new).toList()); + result.removeIf(FluffImageHelper::isNoImageFile); } catch (IOException e) { LogManager.getLogger().warn("Error while reading files from " + dir, e); } @@ -373,4 +374,22 @@ public static FluffImageRecord toRecord(String fileName) { return new FluffImageRecord(new ImageIcon(fileName).getImage(), fileName); } } + + private static boolean isNoImageFile(File file) { + Optional extension = getExtension(file.toString()); + return extension.isEmpty() || !EXTENSIONS_FLUFF_IMAGE_FORMATS.contains(extension.get()); + } + + /** + * Returns the file extension of a given filename. + * source: baeldung.com/java-file-extension + * + * @param filename The filename, potentially with directories + * @return The extension, including the dot + */ + public static Optional getExtension(String filename) { + return Optional.ofNullable(filename) + .filter(f -> f.contains(".")) + .map(f -> f.substring(filename.lastIndexOf("."))); + } } \ No newline at end of file From 8c253aeb3d0712e4283cd9695d68c8d18a3b84a9 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 13 Aug 2024 09:47:04 +0200 Subject: [PATCH 12/22] add yaml parsed fluff tooltip to the MechViewPanel --- .../client/ui/swing/FluffImageTooltip.java | 130 ++++++++++++++++++ .../client/ui/swing/MechViewPanel.java | 19 ++- .../ui/swing/util/FluffImageHelper.java | 17 ++- 3 files changed, 156 insertions(+), 10 deletions(-) create mode 100644 megamek/src/megamek/client/ui/swing/FluffImageTooltip.java diff --git a/megamek/src/megamek/client/ui/swing/FluffImageTooltip.java b/megamek/src/megamek/client/ui/swing/FluffImageTooltip.java new file mode 100644 index 00000000000..3e30054a555 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/FluffImageTooltip.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MegaMek. + * + * MegaMek is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MegaMek is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MegaMek. If not, see . + */ +package megamek.client.ui.swing; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import megamek.client.ui.swing.util.FluffImageHelper; +import megamek.client.ui.swing.util.UIUtil; +import org.apache.logging.log4j.LogManager; + +import java.awt.*; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * This class is very specialized. It provides tooltip information for the fluff image tooltip in the + * MechViewPanel, taken from yaml files that are supplied with painted minis images. + */ +public class FluffImageTooltip { + + private static final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory()); + + public static String styles() { + int labelSize = UIUtil.scaleForGUI(UIUtil.FONT_SCALE1); + Color color = GUIPreferences.getInstance().getToolTipLightFGColor(); + String styleColor = Integer.toHexString(color.getRGB() & 0xFFFFFF); + return "span { font-family:Noto Sans; font-size:" + labelSize + "; }" + + ".label { color:" + styleColor + "; }"; + } + + /** + * Returns the tooltip text for the supplied FluffImageRecord, if any can be found, null otherwise. + * + * @param record The FluffImageRecord that is currently shown as an image + * @return A tooltip text or null if no yaml info is available + */ + public static String getTooltip(FluffImageHelper.FluffImageRecord record) { + return findYamlInfo(record).map(FluffImageTooltip::getTooltip).orElse(null); + } + + private static Optional findYamlInfo(FluffImageHelper.FluffImageRecord record) { + return (record.file() == null) ? Optional.empty() : getYamlFile(record.file()); + } + + private static String getTooltip(File yamlFile) { + try { + JsonNode node = yamlMapper.readTree(yamlFile); + + StringBuilder result = new StringBuilder(""); + int width = UIUtil.scaleForGUI(400); + result.append("
"); + + if (node.has("title")) { + String unit = node.get("title").asText(); + if (!unit.isBlank()) { + result.append(UIUtil.spanCSS("label", "Unit: ")) + .append(UIUtil.spanCSS("value", unit)); + } + } + if (node.has("author")) { + String artist = node.get("author").asText(); + if (!artist.isBlank()) { + result.append(UIUtil.spanCSS("label", "
Artist: ")) + .append(UIUtil.spanCSS("value", artist)); + } + } + if (node.has("content")) { + JsonNode contentNode = node.get("content"); + String description = findInsignia(contentNode); + if (!description.isBlank()) { + result.append(UIUtil.spanCSS("label", "
Insignia: ")) + .append(UIUtil.spanCSS("value", description)); + } + } + result.append("
"); + return result.toString(); + } catch (IOException e) { + return null; + } + } + + private static String findInsignia(JsonNode contentNode) { + List nodes = new ArrayList<>(); + contentNode.iterator().forEachRemaining(nodes::add); + for (JsonNode node : nodes) { + if (node.has("type") && node.get("type").asText().equals("insignia")) { + return node.get("content").asText(); + } + } + return ""; + } + + private static Optional getYamlFile(File imageFile) { + try (Stream entries = Files.walk(imageFile.getParentFile().toPath())) { + return entries.filter(p -> isSuitableYamlFile(p, imageFile)).map(Path::toFile).findFirst(); + } catch (Exception e) { + LogManager.getLogger().warn("Error while reading files from {}", imageFile.getParentFile(), e); + return Optional.empty(); + } + } + + private static boolean isSuitableYamlFile(Path yamlFile, File imageFile) { + String yamlFileName = yamlFile.getFileName().toString(); + return !Files.isDirectory(yamlFile) && yamlFileName.endsWith("data.yaml") + && imageFile.getName().contains(yamlFileName.substring(0, yamlFileName.length() - 9)); + } +} diff --git a/megamek/src/megamek/client/ui/swing/MechViewPanel.java b/megamek/src/megamek/client/ui/swing/MechViewPanel.java index b9e30a97811..5ae71e32a90 100644 --- a/megamek/src/megamek/client/ui/swing/MechViewPanel.java +++ b/megamek/src/megamek/client/ui/swing/MechViewPanel.java @@ -26,6 +26,8 @@ import megamek.common.Report; import megamek.common.templates.TROView; +import java.io.File; +import java.io.IOException; import java.util.List; import javax.swing.*; import javax.swing.border.EmptyBorder; @@ -155,6 +157,7 @@ private void setFluffImage(Image image) { fluffImageLabel.setIcon(new ImageIcon(image)); } else { fluffImageLabel.setIcon(null); + fluffImageLabel.setToolTipText(null); } } @@ -164,7 +167,7 @@ public void reset() { setFluffImage((Entity) null); } - /** Forwards a mouse wheel scroll on the fluff image or free space to the TRO entry. */ + /** Forwards a mouse wheel scroll on the fluff image or free space to the TRO entry. */ private final MouseWheelListener wheelForwarder = e -> { MouseWheelEvent converted = (MouseWheelEvent) SwingUtilities.convertMouseEvent(MechViewPanel.this, e, scrMek); for (MouseWheelListener listener : scrMek.getMouseWheelListeners()) { @@ -189,16 +192,24 @@ private void changeFluffImageIndex(int delta) { fluffImageIndex = fluffImageList.size() - 1; } if ((fluffImageIndex >= 0) && (fluffImageIndex < fluffImageList.size())) { - setFluffImage(fluffImageList.get(fluffImageIndex).image()); - imageInfoLabel.setText(prepareLabelText(fluffImageList.get(fluffImageIndex).fileName())); + try { + FluffImageHelper.FluffImageRecord record = fluffImageList.get(fluffImageIndex); + setFluffImage(record.getImage()); + imageInfoLabel.setText(prepareLabelText(record.file())); + fluffImageLabel.setToolTipText(FluffImageTooltip.getTooltip(record)); + } catch (IOException ex) { + setFluffImage((Image) null); + imageInfoLabel.setText("Error loading fluff image"); + } } else { setFluffImage((Image) null); imageInfoLabel.setText(""); } } - private String prepareLabelText(String labelInfo) { + private String prepareLabelText(File file) { String labelText = ""; + String labelInfo = file.toString(); if (labelInfo.contains("__")) { labelText = labelInfo.substring(labelInfo.lastIndexOf("__") + 2); } diff --git a/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java b/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java index c8a7eb3d9e1..88009538627 100644 --- a/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java +++ b/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java @@ -27,6 +27,7 @@ import megamek.common.preference.PreferenceManager; import org.apache.logging.log4j.LogManager; +import javax.imageio.ImageIO; import javax.swing.*; import java.awt.*; import java.io.File; @@ -163,7 +164,7 @@ public static List getFluffRecords(@Nullable BTObject unit) { * list may be empty, but not null. * * @param unit The unit - * @param recordSheet True if this image search is meant for a record sheet + * @param recordSheet True if this image search is meant for a record sheet (used in MML) * @return Available fluff images or the embedded fluff image */ private static List getFluffImageRecords(@Nullable BTObject unit, boolean recordSheet) { @@ -172,7 +173,7 @@ private static List getFluffImageRecords(@Nullable BTObject un } Image embeddedFluffImage = unit.getFluffImage(); if (embeddedFluffImage != null) { - return List.of(new FluffImageRecord(embeddedFluffImage, "")); + return List.of(new FluffImageRecord(embeddedFluffImage, null)); } else { return findFluffFiles(unit, recordSheet).stream().map(FluffImageRecord::toRecord).toList(); } @@ -364,14 +365,18 @@ public static String getFluffPath(BTObject unit) { } } - public record FluffImageRecord(Image image, String fileName) { + public record FluffImageRecord(Image image, File file) { public static FluffImageRecord toRecord(File file) { - return toRecord(file.toString()); + return new FluffImageRecord(null, file); } - public static FluffImageRecord toRecord(String fileName) { - return new FluffImageRecord(new ImageIcon(fileName).getImage(), fileName); + public Image getImage() throws IOException { + if (image != null) { + return image; + } else { + return ImageIO.read(file); + } } } From 4d1af5c49a1f4dce0879bc1548f5aa3337376e67 Mon Sep 17 00:00:00 2001 From: Simon Date: Sun, 18 Aug 2024 22:37:50 +0200 Subject: [PATCH 13/22] show placeholder for units without fluff --- .../data/images/fluff/fluff_placeholder.png | Bin 0 -> 71456 bytes .../client/ui/swing/MechViewPanel.java | 21 ++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 megamek/data/images/fluff/fluff_placeholder.png diff --git a/megamek/data/images/fluff/fluff_placeholder.png b/megamek/data/images/fluff/fluff_placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..2bc7c75e5f9ebb2ef71137ade29128f3728f67e2 GIT binary patch literal 71456 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 z 1); prevImageButton.setEnabled(fluffImageList.size() > 1); - showNextFluffImage(); + if (entity != null) { + showNextFluffImage(); + } else { + setFluffImage((Image) null); + } } private void setFluffImage(Image image) { @@ -202,7 +211,7 @@ private void changeFluffImageIndex(int delta) { imageInfoLabel.setText("Error loading fluff image"); } } else { - setFluffImage((Image) null); + setFluffImage(PLACEHOLDER_IMAGE); imageInfoLabel.setText(""); } } @@ -218,4 +227,12 @@ private String prepareLabelText(File file) { } return labelText; } + + private static Image readPlaceHolderImage() { + try { + return ImageIO.read(new File(PLACEHOLDER_IMAGE_NAME)); + } catch (IOException e) { + return null; + } + } } \ No newline at end of file From 15eef9a4625064f84b343f738cf7294163d01485 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 23 Aug 2024 09:57:49 +0200 Subject: [PATCH 14/22] show info below fluff image --- megamek/src/megamek/client/ui/swing/FluffImageTooltip.java | 2 +- megamek/src/megamek/client/ui/swing/MechViewPanel.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/megamek/src/megamek/client/ui/swing/FluffImageTooltip.java b/megamek/src/megamek/client/ui/swing/FluffImageTooltip.java index 3e30054a555..c7a73808749 100644 --- a/megamek/src/megamek/client/ui/swing/FluffImageTooltip.java +++ b/megamek/src/megamek/client/ui/swing/FluffImageTooltip.java @@ -70,7 +70,7 @@ private static String getTooltip(File yamlFile) { JsonNode node = yamlMapper.readTree(yamlFile); StringBuilder result = new StringBuilder(""); - int width = UIUtil.scaleForGUI(400); + int width = UIUtil.scaleForGUI(360); result.append("
"); if (node.has("title")) { diff --git a/megamek/src/megamek/client/ui/swing/MechViewPanel.java b/megamek/src/megamek/client/ui/swing/MechViewPanel.java index 25163901688..56f570c3162 100644 --- a/megamek/src/megamek/client/ui/swing/MechViewPanel.java +++ b/megamek/src/megamek/client/ui/swing/MechViewPanel.java @@ -100,6 +100,7 @@ public MechViewPanel(int width, int height, boolean noBorder) { fluffPanel.setAlignmentY(0); fluffPanel.add(imageControlsPanel); fluffPanel.add(fluffImageLabel); + fluffPanel.add(Box.createVerticalStrut(10)); fluffPanel.add(imageInfoLabel); Box p = Box.createHorizontalBox(); @@ -206,6 +207,7 @@ private void changeFluffImageIndex(int delta) { setFluffImage(record.getImage()); imageInfoLabel.setText(prepareLabelText(record.file())); fluffImageLabel.setToolTipText(FluffImageTooltip.getTooltip(record)); + imageInfoLabel.setText(FluffImageTooltip.getTooltip(record)); } catch (IOException ex) { setFluffImage((Image) null); imageInfoLabel.setText("Error loading fluff image"); From d5953a3bf09dbae50be8cdc00c0408afe8c8341b Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 27 Feb 2025 18:18:39 +0100 Subject: [PATCH 15/22] after merge changes --- .../megamek/client/ui/swing/MekViewPanel.java | 20 +----- .../ui/swing/util/FluffImageHelper.java | 70 ++++++------------- 2 files changed, 26 insertions(+), 64 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/MekViewPanel.java b/megamek/src/megamek/client/ui/swing/MekViewPanel.java index 4ee12d841e6..f0eabe1ddd0 100644 --- a/megamek/src/megamek/client/ui/swing/MekViewPanel.java +++ b/megamek/src/megamek/client/ui/swing/MekViewPanel.java @@ -28,7 +28,6 @@ import java.awt.event.MouseWheelListener; import javax.swing.Box; -import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.JPanel; @@ -51,10 +50,7 @@ import java.util.List; import javax.imageio.ImageIO; import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.event.HyperlinkEvent; import java.awt.*; -import java.awt.event.*; import java.util.ArrayList; /** @@ -76,7 +72,6 @@ public class MekViewPanel extends JPanel { private final JLabel imageInfoLabel = new JLabel("", JLabel.CENTER); public static final int DEFAULT_WIDTH = 360; - public static final int DEFAULT_HEIGHT = 600; public MekViewPanel() { this(-1, -1, true); @@ -104,9 +99,6 @@ public MekViewPanel(int width, int height, boolean noBorder) { scrMek.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); var textPanel = new JPanel(new GridLayout(1, 1)); - textPanel.setAlignmentY(0); - textPanel.setMinimumSize(new Dimension(width, height)); - textPanel.setPreferredSize(new Dimension(width, height)); if (width != -1) { textPanel.setMinimumSize(new Dimension(width, height)); textPanel.setPreferredSize(new Dimension(width, height)); @@ -120,12 +112,6 @@ public MekViewPanel(int width, int height, boolean noBorder) { imageControlsPanel.setAlignmentX(0.5f); fluffImageLabel.setAlignmentX(0.5f); imageInfoLabel.setAlignmentX(0.5f); - var fluffPanel = new FixedXPanel(); - if (width != -1) { - fluffPanel.setMinimumSize(new Dimension(width, height)); - fluffPanel.setPreferredSize(new Dimension(width, height)); - } - fluffPanel.add(lblMek); Box fluffPanel = Box.createVerticalBox(); fluffPanel.setAlignmentY(0); @@ -165,9 +151,9 @@ public void setMek(Entity entity, TROView troView) { } @SuppressWarnings("unused") // Used in MHQ - public void setMech(Entity entity, boolean useAlternateCost) { - MechView mechView = new MechView(entity, false, useAlternateCost); - setMech(entity, mechView); + public void setMek(Entity entity, boolean useAlternateCost) { + MekView mechView = new MekView(entity, false, useAlternateCost); + setMek(entity, mechView); } public void setMek(Entity entity, String fontName) { diff --git a/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java b/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java index 840198a0cba..3a3ad5db093 100644 --- a/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java +++ b/megamek/src/megamek/client/ui/swing/util/FluffImageHelper.java @@ -35,14 +35,10 @@ import org.apache.logging.log4j.LogManager; import javax.imageio.ImageIO; -import javax.swing.*; -import java.awt.*; -import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; -import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -69,26 +65,16 @@ public final class FluffImageHelper { ".JPEG", ".jpg", ".jpeg", ".GIF", ".gif"); /** - * Returns a fluff image for the given unit/object to be shown e.g. in the unit - * summary. + * Returns a fluff image for the given unit/object to be shown e.g. in the unit summary. * *

- * If a fluff image is stored in the unit/object itself, e.g. if it was part of - * the - * unit's file or is created by the unit itself, this is returned. Note that - * this - * is not used for canon units, but may be used in custom ones by adding a fluff - * image to the unit in MML. + * If a fluff image is stored in the unit/object itself, e.g. if it was part of the unit's file or is created by the unit itself, this + * is returned. Note that this is not used for canon units, but may be used in custom ones by adding a fluff image to the unit in MML. * *

- * Otherwise, the fluff images directories are searched. First searches the user - * dir, - * then the internal dir. Tries to match the image by chassis + model or chassis - * alone. - * Chassis and model names are cleaned from " and / characters before matching. - * For - * Meks with clan names, both names and the combinations are searched. The model - * alone is not used to search. + * Otherwise, the fluff images directories are searched. First searches the user dir, then the internal dir. Tries to match the image by + * chassis + model or chassis alone. Chassis and model names are cleaned from " and / characters before matching. For Meks with clan + * names, both names and the combinations are searched. The model alone is not used to search. * * Returns null if no fluff image can be found. * @@ -100,19 +86,16 @@ public final class FluffImageHelper { } /** - * Returns a list of all fluff images for the given unit/object to be shown e.g. in the - * unit summary. + * Returns a list of all fluff images for the given unit/object to be shown e.g. in the unit summary. * *

If a fluff image is stored in the unit/object itself, e.g. if it was part of the - * unit's file or is created by the unit itself, only this is returned. Note that this - * is not used for canon units, but may be used in custom ones by adding a fluff - * image to the unit in MML. + * unit's file or is created by the unit itself, only this is returned. Note that this is not used for canon units, but may be used in + * custom ones by adding a fluff image to the unit in MML. * *

Otherwise, the fluff image directories are searched. First searches the user dir, - * then the internal dir. Tries to match the image by chassis + model or chassis alone. - * Chassis and model names are cleaned from " and / characters before matching. For - * Meks with clan names, both names and the combinations are searched. The model - * alone is not used to search. + * then the internal dir. Tries to match the image by chassis + model or chassis alone. Chassis and model names are cleaned from " and / + * characters before matching. For Meks with clan names, both names and the combinations are searched. The model alone is not used to + * search. * * Returns null if no fluff image can be found. * @@ -124,19 +107,16 @@ public static List getFluffImages(@Nullable BTObject unit) { } /** - * Returns a list of all fluff images for the given unit/object to be shown e.g. in the - * unit summary. + * Returns a list of all fluff images for the given unit/object to be shown e.g. in the unit summary. * *

If a fluff image is stored in the unit/object itself, e.g. if it was part of the - * unit's file or is created by the unit itself, only this is returned. Note that this - * is not used for canon units, but may be used in custom ones by adding a fluff - * image to the unit in MML. + * unit's file or is created by the unit itself, only this is returned. Note that this is not used for canon units, but may be used in + * custom ones by adding a fluff image to the unit in MML. * *

Otherwise, the fluff image directories are searched. First searches the user dir, - * then the internal dir. Tries to match the image by chassis + model or chassis alone. - * Chassis and model names are cleaned from " and / characters before matching. For - * Meks with clan names, both names and the combinations are searched. The model - * alone is not used to search. + * then the internal dir. Tries to match the image by chassis + model or chassis alone. Chassis and model names are cleaned from " and / + * characters before matching. For Meks with clan names, both names and the combinations are searched. The model alone is not used to + * search. * * Returns null if no fluff image can be found. * @@ -148,13 +128,9 @@ public static List getFluffRecords(@Nullable BTObject unit) { } /** - * Returns a fluff image for the given unit for the record sheet, with a - * fallback - * file named "hud.png" if that is present in the right fluff directory, or null - * if nothing - * can be found. See {@link #getFluffImage(BTObject)} for further comments on - * how the fluff - * image is searched. + * Returns a fluff image for the given unit for the record sheet, with a fallback file named "hud.png" if that is present in the right + * fluff directory, or null if nothing can be found. See {@link #getFluffImage(BTObject)} for further comments on how the fluff image is + * searched. * * @param unit The unit * @return a fluff image or null, if no match is found @@ -294,8 +270,8 @@ private static List chassisNameCandidates(BTObject unit) { List result = new ArrayList<>(); String sanitizedChassis = sanitize(unit.generalName()); result.add(sanitizedChassis); - if ((unit instanceof Mech) && !((Mech) unit).getClanChassisName().isBlank()) { - String sanitizedClanChassis = sanitize(((Mech) unit).getClanChassisName()); + if ((unit instanceof Mek) && !((Mek) unit).getClanChassisName().isBlank()) { + String sanitizedClanChassis = sanitize(((Mek) unit).getClanChassisName()); result.add(sanitizedClanChassis + " (" + sanitizedChassis + ")"); result.add(sanitizedChassis + " (" + sanitizedClanChassis + ")"); result.add(sanitizedClanChassis); From 7033c71cabe6b5fdfcb04273107394b02c54a56a Mon Sep 17 00:00:00 2001 From: HammerGS Date: Fri, 28 Feb 2025 20:53:22 -0700 Subject: [PATCH 16/22] Updating history.txts --- megamek/docs/history.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/megamek/docs/history.txt b/megamek/docs/history.txt index 5a719163b2a..6541ba5b2df 100644 --- a/megamek/docs/history.txt +++ b/megamek/docs/history.txt @@ -65,6 +65,7 @@ MEGAMEK VERSION HISTORY: + PR #6606: Improve Deployment-Phase Towing Logic + PR #6611: Add VTOL/WiGE exemption for AAA and LAA missiles + PR #6505: Remove XStream from Image Atlas Generation ++ PR #6626: Fix: ejected crew, ignored targets and broken enemies wont be target of physical attacks anymore 0.50.03 (2025-02-02 2030 UTC) + PR #6335: default the directory filter to Select All in Advanced Board Search From d72c30ed4410ba43e78d0740f583d2afc4c7175a Mon Sep 17 00:00:00 2001 From: SJuliez Date: Wed, 15 Oct 2025 17:48:03 +0200 Subject: [PATCH 17/22] aftermerge corrections --- .../data/images/fluff/fluff_placeholder.png | Bin 71456 -> 0 bytes .../megamek/client/ui/FluffImageTooltip.java | 7 +- .../EntityReadoutPanel.java | 150 +++++++++- .../megamek/client/ui/swing/MekViewPanel.java | 256 ------------------ .../client/ui/util/FluffImageHelper.java | 1 + 5 files changed, 141 insertions(+), 273 deletions(-) delete mode 100644 megamek/data/images/fluff/fluff_placeholder.png delete mode 100644 megamek/src/megamek/client/ui/swing/MekViewPanel.java diff --git a/megamek/data/images/fluff/fluff_placeholder.png b/megamek/data/images/fluff/fluff_placeholder.png deleted file mode 100644 index 2bc7c75e5f9ebb2ef71137ade29128f3728f67e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71456 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 z. */ -package megamek.client.ui.swing; +package megamek.client.ui; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import megamek.client.ui.swing.util.FluffImageHelper; -import megamek.client.ui.swing.util.UIUtil; +import megamek.client.ui.clientGUI.GUIPreferences; +import megamek.client.ui.util.FluffImageHelper; +import megamek.client.ui.util.UIUtil; import org.apache.logging.log4j.LogManager; import java.awt.*; diff --git a/megamek/src/megamek/client/ui/dialogs/unitSelectorDialogs/EntityReadoutPanel.java b/megamek/src/megamek/client/ui/dialogs/unitSelectorDialogs/EntityReadoutPanel.java index 2047c6ff1ca..ccad7d3b384 100644 --- a/megamek/src/megamek/client/ui/dialogs/unitSelectorDialogs/EntityReadoutPanel.java +++ b/megamek/src/megamek/client/ui/dialogs/unitSelectorDialogs/EntityReadoutPanel.java @@ -36,16 +36,22 @@ import java.awt.BorderLayout; import java.awt.ComponentOrientation; import java.awt.Dimension; +import java.awt.FlowLayout; import java.awt.GridLayout; import java.awt.Image; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; +import javax.imageio.ImageIO; import javax.swing.Box; -import javax.swing.BoxLayout; import javax.swing.ImageIcon; +import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; @@ -57,12 +63,13 @@ import javax.swing.text.html.HTML; import javax.swing.text.html.HTMLDocument; +import megamek.client.ui.FluffImageTooltip; import megamek.client.ui.entityreadout.EntityReadout; import megamek.client.ui.entityreadout.ReadoutSections; import megamek.client.ui.util.FluffImageHelper; import megamek.client.ui.util.UIUtil; -import megamek.client.ui.util.UIUtil.FixedXPanel; import megamek.client.ui.util.ViewFormatting; +import megamek.common.Configuration; import megamek.common.units.Entity; import megamek.common.Report; import megamek.common.templates.TROView; @@ -77,8 +84,18 @@ public class EntityReadoutPanel extends JPanel { private final JLabel fluffImageComponent = new JLabel(); private final JScrollPane scrollPane = new JScrollPane(readoutTextComponent); + private final JLabel fluffImageLabel = new JLabel(); + private final List fluffImageList = new ArrayList<>(); + private int fluffImageIndex = 0; + private final JButton nextImageButton = new JButton(" > "); + private final JButton prevImageButton = new JButton(" < "); + private final JLabel imageInfoLabel = new JLabel("", JLabel.CENTER); + public static final int DEFAULT_WIDTH = 360; + private static final String PLACEHOLDER_IMAGE_NAME = Configuration.fluffImagesDir() + "/fluff_placeholder.png"; + private static final Image PLACEHOLDER_IMAGE = readPlaceHolderImage(); + public EntityReadoutPanel() { this(-1, -1); } @@ -140,21 +157,47 @@ public void mouseMoved(MouseEvent e) { } textPanel.add(scrollPane); - var fluffPanel = new FixedXPanel(); - if (width != -1) { - fluffPanel.setMinimumSize(new Dimension(width, height)); - fluffPanel.setPreferredSize(new Dimension(width, height)); - } - fluffPanel.add(fluffImageComponent); +// var fluffPanel = new FixedXPanel(); +// if (width != -1) { +// fluffPanel.setMinimumSize(new Dimension(width, height)); +// fluffPanel.setPreferredSize(new Dimension(width, height)); +// } +// fluffPanel.add(fluffImageComponent); +// +// JPanel p = new JPanel(); +// p.setLayout(new BoxLayout(p, BoxLayout.LINE_AXIS)); +// p.add(textPanel); +// p.add(fluffPanel); +// p.add(Box.createHorizontalGlue()); +// setLayout(new BorderLayout()); +// add(p); +// addMouseWheelListener(wheelForwarder); + + var imageControlsPanel = new UIUtil.FixedYPanel(new FlowLayout()); + imageControlsPanel.add(prevImageButton); + imageControlsPanel.add(nextImageButton); - JPanel p = new JPanel(); - p.setLayout(new BoxLayout(p, BoxLayout.LINE_AXIS)); + imageControlsPanel.setAlignmentX(0.5f); + fluffImageLabel.setAlignmentX(0.5f); + imageInfoLabel.setAlignmentX(0.5f); + + Box fluffPanel = Box.createVerticalBox(); + fluffPanel.setAlignmentY(0); + fluffPanel.add(imageControlsPanel); + fluffPanel.add(fluffImageLabel); + fluffPanel.add(Box.createVerticalStrut(10)); + fluffPanel.add(imageInfoLabel); + + Box p = Box.createHorizontalBox(); p.add(textPanel); p.add(fluffPanel); p.add(Box.createHorizontalGlue()); setLayout(new BorderLayout()); add(p); addMouseWheelListener(wheelForwarder); + + nextImageButton.addActionListener(e -> showNextFluffImage()); + prevImageButton.addActionListener(e -> showPrevFluffImage()); } public void showEntity(Entity entity, EntityReadout mekView) { @@ -208,16 +251,42 @@ public void showEntity(Entity entity, boolean showDetail, boolean useAlternateCo showEntity(entity, mekView, fontName, sections); } - private void setFluffImage(Entity entity) { - Image image = FluffImageHelper.getFluffImage(entity); +// private void setFluffImage(Entity entity) { + // Image image = FluffImageHelper.getFluffImage(entity); + // // Scale down to the default width if the image is wider than that + // if (null != image) { + // if (image.getWidth(this) > DEFAULT_WIDTH) { + // image = image.getScaledInstance(DEFAULT_WIDTH, -1, Image.SCALE_SMOOTH); + // } + // fluffImageComponent.setIcon(new ImageIcon(image)); + // } else { + // fluffImageComponent.setIcon(null); + // } + // } + + private void setFluffImage(Image image) { // Scale down to the default width if the image is wider than that if (null != image) { if (image.getWidth(this) > DEFAULT_WIDTH) { image = image.getScaledInstance(DEFAULT_WIDTH, -1, Image.SCALE_SMOOTH); } - fluffImageComponent.setIcon(new ImageIcon(image)); + fluffImageLabel.setIcon(new ImageIcon(image)); + } else { + fluffImageLabel.setIcon(null); + fluffImageLabel.setToolTipText(null); + } + } + + private void setFluffImage(Entity entity) { + fluffImageList.clear(); + fluffImageList.addAll(FluffImageHelper.getFluffRecords(entity)); + fluffImageIndex = 0; + nextImageButton.setEnabled(fluffImageList.size() > 1); + prevImageButton.setEnabled(fluffImageList.size() > 1); + if (entity != null) { + showNextFluffImage(); } else { - fluffImageComponent.setIcon(null); + setFluffImage((Image) null); } } @@ -234,4 +303,57 @@ public void reset() { listener.mouseWheelMoved(converted); } }; + + private void showNextFluffImage() { + changeFluffImageIndex(1); + } + + private void showPrevFluffImage() { + changeFluffImageIndex(-1); + } + + private void changeFluffImageIndex(int delta) { + fluffImageIndex += delta; + if (fluffImageIndex >= fluffImageList.size()) { + fluffImageIndex = 0; + } + if (fluffImageIndex < 0) { + fluffImageIndex = fluffImageList.size() - 1; + } + if ((fluffImageIndex >= 0) && (fluffImageIndex < fluffImageList.size())) { + try { + FluffImageHelper.FluffImageRecord record = fluffImageList.get(fluffImageIndex); + setFluffImage(record.getImage()); + imageInfoLabel.setText(prepareLabelText(record.file())); + fluffImageLabel.setToolTipText(FluffImageTooltip.getTooltip(record)); + imageInfoLabel.setText(FluffImageTooltip.getTooltip(record)); + } catch (IOException ex) { + setFluffImage((Image) null); + imageInfoLabel.setText("Error loading fluff image"); + } + } else { + setFluffImage(PLACEHOLDER_IMAGE); + imageInfoLabel.setText(""); + } + } + + private String prepareLabelText(File file) { + String labelText = ""; + String labelInfo = file.toString(); + if (labelInfo.contains("__")) { + labelText = labelInfo.substring(labelInfo.lastIndexOf("__") + 2); + } + if (labelText.contains(".")) { + labelText = labelText.substring(0, labelText.lastIndexOf(".")); + } + return labelText; + } + + private static Image readPlaceHolderImage() { + try { + return ImageIO.read(new File(PLACEHOLDER_IMAGE_NAME)); + } catch (IOException e) { + return null; + } + } } diff --git a/megamek/src/megamek/client/ui/swing/MekViewPanel.java b/megamek/src/megamek/client/ui/swing/MekViewPanel.java deleted file mode 100644 index f0eabe1ddd0..00000000000 --- a/megamek/src/megamek/client/ui/swing/MekViewPanel.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * MCopyright (C) 2000-2004 Ben Mazur (bmazur@sev.org) - * Copyright (c) 2021, 2024 - The MegaMek Team. All Rights Reserved. - * - * This file is part of MegaMek. - * - * MegaMek is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * MegaMek is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with MegaMek. If not, see . - */ -package megamek.client.ui.swing; - -import java.awt.BorderLayout; -import java.awt.ComponentOrientation; -import java.awt.Dimension; -import java.awt.GridLayout; -import java.awt.Image; -import java.awt.event.MouseWheelEvent; -import java.awt.event.MouseWheelListener; - -import javax.swing.Box; -import javax.swing.ImageIcon; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextPane; -import javax.swing.SwingUtilities; -import javax.swing.border.EmptyBorder; -import javax.swing.event.HyperlinkEvent; - -import megamek.client.ui.swing.util.FluffImageHelper; -import megamek.client.ui.swing.util.UIUtil; -import megamek.common.Configuration; -import megamek.common.Entity; -import megamek.common.MekView; -import megamek.common.Report; -import megamek.common.templates.TROView; - -import java.io.File; -import java.io.IOException; -import java.util.List; -import javax.imageio.ImageIO; -import javax.swing.*; -import java.awt.*; -import java.util.ArrayList; - -/** - * @author Jay Lawson - */ -public class MekViewPanel extends JPanel { - - private static final String PLACEHOLDER_IMAGE_NAME = Configuration.fluffImagesDir() + "/fluff_placeholder.png"; - private static final Image PLACEHOLDER_IMAGE = readPlaceHolderImage(); - - private final JTextPane txtMek = new JTextPane(); - private JScrollPane scrMek; - - private final JLabel fluffImageLabel = new JLabel(); - private final List fluffImageList = new ArrayList<>(); - private int fluffImageIndex = 0; - private final JButton nextImageButton = new JButton(" > "); - private final JButton prevImageButton = new JButton(" < "); - private final JLabel imageInfoLabel = new JLabel("", JLabel.CENTER); - - public static final int DEFAULT_WIDTH = 360; - - public MekViewPanel() { - this(-1, -1, true); - } - - public MekViewPanel(int width, int height, boolean noBorder) { - Report.setupStylesheet(txtMek); - txtMek.setEditable(false); - txtMek.setBorder(new EmptyBorder(5, 10, 0, 0)); - if (width != -1) { - txtMek.setMinimumSize(new Dimension(width, height)); - txtMek.setPreferredSize(new Dimension(width, height)); - } - txtMek.addHyperlinkListener(e -> { - if (HyperlinkEvent.EventType.ACTIVATED == e.getEventType()) { - UIUtil.browse(e.getURL().toString(), this); - } - }); - scrMek = new JScrollPane(txtMek); - scrMek.getVerticalScrollBar().setUnitIncrement(16); - - if (noBorder) { - scrMek.setBorder(null); - } - scrMek.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); - - var textPanel = new JPanel(new GridLayout(1, 1)); - if (width != -1) { - textPanel.setMinimumSize(new Dimension(width, height)); - textPanel.setPreferredSize(new Dimension(width, height)); - } - textPanel.add(scrMek); - - var imageControlsPanel = new UIUtil.FixedYPanel(new FlowLayout()); - imageControlsPanel.add(prevImageButton); - imageControlsPanel.add(nextImageButton); - - imageControlsPanel.setAlignmentX(0.5f); - fluffImageLabel.setAlignmentX(0.5f); - imageInfoLabel.setAlignmentX(0.5f); - - Box fluffPanel = Box.createVerticalBox(); - fluffPanel.setAlignmentY(0); - fluffPanel.add(imageControlsPanel); - fluffPanel.add(fluffImageLabel); - fluffPanel.add(Box.createVerticalStrut(10)); - fluffPanel.add(imageInfoLabel); - - Box p = Box.createHorizontalBox(); - p.add(textPanel); - p.add(fluffPanel); - p.add(Box.createHorizontalGlue()); - setLayout(new BorderLayout()); - add(p); - addMouseWheelListener(wheelForwarder); - - nextImageButton.addActionListener(e -> showNextFluffImage()); - prevImageButton.addActionListener(e -> showPrevFluffImage()); - } - - public void setMek(Entity entity, MekView mekView) { - txtMek.setText(mekView.getMekReadout()); - txtMek.setCaretPosition(0); - setFluffImage(entity); - } - - public void setMek(Entity entity, MekView mekView, String fontName) { - txtMek.setText(mekView.getMekReadout(fontName)); - txtMek.setCaretPosition(0); - setFluffImage(entity); - } - - public void setMek(Entity entity, TROView troView) { - txtMek.setText(troView.processTemplate()); - txtMek.setCaretPosition(0); - setFluffImage(entity); - } - - @SuppressWarnings("unused") // Used in MHQ - public void setMek(Entity entity, boolean useAlternateCost) { - MekView mechView = new MekView(entity, false, useAlternateCost); - setMek(entity, mechView); - } - - public void setMek(Entity entity, String fontName) { - MekView mekView = new MekView(entity, false, false); - setMek(entity, mekView, fontName); - } - - private void setFluffImage(Entity entity) { - fluffImageList.clear(); - fluffImageList.addAll(FluffImageHelper.getFluffRecords(entity)); - fluffImageIndex = 0; - nextImageButton.setEnabled(fluffImageList.size() > 1); - prevImageButton.setEnabled(fluffImageList.size() > 1); - if (entity != null) { - showNextFluffImage(); - } else { - setFluffImage((Image) null); - } - } - - private void setFluffImage(Image image) { - // Scale down to the default width if the image is wider than that - if (null != image) { - if (image.getWidth(this) > DEFAULT_WIDTH) { - image = image.getScaledInstance(DEFAULT_WIDTH, -1, Image.SCALE_SMOOTH); - } - fluffImageLabel.setIcon(new ImageIcon(image)); - } else { - fluffImageLabel.setIcon(null); - fluffImageLabel.setToolTipText(null); - } - } - - public void reset() { - txtMek.setText(""); - fluffImageList.clear(); - setFluffImage((Entity) null); - } - - /** Forwards a mouse wheel scroll on the fluff image or free space to the TRO entry. */ - private final MouseWheelListener wheelForwarder = e -> { - MouseWheelEvent converted = (MouseWheelEvent) SwingUtilities.convertMouseEvent(MekViewPanel.this, e, scrMek); - for (MouseWheelListener listener : scrMek.getMouseWheelListeners()) { - listener.mouseWheelMoved(converted); - } - }; - - private void showNextFluffImage() { - changeFluffImageIndex(1); - } - - private void showPrevFluffImage() { - changeFluffImageIndex(-1); - } - - private void changeFluffImageIndex(int delta) { - fluffImageIndex += delta; - if (fluffImageIndex >= fluffImageList.size()) { - fluffImageIndex = 0; - } - if (fluffImageIndex < 0) { - fluffImageIndex = fluffImageList.size() - 1; - } - if ((fluffImageIndex >= 0) && (fluffImageIndex < fluffImageList.size())) { - try { - FluffImageHelper.FluffImageRecord record = fluffImageList.get(fluffImageIndex); - setFluffImage(record.getImage()); - imageInfoLabel.setText(prepareLabelText(record.file())); - fluffImageLabel.setToolTipText(FluffImageTooltip.getTooltip(record)); - imageInfoLabel.setText(FluffImageTooltip.getTooltip(record)); - } catch (IOException ex) { - setFluffImage((Image) null); - imageInfoLabel.setText("Error loading fluff image"); - } - } else { - setFluffImage(PLACEHOLDER_IMAGE); - imageInfoLabel.setText(""); - } - } - - private String prepareLabelText(File file) { - String labelText = ""; - String labelInfo = file.toString(); - if (labelInfo.contains("__")) { - labelText = labelInfo.substring(labelInfo.lastIndexOf("__") + 2); - } - if (labelText.contains(".")) { - labelText = labelText.substring(0, labelText.lastIndexOf(".")); - } - return labelText; - } - - private static Image readPlaceHolderImage() { - try { - return ImageIO.read(new File(PLACEHOLDER_IMAGE_NAME)); - } catch (IOException e) { - return null; - } - } -} diff --git a/megamek/src/megamek/client/ui/util/FluffImageHelper.java b/megamek/src/megamek/client/ui/util/FluffImageHelper.java index 89179450151..94d5aae62db 100644 --- a/megamek/src/megamek/client/ui/util/FluffImageHelper.java +++ b/megamek/src/megamek/client/ui/util/FluffImageHelper.java @@ -341,6 +341,7 @@ private static String sanitize(String original) { private static List nameCandidates(BTObject unit) { List candidates = new ArrayList<>(); + String sanitizedChassis = sanitize(unit.generalName()); String sanitizedModel = sanitize(unit.specificName()); // Check for an empty model so the order more specific -> less specific name candidate is always kept if (!sanitizedModel.isBlank()) { From 3218c68370ad076f48e338c1d21102b6b12c94f8 Mon Sep 17 00:00:00 2001 From: HammerGS Date: Sat, 10 Jan 2026 14:02:52 -0700 Subject: [PATCH 18/22] Fix: Use findFluffFiles instead of undefined findFluffFile method --- megamek/src/megamek/client/ui/util/FluffImageHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/megamek/src/megamek/client/ui/util/FluffImageHelper.java b/megamek/src/megamek/client/ui/util/FluffImageHelper.java index 56011db6aec..af21c77ea82 100644 --- a/megamek/src/megamek/client/ui/util/FluffImageHelper.java +++ b/megamek/src/megamek/client/ui/util/FluffImageHelper.java @@ -108,7 +108,7 @@ public final class FluffImageHelper { if (unit == null) { return null; } - File fluffImageFile = findFluffFile(unit, true); + File fluffImageFile = findFluffFiles(unit, true).stream().findFirst().orElse(null); if (fluffImageFile != null) { return fluffImageFile.toString(); } else { From 1fd5a2521c6536d08e7a8e26bc4e1b8e6bd49818 Mon Sep 17 00:00:00 2001 From: HammerGS Date: Sat, 10 Jan 2026 14:08:32 -0700 Subject: [PATCH 19/22] Address Copilot review feedback - Fix getExtension variable bug (filename vs f) - Remove test code: use get(0) instead of random image selection - Fix double setText on imageInfoLabel (removed redundant call) - Add null safety to FluffImageRecord.getImage() for null file - Add null parent check in FluffImageTooltip.getYamlFile() - Add Files.walk depth limits for performance - Fix JavaDoc: return descriptions now match actual behavior - Remove commented-out code blocks - Remove unused prepareLabelText method - Fix path construction for PLACEHOLDER_IMAGE_NAME - Add bounds checks in isSuitableYamlFile() --- .../megamek/client/ui/FluffImageTooltip.java | 34 ++++++++++++--- .../EntityReadoutPanel.java | 36 ++-------------- .../client/ui/util/FluffImageHelper.java | 42 ++++++++----------- 3 files changed, 51 insertions(+), 61 deletions(-) diff --git a/megamek/src/megamek/client/ui/FluffImageTooltip.java b/megamek/src/megamek/client/ui/FluffImageTooltip.java index 28416fbaf8a..e7accbe1210 100644 --- a/megamek/src/megamek/client/ui/FluffImageTooltip.java +++ b/megamek/src/megamek/client/ui/FluffImageTooltip.java @@ -115,17 +115,41 @@ private static String findInsignia(JsonNode contentNode) { } private static Optional getYamlFile(File imageFile) { - try (Stream entries = Files.walk(imageFile.getParentFile().toPath())) { + File parent = imageFile.getParentFile(); + if (parent == null) { + LogManager.getLogger().warn("Image file {} has no parent directory; cannot search for YAML.", imageFile); + return Optional.empty(); + } + try (Stream entries = Files.walk(parent.toPath(), 1)) { return entries.filter(p -> isSuitableYamlFile(p, imageFile)).map(Path::toFile).findFirst(); } catch (Exception e) { - LogManager.getLogger().warn("Error while reading files from {}", imageFile.getParentFile(), e); + LogManager.getLogger().warn("Error while reading files from {}", parent, e); return Optional.empty(); } } private static boolean isSuitableYamlFile(Path yamlFile, File imageFile) { - String yamlFileName = yamlFile.getFileName().toString(); - return !Files.isDirectory(yamlFile) && yamlFileName.endsWith("data.yaml") - && imageFile.getName().contains(yamlFileName.substring(0, yamlFileName.length() - 9)); + if (Files.isDirectory(yamlFile)) { + return false; + } + + Path yamlFileNamePath = yamlFile.getFileName(); + if (yamlFileNamePath == null) { + return false; + } + + String yamlFileName = yamlFileNamePath.toString(); + String suffix = "data.yaml"; + if (!yamlFileName.endsWith(suffix)) { + return false; + } + + int baseLength = yamlFileName.length() - suffix.length(); + if (baseLength <= 0) { + return false; + } + + String baseName = yamlFileName.substring(0, baseLength); + return imageFile.getName().contains(baseName); } } diff --git a/megamek/src/megamek/client/ui/dialogs/unitSelectorDialogs/EntityReadoutPanel.java b/megamek/src/megamek/client/ui/dialogs/unitSelectorDialogs/EntityReadoutPanel.java index 564bdc2a48e..dc5254efd26 100644 --- a/megamek/src/megamek/client/ui/dialogs/unitSelectorDialogs/EntityReadoutPanel.java +++ b/megamek/src/megamek/client/ui/dialogs/unitSelectorDialogs/EntityReadoutPanel.java @@ -88,13 +88,14 @@ public class EntityReadoutPanel extends JPanel { private final JLabel fluffImageLabel = new JLabel(); private final List fluffImageList = new ArrayList<>(); private int fluffImageIndex = 0; - private final JButton nextImageButton = new JButton(" > "); - private final JButton prevImageButton = new JButton(" < "); + private final JButton nextImageButton = new JButton(">"); + private final JButton prevImageButton = new JButton("<"); private final JLabel imageInfoLabel = new JLabel("", JLabel.CENTER); public static final int DEFAULT_WIDTH = 360; - private static final String PLACEHOLDER_IMAGE_NAME = Configuration.fluffImagesDir() + "/fluff_placeholder.png"; + private static final String PLACEHOLDER_IMAGE_NAME = + new File(Configuration.fluffImagesDir(), "fluff_placeholder.png").getPath(); private static final Image PLACEHOLDER_IMAGE = readPlaceHolderImage(); public EntityReadoutPanel() { @@ -158,22 +159,6 @@ public void mouseMoved(MouseEvent e) { } textPanel.add(scrollPane); -// var fluffPanel = new FixedXPanel(); -// if (width != -1) { -// fluffPanel.setMinimumSize(new Dimension(width, height)); -// fluffPanel.setPreferredSize(new Dimension(width, height)); -// } -// fluffPanel.add(fluffImageComponent); -// -// JPanel p = new JPanel(); -// p.setLayout(new BoxLayout(p, BoxLayout.LINE_AXIS)); -// p.add(textPanel); -// p.add(fluffPanel); -// p.add(Box.createHorizontalGlue()); -// setLayout(new BorderLayout()); -// add(p); -// addMouseWheelListener(wheelForwarder); - var imageControlsPanel = new UIUtil.FixedYPanel(new FlowLayout()); imageControlsPanel.add(prevImageButton); imageControlsPanel.add(nextImageButton); @@ -317,7 +302,6 @@ private void changeFluffImageIndex(int delta) { try { FluffImageHelper.FluffImageRecord record = fluffImageList.get(fluffImageIndex); setFluffImage(record.getImage()); - imageInfoLabel.setText(prepareLabelText(record.file())); fluffImageLabel.setToolTipText(FluffImageTooltip.getTooltip(record)); imageInfoLabel.setText(FluffImageTooltip.getTooltip(record)); } catch (IOException ex) { @@ -330,18 +314,6 @@ private void changeFluffImageIndex(int delta) { } } - private String prepareLabelText(File file) { - String labelText = ""; - String labelInfo = file.toString(); - if (labelInfo.contains("__")) { - labelText = labelInfo.substring(labelInfo.lastIndexOf("__") + 2); - } - if (labelText.contains(".")) { - labelText = labelText.substring(0, labelText.lastIndexOf(".")); - } - return labelText; - } - private static Image readPlaceHolderImage() { try { return ImageIO.read(new File(PLACEHOLDER_IMAGE_NAME)); diff --git a/megamek/src/megamek/client/ui/util/FluffImageHelper.java b/megamek/src/megamek/client/ui/util/FluffImageHelper.java index af21c77ea82..08a061a0d41 100644 --- a/megamek/src/megamek/client/ui/util/FluffImageHelper.java +++ b/megamek/src/megamek/client/ui/util/FluffImageHelper.java @@ -119,40 +119,36 @@ public final class FluffImageHelper { /** * Returns a list of all fluff images for the given unit/object to be shown e.g. in the unit summary. * - *

If a fluff image is stored in the unit/object itself, e.g. if it was part of the + *

If a fluff image is stored in the unit/object itself, e.g. if it was part of the * unit's file or is created by the unit itself, only this is returned. Note that this is not used for canon units, but may be used in - * custom ones by adding a fluff image to the unit in MML. + * custom ones by adding a fluff image to the unit in MML.

* - *

Otherwise, the fluff image directories are searched. First searches the user dir, + *

Otherwise, the fluff image directories are searched. First searches the user dir, * then the internal dir. Tries to match the image by chassis + model or chassis alone. Chassis and model names are cleaned from " and / * characters before matching. For Meks with clan names, both names and the combinations are searched. The model alone is not used to - * search. - * - * Returns null if no fluff image can be found. + * search.

* * @param unit The unit - * @return a fluff image or null, if no match is found + * @return a list of fluff images, or an empty list if none are found */ public static List getFluffImages(@Nullable BTObject unit) { return getFluffImageList(unit, false); } /** - * Returns a list of all fluff images for the given unit/object to be shown e.g. in the unit summary. + * Returns a list of all fluff image records for the given unit/object to be shown e.g. in the unit summary. * - *

If a fluff image is stored in the unit/object itself, e.g. if it was part of the + *

If a fluff image is stored in the unit/object itself, e.g. if it was part of the * unit's file or is created by the unit itself, only this is returned. Note that this is not used for canon units, but may be used in - * custom ones by adding a fluff image to the unit in MML. + * custom ones by adding a fluff image to the unit in MML.

* - *

Otherwise, the fluff image directories are searched. First searches the user dir, + *

Otherwise, the fluff image directories are searched. First searches the user dir, * then the internal dir. Tries to match the image by chassis + model or chassis alone. Chassis and model names are cleaned from " and / * characters before matching. For Meks with clan names, both names and the combinations are searched. The model alone is not used to - * search. - * - * Returns null if no fluff image can be found. + * search.

* * @param unit The unit - * @return a fluff image or null, if no match is found + * @return a list of fluff image records, or an empty list if none are found */ public static List getFluffRecords(@Nullable BTObject unit) { return getFluffImageRecords(unit, false); @@ -176,11 +172,7 @@ public static List getFluffRecords(@Nullable BTObject unit) { private static @Nullable Image getFluffImage(@Nullable BTObject unit, boolean recordSheet) { List fluffImages = getFluffImageList(unit, recordSheet); if (!fluffImages.isEmpty()) { - //return fluffImages.get(0); - // TEST --- just choose a random image from the available ones. Should be get(0) instead - int rndIndex = (int) (Math.random() * fluffImages.size()); - return fluffImages.get(rndIndex); - // --- + return fluffImages.get(0); } else { return null; } @@ -346,11 +338,11 @@ private static List getFluffInChassisDir(BTObject unit, File chassisDir) { private static List getFluffInDir(File dir) { List result = new ArrayList<>(); - try (Stream entries = Files.walk(dir.toPath())) { + try (Stream entries = Files.walk(dir.toPath(), 1)) { result.addAll(entries.map(Objects::toString).map(File::new).toList()); result.removeIf(FluffImageHelper::isNoImageFile); } catch (IOException e) { - LogManager.getLogger().warn("Error while reading files from " + dir, e); + LogManager.getLogger().warn("Error while reading files from {}", dir, e); } return result; } @@ -481,8 +473,10 @@ public static FluffImageRecord toRecord(File file) { public Image getImage() throws IOException { if (image != null) { return image; - } else { + } else if (file != null) { return ImageIO.read(file); + } else { + return null; } } } @@ -502,6 +496,6 @@ private static boolean isNoImageFile(File file) { public static Optional getExtension(String filename) { return Optional.ofNullable(filename) .filter(f -> f.contains(".")) - .map(f -> f.substring(filename.lastIndexOf("."))); + .map(f -> f.substring(f.lastIndexOf("."))); } } From ba7abd7b7201d455d613772dde485c27854dfdb4 Mon Sep 17 00:00:00 2001 From: HammerGS Date: Thu, 6 Aug 2026 14:31:38 -0600 Subject: [PATCH 20/22] Keep EXTENSIONS_FLUFF_IMAGE_FORMATS as String[] so MegaMekLab still builds The multi-fluff work changed this public constant from String[] to List to get a contains() check for the new chassis-directory scan. MegaMekLab calls Arrays.stream() on it in FluffTab and IconView, so the type swap breaks the MML build, and MegaMek's deprecation policy does not allow silently changing a public API that MML and MekHQ consume. Restore the constant to main's String[] and derive a private Set for the membership test instead, which also makes the lookup O(1). Co-Authored-By: Claude Opus 5 (1M context) --- megamek/src/megamek/client/ui/util/FluffImageHelper.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/megamek/src/megamek/client/ui/util/FluffImageHelper.java b/megamek/src/megamek/client/ui/util/FluffImageHelper.java index cc98ae10597..c11eb855ac2 100644 --- a/megamek/src/megamek/client/ui/util/FluffImageHelper.java +++ b/megamek/src/megamek/client/ui/util/FluffImageHelper.java @@ -78,8 +78,11 @@ public final class FluffImageHelper { public static final String DIR_NAME_SPACE_STATION = "Space Station"; public static final String DIR_NAME_VEHICLE = "Vehicle"; public static final String DIR_NAME_WARSHIP = "WarShip"; - public static final List EXTENSIONS_FLUFF_IMAGE_FORMATS = List.of(".PNG", ".png", ".JPG", - ".JPEG", ".jpg", ".jpeg", ".GIF", ".gif"); + public static final String[] EXTENSIONS_FLUFF_IMAGE_FORMATS = { ".PNG", ".png", ".JPG", + ".JPEG", ".jpg", ".jpeg", ".GIF", ".gif" }; + + /** The extensions above as a set, for membership tests when scanning a chassis or model directory. */ + private static final Set EXTENSIONS_FLUFF_IMAGE_FORMAT_SET = Set.of(EXTENSIONS_FLUFF_IMAGE_FORMATS); /** * Returns a fluff image for the given unit/object to be shown e.g. in the unit summary. @@ -531,7 +534,7 @@ public Image getImage() throws IOException { private static boolean isNoImageFile(File file) { Optional extension = getExtension(file.toString()); - return extension.isEmpty() || !EXTENSIONS_FLUFF_IMAGE_FORMATS.contains(extension.get()); + return extension.isEmpty() || !EXTENSIONS_FLUFF_IMAGE_FORMAT_SET.contains(extension.get()); } /** From 48faefe0dcff421f2f3bed9102a94fb3abea1ad0 Mon Sep 17 00:00:00 2001 From: HammerGS Date: Thu, 6 Aug 2026 14:46:17 -0600 Subject: [PATCH 21/22] Address the MegaMek review checklist items on the multi-fluff code Style and diagnosability pass over the fluff image work, no intended behaviour change beyond the three fixes noted at the end. - Externalise the user-facing strings ("Unit:", "Artist:", "Insignia:", the image load error) into messages.properties, and give the browse buttons localized tooltips. The separating space after each label is added in code so the translated strings do not need a trailing-space escape. - Give FluffImageTooltip the current full MegaMek license header and correct its class javadoc, which still referred to the long-renamed MechViewPanel. - Replace log4j LogManager with MMLogger in FluffImageTooltip and FluffImageHelper, and add [FluffImages] diagnostic logging where failures were previously silent: an unreadable fluff image, a missing placeholder image and a unit with no fluff at all. - Rename abbreviated identifiers: catch parameters e/ex to exception, lambda parameters e/p to event/entry/candidate, and Box p to readoutAndFluffPanel. - yamlMapper becomes YAML_MAPPER, and the yaml suffix, tooltip width and node names become named constants. - Scale the gap under the fluff image with UIUtil.scaleForGUI, and use CENTER_ALIGNMENT/TOP_ALIGNMENT instead of raw 0.5f/0. - Replace the wildcard java.util and java.awt imports with explicit ones and drop the now-unused BoxLayout and FixedXPanel imports. - Add @Nullable and javadoc (including @param tags on FluffImageRecord) where null is a legitimate value, and wrap javadoc literals in {@code ...}. Three real fixes came out of the pass: - reset() cleared the leftover fluffImageComponent field rather than the label actually on screen, so resetting the panel never cleared the fluff image. That field is now removed and reset() clears the image, the info line, the record list and the browse buttons. - Selecting a unit with sprites-only enabled, or with no unit at all, left the browse buttons enabled from the previously shown unit. - changeFluffImageIndex built the tooltip twice per image change. Co-Authored-By: Claude Opus 5 (1M context) --- .../megamek/client/messages.properties | 8 + .../megamek/client/ui/FluffImageTooltip.java | 175 ++++++++++++------ .../EntityReadoutPanel.java | 137 +++++++++----- .../client/ui/util/FluffImageHelper.java | 57 ++++-- 4 files changed, 253 insertions(+), 124 deletions(-) diff --git a/megamek/resources/megamek/client/messages.properties b/megamek/resources/megamek/client/messages.properties index bee29599b82..35cd62606bc 100644 --- a/megamek/resources/megamek/client/messages.properties +++ b/megamek/resources/megamek/client/messages.properties @@ -102,6 +102,14 @@ TechLevelDisplayDialog.copyAsHTML=Copy as HTML TechLevelDisplayDialog.noUnit=Error: Could not access the unit! ## EntityReadoutDialog EntityReadoutDialog.title=Unit Readout:\u0020 +## EntityReadoutPanel +EntityReadoutPanel.previousImage.toolTipText=Show the previous fluff image +EntityReadoutPanel.nextImage.toolTipText=Show the next fluff image +EntityReadoutPanel.imageLoadError=Error loading fluff image +## FluffImageTooltip +FluffImageTooltip.unit=Unit: +FluffImageTooltip.artist=Artist: +FluffImageTooltip.insignia=Insignia: ## RandomSkillDialog Class SkillGenerationDialog.title=Skill Generation Dialog SkillGenerationDialog.btnRandomize.toolTipText=Randomize the skills for the units assigned to the current player and close the dialog. diff --git a/megamek/src/megamek/client/ui/FluffImageTooltip.java b/megamek/src/megamek/client/ui/FluffImageTooltip.java index e7accbe1210..5356aa6802f 100644 --- a/megamek/src/megamek/client/ui/FluffImageTooltip.java +++ b/megamek/src/megamek/client/ui/FluffImageTooltip.java @@ -1,32 +1,38 @@ /* - * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved. * * This file is part of MegaMek. * * MegaMek is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. + * it under the terms of the GNU General Public License (GPL), + * version 3 or (at your option) any later version, + * as published by the Free Software Foundation. * * MegaMek is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. * - * You should have received a copy of the GNU General Public License - * along with MegaMek. If not, see . + * A copy of the GPL should have been included with this project; + * if not, see . + * + * NOTICE: The MegaMek organization is a non-profit group of volunteers + * creating free software for the BattleTech community. + * + * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks + * of The Topps Company, Inc. All Rights Reserved. + * + * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of + * InMediaRes Productions, LLC. + * + * MechWarrior Copyright Microsoft Corporation. MegaMek was created under + * Microsoft's "Game Content Usage Rules" + * and it is not endorsed by or + * affiliated with Microsoft. */ package megamek.client.ui; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import megamek.client.ui.clientGUI.GUIPreferences; -import megamek.client.ui.util.FluffImageHelper; -import megamek.client.ui.util.UIUtil; -import org.apache.logging.log4j.LogManager; - -import java.awt.*; +import java.awt.Color; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -36,15 +42,43 @@ import java.util.Optional; import java.util.stream.Stream; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import megamek.client.ui.clientGUI.GUIPreferences; +import megamek.client.ui.util.FluffImageHelper; +import megamek.client.ui.util.UIUtil; +import megamek.common.annotations.Nullable; +import megamek.logging.MMLogger; + /** * This class is very specialized. It provides tooltip information for the fluff image tooltip in the - * MechViewPanel, taken from yaml files that are supplied with painted minis images. + * {@link megamek.client.ui.dialogs.unitSelectorDialogs.EntityReadoutPanel}, taken from yaml files that are supplied + * with painted minis images. */ public class FluffImageTooltip { - private static final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory()); + private static final MMLogger LOGGER = MMLogger.create(FluffImageTooltip.class); + + private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); + + /** The suffix that marks a yaml file as the info sidecar of a fluff image. */ + private static final String YAML_FILE_SUFFIX = "data.yaml"; + + /** The width of the tooltip text block, before GUI scaling. */ + private static final int TOOLTIP_WIDTH = 360; - public static String styles() { + private static final String NODE_TITLE = "title"; + private static final String NODE_AUTHOR = "author"; + private static final String NODE_CONTENT = "content"; + private static final String NODE_TYPE = "type"; + private static final String VALUE_INSIGNIA = "insignia"; + + /** + * @return The CSS styles used by the fluff image tooltip. + */ + private static String styles() { int labelSize = UIUtil.scaleForGUI(UIUtil.FONT_SCALE1); Color color = GUIPreferences.getInstance().getToolTipLightFGColor(); String styleColor = Integer.toHexString(color.getRGB() & 0xFFFFFF); @@ -53,12 +87,13 @@ public static String styles() { } /** - * Returns the tooltip text for the supplied FluffImageRecord, if any can be found, null otherwise. + * Returns the tooltip text for the supplied FluffImageRecord, if any can be found, {@code null} otherwise. * * @param record The FluffImageRecord that is currently shown as an image - * @return A tooltip text or null if no yaml info is available + * + * @return A tooltip text, or {@code null} if no yaml info is available */ - public static String getTooltip(FluffImageHelper.FluffImageRecord record) { + public static @Nullable String getTooltip(FluffImageHelper.FluffImageRecord record) { return findYamlInfo(record).map(FluffImageTooltip::getTooltip).orElse(null); } @@ -66,49 +101,74 @@ private static Optional findYamlInfo(FluffImageHelper.FluffImageRecord rec return (record.file() == null) ? Optional.empty() : getYamlFile(record.file()); } - private static String getTooltip(File yamlFile) { + private static @Nullable String getTooltip(File yamlFile) { try { - JsonNode node = yamlMapper.readTree(yamlFile); + JsonNode node = YAML_MAPPER.readTree(yamlFile); StringBuilder result = new StringBuilder(""); - int width = UIUtil.scaleForGUI(360); + int width = UIUtil.scaleForGUI(TOOLTIP_WIDTH); result.append("
"); - if (node.has("title")) { - String unit = node.get("title").asText(); - if (!unit.isBlank()) { - result.append(UIUtil.spanCSS("label", "Unit: ")) - .append(UIUtil.spanCSS("value", unit)); - } - } - if (node.has("author")) { - String artist = node.get("author").asText(); - if (!artist.isBlank()) { - result.append(UIUtil.spanCSS("label", "
Artist: ")) - .append(UIUtil.spanCSS("value", artist)); - } - } - if (node.has("content")) { - JsonNode contentNode = node.get("content"); - String description = findInsignia(contentNode); + appendNodeValue(result, node, NODE_TITLE, Messages.getString("FluffImageTooltip.unit"), false); + appendNodeValue(result, node, NODE_AUTHOR, Messages.getString("FluffImageTooltip.artist"), true); + + if (node.has(NODE_CONTENT)) { + String description = findInsignia(node.get(NODE_CONTENT)); if (!description.isBlank()) { - result.append(UIUtil.spanCSS("label", "
Insignia: ")) - .append(UIUtil.spanCSS("value", description)); + appendLabelledValue(result, Messages.getString("FluffImageTooltip.insignia"), description, true); } } result.append("
"); return result.toString(); - } catch (IOException e) { + } catch (IOException exception) { + LOGGER.warn("Could not read fluff image info from {}", yamlFile, exception); return null; } } + /** + * Appends a "label value" pair to the tooltip, taking the value from the given yaml node. Does nothing if the + * node is absent or its value is blank. + * + * @param result The tooltip being assembled + * @param node The yaml root node + * @param nodeName The name of the yaml node holding the value + * @param label The already localized label to show in front of the value + * @param precedeWithLineBreak {@code true} to start the entry on a new line + */ + private static void appendNodeValue(StringBuilder result, JsonNode node, String nodeName, String label, + boolean precedeWithLineBreak) { + if (!node.has(nodeName)) { + return; + } + String value = node.get(nodeName).asText(); + if (!value.isBlank()) { + appendLabelledValue(result, label, value, precedeWithLineBreak); + } + } + + /** + * Appends a "label value" pair to the tooltip. The separating space is added here so that the localized labels + * do not have to carry a trailing space. + * + * @param result The tooltip being assembled + * @param label The already localized label to show in front of the value + * @param value The value to show + * @param precedeWithLineBreak {@code true} to start the entry on a new line + */ + private static void appendLabelledValue(StringBuilder result, String label, String value, + boolean precedeWithLineBreak) { + String lineBreak = precedeWithLineBreak ? "
" : ""; + result.append(UIUtil.spanCSS("label", lineBreak + label + " ")) + .append(UIUtil.spanCSS("value", value)); + } + private static String findInsignia(JsonNode contentNode) { List nodes = new ArrayList<>(); contentNode.iterator().forEachRemaining(nodes::add); for (JsonNode node : nodes) { - if (node.has("type") && node.get("type").asText().equals("insignia")) { - return node.get("content").asText(); + if (node.has(NODE_TYPE) && node.get(NODE_TYPE).asText().equals(VALUE_INSIGNIA)) { + return node.get(NODE_CONTENT).asText(); } } return ""; @@ -117,13 +177,15 @@ private static String findInsignia(JsonNode contentNode) { private static Optional getYamlFile(File imageFile) { File parent = imageFile.getParentFile(); if (parent == null) { - LogManager.getLogger().warn("Image file {} has no parent directory; cannot search for YAML.", imageFile); + LOGGER.warn("Image file {} has no parent directory; cannot search for YAML.", imageFile); return Optional.empty(); } try (Stream entries = Files.walk(parent.toPath(), 1)) { - return entries.filter(p -> isSuitableYamlFile(p, imageFile)).map(Path::toFile).findFirst(); - } catch (Exception e) { - LogManager.getLogger().warn("Error while reading files from {}", parent, e); + return entries.filter(entry -> isSuitableYamlFile(entry, imageFile)).map(Path::toFile).findFirst(); + } catch (Exception exception) { + // Deliberately broad: walking a user-supplied directory can also fail with UncheckedIOException + // or a SecurityException, and a missing tooltip must never break the readout panel. + LOGGER.warn("Error while reading files from {}", parent, exception); return Optional.empty(); } } @@ -139,12 +201,11 @@ private static boolean isSuitableYamlFile(Path yamlFile, File imageFile) { } String yamlFileName = yamlFileNamePath.toString(); - String suffix = "data.yaml"; - if (!yamlFileName.endsWith(suffix)) { + if (!yamlFileName.endsWith(YAML_FILE_SUFFIX)) { return false; } - int baseLength = yamlFileName.length() - suffix.length(); + int baseLength = yamlFileName.length() - YAML_FILE_SUFFIX.length(); if (baseLength <= 0) { return false; } @@ -152,4 +213,6 @@ private static boolean isSuitableYamlFile(Path yamlFile, File imageFile) { String baseName = yamlFileName.substring(0, baseLength); return imageFile.getName().contains(baseName); } + + private FluffImageTooltip() {} } diff --git a/megamek/src/megamek/client/ui/dialogs/unitSelectorDialogs/EntityReadoutPanel.java b/megamek/src/megamek/client/ui/dialogs/unitSelectorDialogs/EntityReadoutPanel.java index 51aa901f83f..cd6ac63c902 100644 --- a/megamek/src/megamek/client/ui/dialogs/unitSelectorDialogs/EntityReadoutPanel.java +++ b/megamek/src/megamek/client/ui/dialogs/unitSelectorDialogs/EntityReadoutPanel.java @@ -65,6 +65,7 @@ import javax.swing.text.html.HTMLDocument; import megamek.client.ui.FluffImageTooltip; +import megamek.client.ui.Messages; import megamek.client.ui.entityreadout.EntityReadout; import megamek.client.ui.entityreadout.ReadoutSections; import megamek.client.ui.util.FluffImageHelper; @@ -72,20 +73,23 @@ import megamek.client.ui.util.ViewFormatting; import megamek.common.Configuration; import megamek.common.Report; +import megamek.common.annotations.Nullable; import megamek.common.preference.PreferenceManager; import megamek.common.templates.TROView; import megamek.common.units.Entity; import megamek.common.util.StringUtil; +import megamek.logging.MMLogger; /** * @author Jay Lawson * @since November 2, 2009 */ public class EntityReadoutPanel extends JPanel { + private static final MMLogger LOGGER = MMLogger.create(EntityReadoutPanel.class); + private final int TOOLTIP_MAX_SIZE = 85; private final JTextPane readoutTextComponent = new JTextPane(); - private final JLabel fluffImageComponent = new JLabel(); private final JScrollPane scrollPane = new JScrollPane(readoutTextComponent); private final JLabel fluffImageLabel = new JLabel(); @@ -97,6 +101,9 @@ public class EntityReadoutPanel extends JPanel { public static final int DEFAULT_WIDTH = 360; + /** The vertical gap between the fluff image and the info line below it, before GUI scaling. */ + private static final int IMAGE_INFO_GAP = 10; + private static final String PLACEHOLDER_IMAGE_NAME = new File(Configuration.fluffImagesDir(), "fluff_placeholder.png").getPath(); private static final Image PLACEHOLDER_IMAGE = readPlaceHolderImage(); @@ -165,31 +172,34 @@ public void mouseMoved(MouseEvent e) { } textPanel.add(scrollPane); + prevImageButton.setToolTipText(Messages.getString("EntityReadoutPanel.previousImage.toolTipText")); + nextImageButton.setToolTipText(Messages.getString("EntityReadoutPanel.nextImage.toolTipText")); + var imageControlsPanel = new UIUtil.FixedYPanel(new FlowLayout()); imageControlsPanel.add(prevImageButton); imageControlsPanel.add(nextImageButton); - imageControlsPanel.setAlignmentX(0.5f); - fluffImageLabel.setAlignmentX(0.5f); - imageInfoLabel.setAlignmentX(0.5f); + imageControlsPanel.setAlignmentX(CENTER_ALIGNMENT); + fluffImageLabel.setAlignmentX(CENTER_ALIGNMENT); + imageInfoLabel.setAlignmentX(CENTER_ALIGNMENT); Box fluffPanel = Box.createVerticalBox(); - fluffPanel.setAlignmentY(0); + fluffPanel.setAlignmentY(TOP_ALIGNMENT); fluffPanel.add(imageControlsPanel); fluffPanel.add(fluffImageLabel); - fluffPanel.add(Box.createVerticalStrut(10)); + fluffPanel.add(Box.createVerticalStrut(UIUtil.scaleForGUI(IMAGE_INFO_GAP))); fluffPanel.add(imageInfoLabel); - Box p = Box.createHorizontalBox(); - p.add(textPanel); - p.add(fluffPanel); - p.add(Box.createHorizontalGlue()); + Box readoutAndFluffPanel = Box.createHorizontalBox(); + readoutAndFluffPanel.add(textPanel); + readoutAndFluffPanel.add(fluffPanel); + readoutAndFluffPanel.add(Box.createHorizontalGlue()); setLayout(new BorderLayout()); - add(p); + add(readoutAndFluffPanel); addMouseWheelListener(wheelForwarder); - nextImageButton.addActionListener(e -> showNextFluffImage()); - prevImageButton.addActionListener(e -> showPrevFluffImage()); + nextImageButton.addActionListener(event -> showNextFluffImage()); + prevImageButton.addActionListener(event -> showPrevFluffImage()); } public void showEntity(Entity entity, EntityReadout mekView) { @@ -243,45 +253,57 @@ public void showEntity(Entity entity, boolean showDetail, boolean useAlternateCo showEntity(entity, mekView, fontName, sections); } - private void setFluffImage(Image image) { - // Scale down to the default width if the image is wider than that - if (null != image) { - if (image.getWidth(this) > DEFAULT_WIDTH) { - image = image.getScaledInstance(DEFAULT_WIDTH, -1, Image.SCALE_SMOOTH); - } - fluffImageLabel.setIcon(new ImageIcon(image)); - } else { + /** + * Shows the given image as the fluff image, scaled down to {@link #DEFAULT_WIDTH} if it is wider than that. + * + * @param image The image to show, or {@code null} to clear the fluff image + */ + private void displayFluffImage(@Nullable Image image) { + if (image == null) { fluffImageLabel.setIcon(null); fluffImageLabel.setToolTipText(null); + return; + } + Image displayedImage = image; + if (displayedImage.getWidth(this) > DEFAULT_WIDTH) { + displayedImage = displayedImage.getScaledInstance(DEFAULT_WIDTH, -1, Image.SCALE_SMOOTH); } + fluffImageLabel.setIcon(new ImageIcon(displayedImage)); } - private void setFluffImage(Entity entity) { + private void setFluffImage(@Nullable Entity entity) { + fluffImageList.clear(); + fluffImageIndex = 0; + boolean isSpritesOnly = PreferenceManager.getClientPreferences().getSpritesOnly(); - if (isSpritesOnly) { - setFluffImage((Image) null); + if (isSpritesOnly || (entity == null)) { + nextImageButton.setEnabled(false); + prevImageButton.setEnabled(false); + imageInfoLabel.setText(""); + displayFluffImage(null); return; } - fluffImageList.clear(); + fluffImageList.addAll(FluffImageHelper.getFluffRecords(entity)); - fluffImageIndex = 0; - nextImageButton.setEnabled(fluffImageList.size() > 1); - prevImageButton.setEnabled(fluffImageList.size() > 1); - if (entity != null) { - showNextFluffImage(); - } else { - setFluffImage((Image) null); - } + boolean hasMultipleImages = fluffImageList.size() > 1; + nextImageButton.setEnabled(hasMultipleImages); + prevImageButton.setEnabled(hasMultipleImages); + showNextFluffImage(); } public void reset() { readoutTextComponent.setText(""); - fluffImageComponent.setIcon(null); + fluffImageList.clear(); + fluffImageIndex = 0; + nextImageButton.setEnabled(false); + prevImageButton.setEnabled(false); + imageInfoLabel.setText(""); + displayFluffImage(null); } /** Forwards a mouse wheel scroll on the fluff image or free space to the TRO entry. */ - MouseWheelListener wheelForwarder = e -> { - MouseWheelEvent converted = (MouseWheelEvent) SwingUtilities.convertMouseEvent(EntityReadoutPanel.this, e, + MouseWheelListener wheelForwarder = event -> { + MouseWheelEvent converted = (MouseWheelEvent) SwingUtilities.convertMouseEvent(EntityReadoutPanel.this, event, scrollPane); for (MouseWheelListener listener : scrollPane.getMouseWheelListeners()) { listener.mouseWheelMoved(converted); @@ -304,26 +326,39 @@ private void changeFluffImageIndex(int delta) { if (fluffImageIndex < 0) { fluffImageIndex = fluffImageList.size() - 1; } - if ((fluffImageIndex >= 0) && (fluffImageIndex < fluffImageList.size())) { - try { - FluffImageHelper.FluffImageRecord record = fluffImageList.get(fluffImageIndex); - setFluffImage(record.getImage()); - fluffImageLabel.setToolTipText(FluffImageTooltip.getTooltip(record)); - imageInfoLabel.setText(FluffImageTooltip.getTooltip(record)); - } catch (IOException ex) { - setFluffImage((Image) null); - imageInfoLabel.setText("Error loading fluff image"); - } - } else { - setFluffImage(PLACEHOLDER_IMAGE); + boolean hasImageAtIndex = (fluffImageIndex >= 0) && (fluffImageIndex < fluffImageList.size()); + if (!hasImageAtIndex) { + LOGGER.debug("[FluffImages] No fluff image available; showing the placeholder image."); + displayFluffImage(PLACEHOLDER_IMAGE); imageInfoLabel.setText(""); + return; } + + FluffImageHelper.FluffImageRecord record = fluffImageList.get(fluffImageIndex); + try { + displayFluffImage(record.getImage()); + } catch (IOException exception) { + LOGGER.warn("[FluffImages] Could not load fluff image {}", record.file(), exception); + displayFluffImage(null); + imageInfoLabel.setText(Messages.getString("EntityReadoutPanel.imageLoadError")); + return; + } + String imageInfo = FluffImageTooltip.getTooltip(record); + fluffImageLabel.setToolTipText(imageInfo); + imageInfoLabel.setText((imageInfo != null) ? imageInfo : ""); } - private static Image readPlaceHolderImage() { + private static @Nullable Image readPlaceHolderImage() { + File placeholderFile = new File(PLACEHOLDER_IMAGE_NAME); + if (!placeholderFile.exists()) { + LOGGER.debug("[FluffImages] No placeholder image at {}; units without fluff will show no image.", + PLACEHOLDER_IMAGE_NAME); + return null; + } try { - return ImageIO.read(new File(PLACEHOLDER_IMAGE_NAME)); - } catch (IOException e) { + return ImageIO.read(placeholderFile); + } catch (IOException exception) { + LOGGER.warn("[FluffImages] Could not read the placeholder image {}", PLACEHOLDER_IMAGE_NAME, exception); return null; } } diff --git a/megamek/src/megamek/client/ui/util/FluffImageHelper.java b/megamek/src/megamek/client/ui/util/FluffImageHelper.java index c11eb855ac2..50701d057fc 100644 --- a/megamek/src/megamek/client/ui/util/FluffImageHelper.java +++ b/megamek/src/megamek/client/ui/util/FluffImageHelper.java @@ -37,27 +37,29 @@ import java.awt.Image; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.imageio.ImageIO; import javax.swing.ImageIcon; import megamek.common.Configuration; -import megamek.common.loaders.MekSummary; import megamek.common.annotations.Nullable; import megamek.common.battlefieldSupport.BFSAssetType; import megamek.common.battlefieldSupport.BattlefieldSupportAsset; +import megamek.common.loaders.MekSummary; import megamek.common.preference.PreferenceManager; -import org.apache.logging.log4j.LogManager; - -import javax.imageio.ImageIO; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; import megamek.common.units.BTObject; import megamek.common.units.Mek; +import megamek.logging.MMLogger; /** * This class provides methods for retrieving fluff images, for use in MM, MML and MHQ; also for record sheets (where @@ -65,6 +67,8 @@ */ public final class FluffImageHelper { + private static final MMLogger LOGGER = MMLogger.create(FluffImageHelper.class); + public static final String DIR_NAME_BA = "BattleArmor"; public static final String DIR_NAME_ASSET = "Asset"; public static final String DIR_NAME_CONV_FIGHTER = "ConvFighter"; @@ -163,7 +167,7 @@ public static List getFluffRecords(@Nullable BTObject unit) { /** * Returns a fluff image for the given unit for the record sheet, with a fallback file named "hud.png" if that is * present in the right - * fluff directory, or null if nothing can be found. See {@link #getFluffImage(BTObject)} for + * fluff directory, or {@code null} if nothing can be found. See {@link #getFluffImage(BTObject)} for * further comments on how the fluff image is * searched. * @@ -187,7 +191,7 @@ public static List getFluffRecords(@Nullable BTObject unit) { /** * Returns a list of available fluff images. If a fluff image is embedded in the unit file, * only that image is returned, even if others are available from the fluff directories. The returned - * list may be empty, but not null. + * list may be empty, but not {@code null}. * * @param unit The unit * @param recordSheet True if this image search is meant for a record sheet (used in MML) @@ -208,7 +212,7 @@ private static List getFluffImageRecords(@Nullable BTObject un /** * Returns a list of available fluff images. If a fluff image is embedded in the unit file, * only that image is returned, even if others are available from the fluff directories. The returned - * list may be empty, but not null. + * list may be empty, but not {@code null}. * * @param unit The unit * @param recordSheet True if this image search is meant for a record sheet @@ -345,8 +349,8 @@ private static List getFluffInDir(File dir) { try (Stream entries = Files.walk(dir.toPath(), 1)) { result.addAll(entries.map(Objects::toString).map(File::new).toList()); result.removeIf(FluffImageHelper::isNoImageFile); - } catch (IOException e) { - LogManager.getLogger().warn("Error while reading files from {}", dir, e); + } catch (IOException exception) { + LOGGER.warn("Error while reading files from {}", dir, exception); } return result; } @@ -515,13 +519,32 @@ public static List getFluffPaths(BTObject unit) { }; } - public record FluffImageRecord(Image image, File file) { + /** + * A fluff image that is either already loaded (when it was embedded in the unit file) or still on disk, in which + * case it is only read when it is actually shown. Exactly one of the two is set. + * + * @param image The already loaded image, or {@code null} when the image must be read from {@code file} + * @param file The file holding the image, or {@code null} when {@code image} is already loaded + */ + public record FluffImageRecord(@Nullable Image image, @Nullable File file) { + /** + * @param file The image file to read when the image is shown + * + * @return A record for a fluff image that has not been loaded yet + */ public static FluffImageRecord toRecord(File file) { return new FluffImageRecord(null, file); } - public Image getImage() throws IOException { + /** + * Returns the fluff image, reading it from disk if it has not been loaded yet. + * + * @return The fluff image, or {@code null} if this record has neither an image nor a file + * + * @throws IOException When the image file is present but cannot be read + */ + public @Nullable Image getImage() throws IOException { if (image != null) { return image; } else if (file != null) { From f2647ac659569bdeab3588c6cadcc79d1ed065a4 Mon Sep 17 00:00:00 2001 From: HammerGS Date: Thu, 6 Aug 2026 16:28:49 -0600 Subject: [PATCH 22/22] Address Copilot review feedback on the multi-fluff PR - The readout panel opened on the second fluff image. setFluffImage() reset the index to 0 and then called showNextFluffImage(), which steps by one before displaying, so a unit with two or more images showed image 2 first and the "<" button went back to image 1. Single-image units wrapped around to 0 and looked correct, which is why it went unnoticed. Now shows the first image. - Tighten the chassis and model directory checks from exists() to isDirectory(), so a stray file named like a chassis is not scanned as a directory. - Use Files.list instead of Files.walk(dir, 1) for the shallow directory scan, which also drops the directory entry itself and the Objects::toString and File::new round trip. - Sort the images found in a chassis or model directory by file name. Neither Files.list nor Files.walk guarantees an order, so without this the image a unit shows first was down to the file system. - Pad the tooltip style colour to six hex digits. A dark tooltip foreground colour such as pure blue produced "ff", which is not a valid CSS colour, so the label text fell back to the default colour. - Extract the "---empty---" model directory name into a constant. Adds FluffImageHelperChassisDirTest covering the chassis and model subdirectory rules: model directory wins over chassis directory, the chassis directory is used when no model directory matches, empty models match the ---empty--- directory, clan chassis variant directories resolve, non-image files and stray files named like a chassis are ignored, and repeated calls return the same deterministic order. Co-Authored-By: Claude Opus 5 (1M context) --- .../megamek/client/ui/FluffImageTooltip.java | 4 +- .../EntityReadoutPanel.java | 3 +- .../client/ui/util/FluffImageHelper.java | 19 +- .../util/FluffImageHelperChassisDirTest.java | 164 ++++++++++++++++++ 4 files changed, 181 insertions(+), 9 deletions(-) create mode 100644 megamek/unittests/megamek/client/ui/util/FluffImageHelperChassisDirTest.java diff --git a/megamek/src/megamek/client/ui/FluffImageTooltip.java b/megamek/src/megamek/client/ui/FluffImageTooltip.java index 5356aa6802f..ef2093d3eee 100644 --- a/megamek/src/megamek/client/ui/FluffImageTooltip.java +++ b/megamek/src/megamek/client/ui/FluffImageTooltip.java @@ -81,7 +81,9 @@ public class FluffImageTooltip { private static String styles() { int labelSize = UIUtil.scaleForGUI(UIUtil.FONT_SCALE1); Color color = GUIPreferences.getInstance().getToolTipLightFGColor(); - String styleColor = Integer.toHexString(color.getRGB() & 0xFFFFFF); + // Pad to six digits: a dark colour such as pure blue would otherwise give "ff", which is not a valid + // CSS colour and makes the renderer fall back to the default text colour + String styleColor = String.format("%06X", color.getRGB() & 0xFFFFFF); return "span { font-family:Noto Sans; font-size:" + labelSize + "; }" + ".label { color:" + styleColor + "; }"; } diff --git a/megamek/src/megamek/client/ui/dialogs/unitSelectorDialogs/EntityReadoutPanel.java b/megamek/src/megamek/client/ui/dialogs/unitSelectorDialogs/EntityReadoutPanel.java index cd6ac63c902..09bb4dca41e 100644 --- a/megamek/src/megamek/client/ui/dialogs/unitSelectorDialogs/EntityReadoutPanel.java +++ b/megamek/src/megamek/client/ui/dialogs/unitSelectorDialogs/EntityReadoutPanel.java @@ -288,7 +288,8 @@ private void setFluffImage(@Nullable Entity entity) { boolean hasMultipleImages = fluffImageList.size() > 1; nextImageButton.setEnabled(hasMultipleImages); prevImageButton.setEnabled(hasMultipleImages); - showNextFluffImage(); + // Show the first image, not the next one - stepping by 1 here would open on the second image + changeFluffImageIndex(0); } public void reset() { diff --git a/megamek/src/megamek/client/ui/util/FluffImageHelper.java b/megamek/src/megamek/client/ui/util/FluffImageHelper.java index 50701d057fc..9cb177c2272 100644 --- a/megamek/src/megamek/client/ui/util/FluffImageHelper.java +++ b/megamek/src/megamek/client/ui/util/FluffImageHelper.java @@ -41,9 +41,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Comparator; import java.util.LinkedHashSet; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -88,6 +88,9 @@ public final class FluffImageHelper { /** The extensions above as a set, for membership tests when scanning a chassis or model directory. */ private static final Set EXTENSIONS_FLUFF_IMAGE_FORMAT_SET = Set.of(EXTENSIONS_FLUFF_IMAGE_FORMATS); + /** The model subdirectory name that units with an empty model match, e.g. fluff/Mek/Chassis/Chassis ---empty---. */ + static final String EMPTY_MODEL_DIR_NAME = "---empty---"; + /** * Returns a fluff image for the given unit/object to be shown e.g. in the unit summary. * @@ -295,11 +298,11 @@ private static Set findFluffFiles(BTObject unit, boolean recordSheet) { *
- In a model subdirectory fluff/[unittype]/[chassis]/[model], all files match if the * unit's chassis and model match [chassis] and [model]. The filename doesn't matter for matching. */ - private static List getFluffInChassisDirs(BTObject unit, File unitTypeFluffDir) { + static List getFluffInChassisDirs(BTObject unit, File unitTypeFluffDir) { List result = new ArrayList<>(); for (String nameCandidate : chassisNameCandidates(unit)) { var chassisDir = new File(unitTypeFluffDir, nameCandidate); - if (chassisDir.exists()) { + if (chassisDir.isDirectory()) { result.addAll(getFluffInChassisDir(unit, chassisDir)); } } @@ -329,12 +332,12 @@ private static List chassisNameCandidates(BTObject unit) { private static List getFluffInChassisDir(BTObject unit, File chassisDir) { String sanitizedModel = sanitize(unit.specificName()); if (sanitizedModel.isBlank()) { - sanitizedModel = "---empty---"; + sanitizedModel = EMPTY_MODEL_DIR_NAME; } List result = new ArrayList<>(); for (String chassisNameCandidate : chassisNameCandidates(unit)) { var modelDir = new File(chassisDir, chassisNameCandidate + " " + sanitizedModel); - if (modelDir.exists()) { + if (modelDir.isDirectory()) { result.addAll(getFluffInDir(modelDir)); } } @@ -346,12 +349,14 @@ private static List getFluffInChassisDir(BTObject unit, File chassisDir) { private static List getFluffInDir(File dir) { List result = new ArrayList<>(); - try (Stream entries = Files.walk(dir.toPath(), 1)) { - result.addAll(entries.map(Objects::toString).map(File::new).toList()); + // Files.list is a shallow listing and, unlike Files.walk(dir, 1), does not include the directory itself + try (Stream entries = Files.list(dir.toPath())) { + result.addAll(entries.map(Path::toFile).toList()); result.removeIf(FluffImageHelper::isNoImageFile); } catch (IOException exception) { LOGGER.warn("Error while reading files from {}", dir, exception); } + result.sort(Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER)); return result; } diff --git a/megamek/unittests/megamek/client/ui/util/FluffImageHelperChassisDirTest.java b/megamek/unittests/megamek/client/ui/util/FluffImageHelperChassisDirTest.java new file mode 100644 index 00000000000..cb295c5b17b --- /dev/null +++ b/megamek/unittests/megamek/client/ui/util/FluffImageHelperChassisDirTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2026 The MegaMek Team. All Rights Reserved. + * + * This file is part of MegaMek. + * + * MegaMek is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License (GPL), + * version 3 or (at your option) any later version, + * as published by the Free Software Foundation. + * + * MegaMek is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * A copy of the GPL should have been included with this project; + * if not, see . + * + * NOTICE: The MegaMek organization is a non-profit group of volunteers + * creating free software for the BattleTech community. + * + * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks + * of The Topps Company, Inc. All Rights Reserved. + * + * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of + * InMediaRes Productions, LLC. + * + * MechWarrior Copyright Microsoft Corporation. MegaMek was created under + * Microsoft's "Game Content Usage Rules" + * and it is not endorsed by or + * affiliated with Microsoft. + */ + +package megamek.client.ui.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import megamek.common.units.BipedMek; +import megamek.common.units.Mek; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Tests the chassis and model subdirectory matching rules for fluff images, i.e. the contents of + * fluff/[unittype]/[chassis] and fluff/[unittype]/[chassis]/[chassis model]. + */ +class FluffImageHelperChassisDirTest { + + private static Mek mek(String chassis, String model) { + Mek mek = new BipedMek(); + mek.setChassis(chassis); + mek.setModel(model); + return mek; + } + + private static void writeImage(Path directory, String fileName) throws IOException { + Files.createDirectories(directory); + Files.write(directory.resolve(fileName), new byte[] { 1, 2, 3 }); + } + + private static List fileNames(List files) { + return files.stream().map(File::getName).toList(); + } + + @Test + void findsEveryImageInTheChassisDirectoryRegardlessOfFileName(@TempDir Path fluffDir) throws IOException { + writeImage(fluffDir.resolve("Atlas"), "anything at all.png"); + writeImage(fluffDir.resolve("Atlas"), "second.jpg"); + + List found = FluffImageHelper.getFluffInChassisDirs(mek("Atlas", "AS7-D"), fluffDir.toFile()); + + assertEquals(List.of("anything at all.png", "second.jpg"), fileNames(found)); + } + + @Test + void modelDirectoryWinsOverChassisDirectory(@TempDir Path fluffDir) throws IOException { + writeImage(fluffDir.resolve("Atlas"), "generic atlas.png"); + writeImage(fluffDir.resolve("Atlas").resolve("Atlas AS7-D"), "specific.png"); + + List found = FluffImageHelper.getFluffInChassisDirs(mek("Atlas", "AS7-D"), fluffDir.toFile()); + + assertEquals(List.of("specific.png"), fileNames(found)); + } + + @Test + void chassisDirectoryIsUsedWhenNoModelDirectoryMatches(@TempDir Path fluffDir) throws IOException { + writeImage(fluffDir.resolve("Atlas"), "generic atlas.png"); + writeImage(fluffDir.resolve("Atlas").resolve("Atlas AS7-K"), "other model.png"); + + List found = FluffImageHelper.getFluffInChassisDirs(mek("Atlas", "AS7-D"), fluffDir.toFile()); + + assertEquals(List.of("generic atlas.png"), fileNames(found)); + } + + @Test + void emptyModelMatchesTheEmptyModelDirectory(@TempDir Path fluffDir) throws IOException { + writeImage(fluffDir.resolve("Atlas"), "generic atlas.png"); + writeImage(fluffDir.resolve("Atlas").resolve("Atlas " + FluffImageHelper.EMPTY_MODEL_DIR_NAME), + "no model.png"); + + List found = FluffImageHelper.getFluffInChassisDirs(mek("Atlas", ""), fluffDir.toFile()); + + assertEquals(List.of("no model.png"), fileNames(found)); + } + + @Test + void multipleImagesAreReturnedInDeterministicOrder(@TempDir Path fluffDir) throws IOException { + writeImage(fluffDir.resolve("Atlas"), "charlie.png"); + writeImage(fluffDir.resolve("Atlas"), "alpha.png"); + writeImage(fluffDir.resolve("Atlas"), "bravo.png"); + + List firstCall = FluffImageHelper.getFluffInChassisDirs(mek("Atlas", "AS7-D"), fluffDir.toFile()); + List secondCall = FluffImageHelper.getFluffInChassisDirs(mek("Atlas", "AS7-D"), fluffDir.toFile()); + + assertEquals(List.of("alpha.png", "bravo.png", "charlie.png"), fileNames(firstCall)); + assertEquals(fileNames(firstCall), fileNames(secondCall)); + } + + @Test + void nonImageFilesAreIgnored(@TempDir Path fluffDir) throws IOException { + writeImage(fluffDir.resolve("Atlas"), "picture.png"); + writeImage(fluffDir.resolve("Atlas"), "picture data.yaml"); + writeImage(fluffDir.resolve("Atlas"), "readme.txt"); + + List found = FluffImageHelper.getFluffInChassisDirs(mek("Atlas", "AS7-D"), fluffDir.toFile()); + + assertEquals(List.of("picture.png"), fileNames(found)); + } + + @Test + void clanChassisVariantDirectoriesAreFound(@TempDir Path fluffDir) throws IOException { + Mek timberWolf = mek("Mad Cat", "Prime"); + timberWolf.setClanChassisName("Timber Wolf"); + writeImage(fluffDir.resolve("Timber Wolf (Mad Cat)"), "timberwolf.png"); + + List found = FluffImageHelper.getFluffInChassisDirs(timberWolf, fluffDir.toFile()); + + assertEquals(List.of("timberwolf.png"), fileNames(found)); + } + + @Test + void aFileWhereAChassisDirectoryWouldBeIsIgnored(@TempDir Path fluffDir) throws IOException { + // A stray file named like a chassis must not be treated as a directory to scan + writeImage(fluffDir, "Atlas"); + + List found = FluffImageHelper.getFluffInChassisDirs(mek("Atlas", "AS7-D"), fluffDir.toFile()); + + assertTrue(found.isEmpty(), "A regular file named like the chassis must not yield fluff images"); + } + + @Test + void missingChassisDirectoryYieldsNoImages(@TempDir Path fluffDir) { + List found = FluffImageHelper.getFluffInChassisDirs(mek("Atlas", "AS7-D"), fluffDir.toFile()); + + assertTrue(found.isEmpty()); + } +}