diff --git a/megamek/src/megamek/client/ui/panes/ConfigurableMechViewPanel.java b/megamek/src/megamek/client/ui/panes/ConfigurableMechViewPanel.java index 06004849468..38a5bfabedf 100644 --- a/megamek/src/megamek/client/ui/panes/ConfigurableMechViewPanel.java +++ b/megamek/src/megamek/client/ui/panes/ConfigurableMechViewPanel.java @@ -27,6 +27,7 @@ import megamek.client.ui.swing.util.UIUtil; import megamek.common.Entity; import megamek.common.MechView; +import megamek.common.MechView.ViewFormatting; import megamek.common.annotations.Nullable; import javax.swing.*; @@ -62,8 +63,10 @@ public ConfigurableMechViewPanel(@Nullable Entity entity) { fontChooser.addActionListener(ev -> updateFont()); fontChooser.setSelectedItem(GUIPreferences.getInstance().getSummaryFont()); - copyHtmlButton.addActionListener(ev -> copyToClipboard(true)); - copyTextButton.addActionListener(ev -> copyToClipboard(false)); + copyHtmlButton.addActionListener(ev -> copyToClipboard(ViewFormatting.Html)); + copyTextButton.addActionListener(ev -> copyToClipboard(ViewFormatting.None)); + // todo: create a copyDiscordButton + // The implementer of the Discord export cared only about the MML UI. mulButton.addActionListener(ev -> UIUtil.showMUL(mulId, this)); mulButton.setToolTipText("Show the Master Unit List entry for this unit. Opens a browser window."); @@ -123,9 +126,9 @@ public void reset() { mechViewPanel.reset(); } - private void copyToClipboard(boolean asHtml) { + private void copyToClipboard(ViewFormatting formatting) { if (entity != null) { - MechView mechView = new MechView(entity, false, false, asHtml); + MechView mechView = new MechView(entity, false, false, formatting); StringSelection stringSelection = new StringSelection(mechView.getMechReadout()); Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(stringSelection, null); diff --git a/megamek/src/megamek/client/ui/panes/EntityViewPane.java b/megamek/src/megamek/client/ui/panes/EntityViewPane.java index 9597ab75867..c6a90090dc2 100644 --- a/megamek/src/megamek/client/ui/panes/EntityViewPane.java +++ b/megamek/src/megamek/client/ui/panes/EntityViewPane.java @@ -24,6 +24,8 @@ import megamek.client.ui.swing.calculationReport.FlexibleCalculationReport; import megamek.common.Entity; import megamek.common.GunEmplacement; +import megamek.common.MechView; +import megamek.common.MechView.ViewFormatting; import megamek.common.alphaStrike.ASCardDisplayable; import megamek.common.alphaStrike.AlphaStrikeElement; import megamek.common.alphaStrike.conversion.ASConverter; @@ -86,7 +88,7 @@ public void updateDisplayedEntity(final @Nullable Entity entity, @Nullable ASCar if (entity == null) { troPanel.reset(); } else { - troPanel.setMech(entity, TROView.createView(entity, true)); + troPanel.setMech(entity, TROView.createView(entity, ViewFormatting.Html)); } summaryPanel.setEntity(entity); cardPanel.setASElement(ASConverter.canConvert(entity) ? asUnit : null); diff --git a/megamek/src/megamek/common/Entity.java b/megamek/src/megamek/common/Entity.java index bbb20445fe6..d18eb0a98d7 100644 --- a/megamek/src/megamek/common/Entity.java +++ b/megamek/src/megamek/common/Entity.java @@ -22,6 +22,7 @@ import megamek.client.ui.swing.calculationReport.CalculationReport; import megamek.client.ui.swing.calculationReport.DummyCalculationReport; import megamek.codeUtilities.StringUtility; +import megamek.common.MechView.ViewFormatting; import megamek.common.MovePath.MoveStepType; import megamek.common.actions.*; import megamek.common.annotations.Nullable; @@ -37,6 +38,8 @@ import megamek.common.planetaryconditions.PlanetaryConditions; import megamek.common.planetaryconditions.Wind; import megamek.common.preference.PreferenceManager; +import megamek.common.util.DiscordExportUtil; +import megamek.common.util.DiscordExportUtil.DiscordFormat; import megamek.common.weapons.*; import megamek.common.weapons.bayweapons.AR10BayWeapon; import megamek.common.weapons.bayweapons.BayWeapon; @@ -8897,7 +8900,7 @@ public void resetTransporter() { @Override public String getUnusedString() { - return getUnusedString(false); + return getUnusedString(ViewFormatting.None); } @Override @@ -8928,8 +8931,8 @@ public double getUnused(Entity e) { * * @return A String meant for a human. */ - public String getUnusedString(boolean ishtml) { - StringBuffer result = new StringBuffer(); + public String getUnusedString(ViewFormatting formatting) { + StringBuilder result = new StringBuilder(); // Walk through this entity's transport components; // add all of their string to ours. @@ -8942,10 +8945,14 @@ public String getUnusedString(boolean ishtml) { if ((next instanceof DockingCollar) && ((DockingCollar) next).isDamaged()) { continue; } - if (ishtml && (next instanceof Bay) && (((Bay) next).getBayDamage() > 0)) { + if (formatting == ViewFormatting.Html && (next instanceof Bay) && (((Bay) next).getBayDamage() > 0)) { result.append("") .append(next.getUnusedString()) .append(""); + } else if (formatting == ViewFormatting.Discord && (next instanceof Bay) && (((Bay) next).getBayDamage() > 0)) { + result.append(DiscordFormat.RED.format()) + .append(next.getUnusedString()) + .append(DiscordFormat.RESET.format()); } else { result.append(next.getUnusedString()); } @@ -8959,7 +8966,7 @@ public String getUnusedString(boolean ishtml) { } // Add a newline character between strings. if (iter.hasMoreElements()) { - if (ishtml) { + if (formatting == ViewFormatting.Html) { result.append("
"); } else { result.append("\n"); diff --git a/megamek/src/megamek/common/MechView.java b/megamek/src/megamek/common/MechView.java index e73239e090f..b7e8eecd0b2 100644 --- a/megamek/src/megamek/common/MechView.java +++ b/megamek/src/megamek/common/MechView.java @@ -24,6 +24,7 @@ import megamek.common.eras.Era; import megamek.common.eras.Eras; import megamek.common.options.*; +import megamek.common.util.DiscordExportUtil.DiscordFormat; import megamek.common.verifier.*; import megamek.common.weapons.bayweapons.BayWeapon; import megamek.common.weapons.infantry.InfantryWeapon; @@ -44,6 +45,11 @@ * @since January 20, 2003 */ public class MechView { + public static enum ViewFormatting { + Html, + None, + Discord + } /** * Provides common interface for various ways to present data that can be formatted @@ -59,6 +65,7 @@ public class MechView { interface ViewElement { String toPlainText(); String toHTML(); + String toDiscord(); } private Entity entity; @@ -84,7 +91,7 @@ interface ViewElement { private List sFluff = new ArrayList<>(); private List sInvalid = new ArrayList<>(); - private final boolean html; + private final ViewFormatting formatting; /** * Compiles information about an {@link Entity} useful for showing a summary of its abilities. @@ -94,7 +101,7 @@ interface ViewElement { * @param showDetail If true, shows individual weapons that make up weapon bays. */ public MechView(Entity entity, boolean showDetail) { - this(entity, showDetail, false, true); + this(entity, showDetail, false, ViewFormatting.Html); } /** @@ -107,7 +114,7 @@ public MechView(Entity entity, boolean showDetail) { * equipment-only cost for conventional infantry for MekHQ. */ public MechView(Entity entity, boolean showDetail, boolean useAlternateCost) { - this(entity, showDetail, useAlternateCost, true); + this(entity, showDetail, useAlternateCost, ViewFormatting.Html); } /** @@ -121,8 +128,8 @@ public MechView(Entity entity, boolean showDetail, boolean useAlternateCost) { * as plain text. */ public MechView(final Entity entity, final boolean showDetail, final boolean useAlternateCost, - final boolean html) { - this(entity, showDetail, useAlternateCost, (entity.getCrew() == null), html); + final ViewFormatting formatting) { + this(entity, showDetail, useAlternateCost, (entity.getCrew() == null), formatting); } /** @@ -138,9 +145,9 @@ public MechView(final Entity entity, final boolean showDetail, final boolean use * as plain text. */ public MechView(final Entity entity, final boolean showDetail, final boolean useAlternateCost, - final boolean ignorePilotBV, final boolean html) { + final boolean ignorePilotBV, final ViewFormatting formatting) { this.entity = entity; - this.html = html; + this.formatting = formatting; isMech = entity instanceof Mech; isInf = entity instanceof Infantry; isBA = entity instanceof BattleArmor; @@ -576,8 +583,20 @@ private String eraText(int startYear, int endYear) { * @return The formatted data. */ private String getReadout(List section) { - Function mapper = html? - ViewElement::toHTML : ViewElement::toPlainText; + Function mapper; + switch (formatting) { + case Html: + mapper = ViewElement::toHTML; + break; + case None: + mapper = ViewElement::toPlainText; + break; + case Discord: + mapper = ViewElement::toDiscord; + break; + default: + throw new IllegalStateException("Impossible"); + } return section.stream().map(mapper).collect(Collectors.joining()); } @@ -620,6 +639,9 @@ public String getMechReadoutLoadout() { * @return The data from the fluff section. */ public String getMechReadoutFluff() { + if (formatting == ViewFormatting.Discord) { + return ""; + } return getReadout(sFluff); } @@ -639,11 +661,14 @@ public String getMechReadout(@Nullable String fontName) { String preStart = ""; String preEnd = ""; - if (html && (fontName != null)) { + if (formatting == ViewFormatting.Html && (fontName != null)) { docStart = "
"; docEnd = "
"; preStart = "
";
             preEnd = "
"; + } else if (formatting == ViewFormatting.Discord) { + docStart = "```ansi\n"; + docEnd = "```"; } return docStart + getMechReadoutHead() + getMechReadoutBasic() + getMechReadoutLoadout() @@ -714,12 +739,12 @@ private List getInternalAndArmor() { } String[] row = {entity.getLocationName(loc), - renderArmor(entity.getInternalForReal(loc), entity.getOInternal(loc), html), + renderArmor(entity.getInternalForReal(loc), entity.getOInternal(loc), formatting), "", "", "" }; if (IArmorState.ARMOR_NA != entity.getArmorForReal(loc)) { row[2] = renderArmor(entity.getArmorForReal(loc), - entity.getOArmor(loc), html); + entity.getOArmor(loc), formatting); } if (entity.hasPatchworkArmor()) { row[3] = ArmorType.forEntity(entity, loc).getName(); @@ -731,7 +756,7 @@ private List getInternalAndArmor() { if (entity.hasRearArmor(loc)) { row = new String[] { entity.getLocationName(loc) + " (rear)", "", renderArmor(entity.getArmorForReal(loc, true), - entity.getOArmor(loc, true), html), "", ""}; + entity.getOArmor(loc, true), formatting), "", ""}; locTable.addRow(row); } } @@ -746,7 +771,7 @@ private List getSIandArmor() { List retVal = new ArrayList<>(); retVal.add(new LabeledElement(Messages.getString("MechView.SI"), - renderArmor(a.getSI(), a.get0SI(), html))); + renderArmor(a.getSI(), a.get0SI(), formatting))); // if it is a jumpship get sail and KF integrity if (isJumpship) { @@ -800,7 +825,7 @@ private List getSIandArmor() { String[] row = { entity.getLocationName(loc), "", "" }; if (IArmorState.ARMOR_NA != entity.getArmor(loc)) { row[1] = renderArmor(entity.getArmor(loc), - entity.getOArmor(loc), html); + entity.getOArmor(loc), formatting); } if (entity.hasPatchworkArmor()) { row[2] = Messages.getString("MechView." @@ -1086,14 +1111,14 @@ private List getMisc() { retVal.add(miscTable); } - String transportersString = entity.getUnusedString(html); + String transportersString = entity.getUnusedString(formatting); if (!transportersString.isBlank()) { retVal.add(new SingleLine()); // Reformat the list to a table to keep the formatting similar between blocks TableElement transportTable = new TableElement(1); transportTable.setColNames(Messages.getString("MechView.CarryingCapacity")); transportTable.setJustification(TableElement.JUSTIFIED_LEFT); - String separator = html ? "
" : "\r\n"; + String separator = formatting == ViewFormatting.Html ? "
" : "\n"; String[] transportersLines = transportersString.split(separator); for (String line : transportersLines) { transportTable.addRow(line); @@ -1144,22 +1169,44 @@ private ViewElement getFailed() { return new EmptyElement(); } - private static String renderArmor(int nArmor, int origArmor, boolean html) { + private static String renderArmor(int nArmor, int origArmor, ViewFormatting formatting) { double percentRemaining = ((double) nArmor) / ((double) origArmor); String armor = Integer.toString(nArmor); - if (!html) { - if (percentRemaining < 0) { - return "X"; - } else { - return armor; - } + + String warnBegin; + String warnEnd; + String cautionBegin; + String cautionEnd; + + switch (formatting) { + case Html: + warnBegin = "'; + warnEnd = ""; + cautionBegin = "'; + cautionEnd = ""; + break; + case None: + warnBegin = ""; + warnEnd = ""; + cautionBegin = ""; + cautionEnd = ""; + break; + case Discord: + warnBegin = DiscordFormat.RED.format(); + warnEnd = DiscordFormat.RESET.format(); + cautionBegin = DiscordFormat.YELLOW.format(); + cautionEnd = DiscordFormat.RESET.format(); + break; + default: + throw new IllegalStateException("Impossible"); } + if (percentRemaining < 0) { - return "X"; + return warnBegin + 'X' + warnEnd; } else if (percentRemaining <= .25) { - return "" + armor + ""; + return warnBegin + armor + warnEnd; } else if (percentRemaining < 1.00) { - return "" + armor + ""; + return cautionBegin + armor + cautionEnd; } else { return armor; } @@ -1180,10 +1227,15 @@ public String toHTML() { return ""; } + @Override + public String toDiscord() { + return ""; + } + } /** - * Basic one-line entry consisting of a label, a colon, and a value. In html the label is bold. + * Basic one-line entry consisting of a label, a colon, and a value. In html and discord the label is bold. * */ private static class LabeledElement implements ViewElement { @@ -1201,19 +1253,28 @@ public String toPlainText() { .replaceAll("<[Pp]> *", "\n\n") .replaceAll(" *", "\n") .replaceAll("<[^>]*>", ""); - return label + ": " + htmlCleanedText + "\n"; + return label + ": " + htmlCleanedText + '\n'; } @Override public String toHTML() { return "" + label + ": " + value + "
"; } + + @Override + public String toDiscord() { + String htmlCleanedText = value.replaceAll("<[Bb][Rr]> *", "\n") + .replaceAll("<[Pp]> *", "\n\n") + .replaceAll(" *", "\n") + .replaceAll("<[^>]*>", ""); + return DiscordFormat.BOLD.format() + label + DiscordFormat.RESET.format() + ": " + htmlCleanedText + '\n'; + } } /** * Data laid out in a table with named columns. The columns are left-justified by default, * but justification can be set for columns individually. Plain text output requires a monospace - * font to line up correctly. For HTML output the background color of an individual row can be set. + * font to line up correctly. For HTML and discord output the background color of an individual row can be set. * */ private static class TableElement implements ViewElement { @@ -1374,10 +1435,47 @@ public String toHTML() { sb.append("\n"); return sb.toString(); } + + @Override + public String toDiscord() { + final String COL_PADDING = " "; + StringBuilder sb = new StringBuilder(); + sb.append(DiscordFormat.UNDERLINE.format()); + for (int col = 0; col < colNames.length; col++) { + sb.append(justify(justification[col], colNames[col], colWidth.get(col))); + if (col < colNames.length - 1) { + sb.append(COL_PADDING); + } + } + sb.append(DiscordFormat.RESET.format()); + sb.append("\n"); + for (int r = 0; r < data.size(); r++) { + if (colors.containsKey(r)) { + try { + sb.append(DiscordFormat.valueOf(colors.get(r).toUpperCase()).format()); + } catch (IllegalArgumentException ignored) {} + } + final String[] row = data.get(r); + for (int col = 0; col < row.length; col++) { + sb.append(justify(justification[col], row[col], colWidth.get(col))); + if (col < row.length - 1) { + sb.append(COL_PADDING); + } + } + if (colors.containsKey(r)) { + try { + var ignored = DiscordFormat.valueOf(colors.get(r).toUpperCase()).format(); + sb.append(DiscordFormat.RESET.format()); + } catch (IllegalArgumentException ignored) {} + } + sb.append("\n"); + } + return sb.toString(); + } } /** - * Displays a label (bold for html output) followed by a column of items + * Displays a label (bold for html and discord output) followed by a column of items * */ private static class ItemList implements ViewElement { @@ -1417,6 +1515,18 @@ public String toHTML() { } return sb.toString(); } + + @Override + public String toDiscord() { + StringBuilder sb = new StringBuilder(); + if (null != heading) { + sb.append(DiscordFormat.BOLD.format()).append(heading).append(DiscordFormat.RESET.format()).append('\n'); + } + for (String item : data) { + sb.append(item).append("\n"); + } + return sb.toString(); + } } /** @@ -1443,6 +1553,11 @@ public String toPlainText() { public String toHTML() { return value + "
\n"; } + + @Override + public String toDiscord() { + return toPlainText(); + } } /** @@ -1477,6 +1592,12 @@ public String toHTML() { String result = label.isBlank() ? "" : "" + label + ": "; return result + "" + displayText + "
"; } + + @Override + public String toDiscord() { + String result = label.isBlank() ? "" : DiscordFormat.BOLD.format() + label + ": " + DiscordFormat.RESET.format(); + return result + displayText + "\n"; + } } /** @@ -1499,6 +1620,11 @@ public String toPlainText() { public String toHTML() { return "" + title + "
\n"; } + + @Override + public String toDiscord() { + return DiscordFormat.BOLD.format() + DiscordFormat.UNDERLINE.format() + title + DiscordFormat.RESET.format() + '\n'; + } } /** @@ -1507,10 +1633,15 @@ public String toHTML() { * @return A String that is used to mark the beginning of a warning. */ private String warningStart() { - if (html) { - return ""; - } else { - return "*"; + switch (formatting) { + case Html: + return ""; + case None: + return "*"; + case Discord: + return DiscordFormat.RED.format(); + default: + throw new IllegalStateException("Impossible"); } } @@ -1519,10 +1650,15 @@ private String warningStart() { * @return A String that is used to mark the end of a warning. */ private String warningEnd() { - if (html) { - return ""; - } else { - return "*"; + switch (formatting) { + case Html: + return ""; + case None: + return "*"; + case Discord: + return DiscordFormat.RESET.format(); + default: + throw new IllegalStateException("Impossible"); } } @@ -1532,10 +1668,15 @@ private String warningEnd() { * @return The starting element for italicized text. */ private String italicsStart() { - if (html) { - return ""; - } else { - return ""; + switch (formatting) { + case Html: + return ""; + case None: + return ""; + case Discord: + return DiscordFormat.UNDERLINE.format(); + default: + throw new IllegalStateException("Impossible"); } } @@ -1544,10 +1685,15 @@ private String italicsStart() { * @return The ending element for italicized text. */ private String italicsEnd() { - if (html) { - return ""; - } else { - return ""; + switch (formatting) { + case Html: + return ""; + case None: + return ""; + case Discord: + return DiscordFormat.RESET.format(); + default: + throw new IllegalStateException("Impossible"); } } } diff --git a/megamek/src/megamek/common/templates/TROView.java b/megamek/src/megamek/common/templates/TROView.java index b9aa374e8e1..a30b980d20e 100644 --- a/megamek/src/megamek/common/templates/TROView.java +++ b/megamek/src/megamek/common/templates/TROView.java @@ -53,7 +53,7 @@ public class TROView { protected TROView() { } - public static TROView createView(Entity entity, boolean html) { + public static TROView createView(Entity entity, MechView.ViewFormatting formatting) { TROView view; if (entity.hasETypeFlag(Entity.ETYPE_MECH)) { view = new MechTROView((Mech) entity); @@ -77,10 +77,10 @@ public static TROView createView(Entity entity, boolean html) { } else { view = new TROView(); } - if (null != view.getTemplateFileName(html)) { + if (null != view.getTemplateFileName(formatting == MechView.ViewFormatting.Html)) { try { view.template = TemplateConfiguration.getInstance() - .getTemplate("tro/" + view.getTemplateFileName(html)); + .getTemplate("tro/" + view.getTemplateFileName(formatting == MechView.ViewFormatting.Html)); } catch (final IOException e) { LogManager.getLogger().error("", e); } diff --git a/megamek/src/megamek/common/util/DiscordExportUtil.java b/megamek/src/megamek/common/util/DiscordExportUtil.java new file mode 100644 index 00000000000..24739de586d --- /dev/null +++ b/megamek/src/megamek/common/util/DiscordExportUtil.java @@ -0,0 +1,32 @@ +package megamek.common.util; + +public class DiscordExportUtil { + private DiscordExportUtil() {} + + private static final char esc = '\u001b'; + + public static enum DiscordFormat { + GRAY(30), + RED(31), + GREEN(32), + YELLOW(33), + BLUE(34), + PINK(35), + CYAN(36), + WHITE(37), + + BOLD(1), + UNDERLINE(4), + + RESET(0); + + private int code; + DiscordFormat(int code) { + this.code = code; + } + + public String format() { + return esc + "[" + code + 'm'; + } + } +}