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("[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 +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';
+ }
+ }
+}