Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
da88a4a
add processing of multiple fluff images
SJuliez May 20, 2024
bd2d0fa
allow clicking the fluff in the unit view to advance to the next image
SJuliez May 20, 2024
f26549a
cleanup
SJuliez May 20, 2024
2c6663f
allow clan mek chassis variants, require chassis in model subdir name
SJuliez May 21, 2024
8fa601d
clean up chassis name generation
SJuliez May 22, 2024
494ca0d
Merging PR's
HammerGS May 25, 2024
4878705
show next/prev buttons for fluff images, show filename (WIP)
SJuliez Aug 3, 2024
9382bef
Merge branch 'refs/heads/master' into WIP-multi-fluff
SJuliez Aug 3, 2024
890ac0f
remove image click mouse listener
SJuliez Aug 3, 2024
6696ed3
Merge branch 'master' into multi-fluff
HammerGS Aug 3, 2024
60b69b6
Fixing
HammerGS Aug 6, 2024
0985559
Add a doc on AI usage (not Princess related) to MegaMek
HammerGS Aug 9, 2024
4f4c794
only take image files as fluff images
SJuliez Aug 10, 2024
8c253ae
add yaml parsed fluff tooltip to the MechViewPanel
SJuliez Aug 13, 2024
4d1af5c
show placeholder for units without fluff
SJuliez Aug 18, 2024
15eef9a
show info below fluff image
SJuliez Aug 23, 2024
ce52f03
Merge branch 'master' into WIP-multi-fluff
SJuliez Feb 26, 2025
d5953a3
after merge changes
SJuliez Feb 27, 2025
7033c71
Updating history.txts
HammerGS Mar 1, 2025
005179b
Merge branch 'main' into multi-fluff
SJuliez Oct 14, 2025
d72c30e
aftermerge corrections
SJuliez Oct 15, 2025
b5fafaa
Merge main into multi-fluff
HammerGS Jan 10, 2026
3218c68
Fix: Use findFluffFiles instead of undefined findFluffFile method
HammerGS Jan 10, 2026
1fd5a25
Address Copilot review feedback
HammerGS Jan 10, 2026
e5e14d7
Merge branch 'main' into multi-fluff
HammerGS Jan 23, 2026
f8f8b5c
Merge branch 'main' into multi-fluff
HammerGS Aug 6, 2026
ba7abd7
Keep EXTENSIONS_FLUFF_IMAGE_FORMATS as String[] so MegaMekLab still b…
HammerGS Aug 6, 2026
48faefe
Address the MegaMek review checklist items on the multi-fluff code
HammerGS Aug 6, 2026
f2647ac
Address Copilot review feedback on the multi-fluff PR
HammerGS Aug 6, 2026
5b79e86
Merge remote-tracking branch 'origin/main' into Implement-multiple-fl…
HammerGS Aug 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions megamek/resources/megamek/client/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
220 changes: 220 additions & 0 deletions megamek/src/megamek/client/ui/FluffImageTooltip.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/*
* 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 (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 <https://www.gnu.org/licenses/>.
*
* 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"
* <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or
* affiliated with Microsoft.
*/
package megamek.client.ui;

import java.awt.Color;
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;

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
* {@link megamek.client.ui.dialogs.unitSelectorDialogs.EntityReadoutPanel}, taken from yaml files that are supplied
* with painted minis images.
*/
public class FluffImageTooltip {

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;

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();
// 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 + "; }";
}

/**
* 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 {@code null} if no yaml info is available
*/
public static @Nullable String getTooltip(FluffImageHelper.FluffImageRecord record) {
return findYamlInfo(record).map(FluffImageTooltip::getTooltip).orElse(null);
}

private static Optional<File> findYamlInfo(FluffImageHelper.FluffImageRecord record) {
return (record.file() == null) ? Optional.empty() : getYamlFile(record.file());
}

private static @Nullable String getTooltip(File yamlFile) {
try {
JsonNode node = YAML_MAPPER.readTree(yamlFile);

StringBuilder result = new StringBuilder("<HTML><HEAD><STYLE>" + styles() + "</STYLE></HEAD><BODY>");
int width = UIUtil.scaleForGUI(TOOLTIP_WIDTH);
result.append("<div width=").append(width).append(">");

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()) {
appendLabelledValue(result, Messages.getString("FluffImageTooltip.insignia"), description, true);
}
}
result.append("</div></BODY></HTML>");
return result.toString();
} 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 ? "<BR>" : "";
result.append(UIUtil.spanCSS("label", lineBreak + label + " "))
.append(UIUtil.spanCSS("value", value));
}

private static String findInsignia(JsonNode contentNode) {
List<JsonNode> nodes = new ArrayList<>();
contentNode.iterator().forEachRemaining(nodes::add);
for (JsonNode node : nodes) {
if (node.has(NODE_TYPE) && node.get(NODE_TYPE).asText().equals(VALUE_INSIGNIA)) {
return node.get(NODE_CONTENT).asText();
}
}
return "";
}

private static Optional<File> getYamlFile(File imageFile) {
File parent = imageFile.getParentFile();
if (parent == null) {
LOGGER.warn("Image file {} has no parent directory; cannot search for YAML.", imageFile);
return Optional.empty();
}
try (Stream<Path> entries = Files.walk(parent.toPath(), 1)) {
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();
}
}

private static boolean isSuitableYamlFile(Path yamlFile, File imageFile) {
if (Files.isDirectory(yamlFile)) {
return false;
}

Path yamlFileNamePath = yamlFile.getFileName();
if (yamlFileNamePath == null) {
return false;
}

String yamlFileName = yamlFileNamePath.toString();
if (!yamlFileName.endsWith(YAML_FILE_SUFFIX)) {
return false;
}

int baseLength = yamlFileName.length() - YAML_FILE_SUFFIX.length();
if (baseLength <= 0) {
return false;
}

String baseName = yamlFileName.substring(0, baseLength);
return imageFile.getName().contains(baseName);
}

private FluffImageTooltip() {}
}
Loading
Loading