diff --git a/megamek/src/megamek/client/ui/panes/ConfigurableMechViewPanel.java b/megamek/src/megamek/client/ui/panes/ConfigurableMechViewPanel.java
index 06004849468..88bd70fa08f 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.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..f52c8306148 100644
--- a/megamek/src/megamek/client/ui/panes/EntityViewPane.java
+++ b/megamek/src/megamek/client/ui/panes/EntityViewPane.java
@@ -23,7 +23,7 @@
import megamek.client.ui.swing.alphaStrike.ConfigurableASCardPanel;
import megamek.client.ui.swing.calculationReport.FlexibleCalculationReport;
import megamek.common.Entity;
-import megamek.common.GunEmplacement;
+import megamek.common.ViewFormatting;
import megamek.common.alphaStrike.ASCardDisplayable;
import megamek.common.alphaStrike.AlphaStrikeElement;
import megamek.common.alphaStrike.conversion.ASConverter;
@@ -86,7 +86,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..e0f86b4a159 100644
--- a/megamek/src/megamek/common/Entity.java
+++ b/megamek/src/megamek/common/Entity.java
@@ -37,6 +37,7 @@
import megamek.common.planetaryconditions.PlanetaryConditions;
import megamek.common.planetaryconditions.Wind;
import megamek.common.preference.PreferenceManager;
+import megamek.common.util.DiscordFormat;
import megamek.common.weapons.*;
import megamek.common.weapons.bayweapons.AR10BayWeapon;
import megamek.common.weapons.bayweapons.BayWeapon;
@@ -8897,7 +8898,7 @@ public void resetTransporter() {
@Override
public String getUnusedString() {
- return getUnusedString(false);
+ return getUnusedString(ViewFormatting.NONE);
}
@Override
@@ -8928,8 +8929,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 +8943,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 +8964,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..20994b8c7ff 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.DiscordFormat;
import megamek.common.verifier.*;
import megamek.common.weapons.bayweapons.BayWeapon;
import megamek.common.weapons.infantry.InfantryWeapon;
@@ -59,6 +60,7 @@ public class MechView {
interface ViewElement {
String toPlainText();
String toHTML();
+ String toDiscord();
}
private Entity entity;
@@ -84,7 +86,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 +96,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 +109,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 +123,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 +140,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 +578,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 +634,9 @@ public String getMechReadoutLoadout() {
* @return The data from the fluff section.
*/
public String getMechReadoutFluff() {
+ if (formatting == ViewFormatting.DISCORD) {
+ return "";
+ }
return getReadout(sFluff);
}
@@ -639,11 +656,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 +734,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 +751,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 +766,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 +820,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 +1106,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 +1164,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 +1222,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 +1248,28 @@ public String toPlainText() {
.replaceAll("<[Pp]> *", "\n\n")
.replaceAll("[Pp]> *", "\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("[Pp]> *", "\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 +1430,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 +1510,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 +1548,11 @@ public String toPlainText() {
public String toHTML() {
return value + "
\n";
}
+
+ @Override
+ public String toDiscord() {
+ return toPlainText();
+ }
}
/**
@@ -1477,6 +1587,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 +1615,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 +1628,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 +1645,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 +1663,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 +1680,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/ViewFormatting.java b/megamek/src/megamek/common/ViewFormatting.java
new file mode 100644
index 00000000000..bbfc72e11a4
--- /dev/null
+++ b/megamek/src/megamek/common/ViewFormatting.java
@@ -0,0 +1,24 @@
+/*
+ * 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.common;
+
+public enum ViewFormatting {
+ HTML, NONE, DISCORD
+}
diff --git a/megamek/src/megamek/common/templates/TROView.java b/megamek/src/megamek/common/templates/TROView.java
index b9aa374e8e1..58d4d411a0f 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, 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 == ViewFormatting.HTML)) {
try {
view.template = TemplateConfiguration.getInstance()
- .getTemplate("tro/" + view.getTemplateFileName(html));
+ .getTemplate("tro/" + view.getTemplateFileName(formatting == ViewFormatting.HTML));
} catch (final IOException e) {
LogManager.getLogger().error("", e);
}
diff --git a/megamek/src/megamek/common/util/DiscordFormat.java b/megamek/src/megamek/common/util/DiscordFormat.java
new file mode 100644
index 00000000000..54a43526f9d
--- /dev/null
+++ b/megamek/src/megamek/common/util/DiscordFormat.java
@@ -0,0 +1,37 @@
+/*
+ * 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.common.util;
+
+public 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 final int code;
+
+ DiscordFormat(int code) {
+ this.code = code;
+ }
+
+ public String format() {
+ return "\u001b[" + code + 'm';
+ }
+}