diff --git a/megamek/src/megamek/common/EntityListFile.java b/megamek/src/megamek/common/EntityListFile.java index 90e603a9c35..f6ba76297c6 100644 --- a/megamek/src/megamek/common/EntityListFile.java +++ b/megamek/src/megamek/common/EntityListFile.java @@ -19,20 +19,26 @@ import megamek.common.AmmoType.Munitions; import megamek.common.equipment.WeaponMounted; import megamek.common.force.Force; +import megamek.common.loaders.BLKFile; +import megamek.common.loaders.EntitySavingException; import megamek.common.options.OptionsConstants; import megamek.common.options.PilotOptions; import megamek.common.weapons.infantry.InfantryWeapon; +import megamek.logging.MMLogger; import megamek.utilities.xml.MMXMLUtility; import java.io.*; +import java.lang.reflect.Array; import java.nio.charset.StandardCharsets; import java.util.*; +import java.util.zip.GZIPOutputStream; /** * This class provides static methods to save a list of Entitys to, * and load a list of Entitys from a file. */ public class EntityListFile { + private static final MMLogger logger = MMLogger.create(EntityListFile.class); /** * Produce a string describing this armor value. Valid output values are any @@ -516,7 +522,7 @@ else if (isDestroyed) { * other campaign-related information are retained, but data specific to a * particular game is ignored. This method is a simpler version of the * overloaded method {@code saveTo}, with a default generic battle value of 0 - * (this causes GBV to be ignored). + * (this causes GBV to be ignored), and with unit embedding off. * * @param file * - The current contents of the file will be discarded and all @@ -528,7 +534,32 @@ else if (isDestroyed) { * - Is thrown on any error. */ public static void saveTo(File file, ArrayList list) throws IOException { - saveTo(file, list, 0); + saveTo(file, list, 0, false); + } + + /** + * Save the Entitys in the list to the given file. + *

+ * The Entitys' pilots, damage, ammo loads, ammo usage, and + * other campaign-related information are retained, but data specific to a + * particular game is ignored. This method is a simpler version of the + * overloaded method {@code saveTo}, with a default generic battle value of 0 + * (this causes GBV to be ignored). + * + * @param file + * - The current contents of the file will be discarded and all + * Entitys in the list will be written to the file. + * @param list + * - An ArrayList containing Entitys to be + * stored in a file. + * @param embedUnits + * - Set to true to embed the unit file of custom units (blk/mtf data) into the file. + * This allows the resulting file to be loaded by someone who doesn't have those custom units available. + * @throws IOException + * - Is thrown on any error. + */ + public static void saveTo(File file, ArrayList list, boolean embedUnits) throws IOException { + saveTo(file, list, 0, embedUnits); } /** @@ -537,6 +568,7 @@ public static void saveTo(File file, ArrayList list) throws IOException * The Entitys' pilots, damage, ammo loads, ammo usage, and * other campaign-related information are retained, but data specific to a * particular game is ignored. + * Unit embedding is off, see {@link #saveTo(File, ArrayList, int, boolean) the overloaded version of this function} * * @param file * - The current contents of the file will be discarded and all @@ -551,6 +583,32 @@ public static void saveTo(File file, ArrayList list) throws IOException * - Is thrown on any error. */ public static void saveTo(File file, ArrayList list, int genericBattleValue) throws IOException { + saveTo(file, list, genericBattleValue, false); + } + + /** + * Save the Entitys in the list to the given file. + *

+ * The Entitys' pilots, damage, ammo loads, ammo usage, and + * other campaign-related information are retained, but data specific to a + * particular game is ignored. + * + * @param file + * - The current contents of the file will be discarded and all + * Entitys in the list will be written to the file. + * @param list + * - A ArrayList containing Entitys to be + * stored in a file. + * @param genericBattleValue + * - An Integer representing the generic battle value. If it + * is greater than 0, it will be written into the XML. + * @param embedUnits + * - Set to true to embed the unit file of custom units (blk/mtf data) into the file. + * This allows the resulting file to be loaded by someone who doesn't have those custom units available. + * @throws IOException + * - Is thrown on any error. + */ + public static void saveTo(File file, ArrayList list, int genericBattleValue, boolean embedUnits) throws IOException { // Open up the file. Produce UTF-8 output. Writer output = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(file), StandardCharsets.UTF_8)); @@ -563,7 +621,7 @@ public static void saveTo(File file, ArrayList list, int genericBattleVa output.write("" + genericBattleValue + "\n\n"); } - writeEntityList(output, list); + writeEntityList(output, list, embedUnits); // Finish writing. output.write("\n"); @@ -731,6 +789,10 @@ private static void writeKills(Writer output, Hashtable kills) t } public static void writeEntityList(Writer output, ArrayList list) throws IOException { + writeEntityList(output, list, false); + } + + public static void writeEntityList(Writer output, ArrayList list, boolean embedUnits) throws IOException { // Walk through the list of entities. Iterator items = list.iterator(); while (items.hasNext()) { @@ -1176,6 +1238,42 @@ public static void writeEntityList(Writer output, ArrayList list) throws output.write("\"/>\n"); } + // Write out the mtf/blk file for the unit + String data = null; + if (embedUnits && !entity.isCanon()) { + if (entity instanceof Mek mek) { + data = mek.getMtf(); + } else { + try { + data = String.join("\n", BLKFile.getBlock(entity).getAllDataAsString()); + } catch (EntitySavingException e) { + logger.error("Error writing unit: {}", entity); + logger.error(e); + } + } + } + + if (data != null) { + String fileName = (entity.getChassis() + ' ' + entity.getModel()).trim(); + fileName = fileName.replaceAll("[/\\\\<>:\"|?*]", "_"); + fileName = fileName + ((entity instanceof Mek) ? ".mtf" : ".blk"); + + + output.write(indentStr(indentLvl + 1) + '<' + MULParser.ELE_CONSTRUCTION_DATA + ' ' + MULParser.ATTR_FILENAME + + "=\"" + fileName + "\">\n"); + + output.write(indentStr(indentLvl + 2)); + + var dataStream = new ByteArrayOutputStream(); + var ps = new PrintStream(new GZIPOutputStream(Base64.getEncoder().wrap(dataStream), true)); + ps.print(data); + ps.close(); + + output.write(dataStream.toString()); + output.write('\n' + indentStr(indentLvl + 1) + "\n"); + } + + // Finish writing this entity to the file. output.write(indentStr(indentLvl) + "\n\n"); diff --git a/megamek/src/megamek/common/MULParser.java b/megamek/src/megamek/common/MULParser.java index 1731944b714..1edc768b4a3 100644 --- a/megamek/src/megamek/common/MULParser.java +++ b/megamek/src/megamek/common/MULParser.java @@ -13,16 +13,9 @@ */ package megamek.common; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.Map; -import java.util.Objects; -import java.util.StringTokenizer; -import java.util.Vector; +import java.io.*; +import java.util.*; +import java.util.zip.GZIPInputStream; import javax.xml.parsers.DocumentBuilder; @@ -108,6 +101,7 @@ public class MULParser { public static final String ELE_BA_APM = "antiPersonnelMount"; public static final String ELE_LOADED = "loaded"; public static final String ELE_SHIP = "ship"; + public static final String ELE_CONSTRUCTION_DATA = "construction_data"; /** * The names of attributes generally associated with Entity tags @@ -238,6 +232,7 @@ public class MULParser { public static final String ATTR_GUNNERYAEROB = "gunneryAeroB"; public static final String ATTR_PILOTINGAERO = "pilotingAero"; public static final String ATTR_CREWTYPE = "crewType"; + public static final String ATTR_FILENAME = "filename"; /** * Special values recognized by this parser. @@ -540,8 +535,30 @@ private void parseEntity(final Element entityNode, final @Nullable GameOptions o String chassis = entityNode.getAttribute(ATTR_CHASSIS); String model = entityNode.getAttribute(ATTR_MODEL); - // Create a new entity - Entity entity = getEntity(chassis, model); + Entity entity = null; + + // Attempt to load the entity from the data embedded into the MUL file + try { + var cdnl = entityNode.getElementsByTagName(ELE_CONSTRUCTION_DATA); + if (cdnl.getLength() == 1) { + var cd = (Element) cdnl.item(0); + logger.info("Trying to load unit {} {} from embedded data instead of cache.", chassis, model); + + try ( + var rawStream = new ByteArrayInputStream(Base64.getDecoder().decode(cd.getTextContent().trim())); + var gzs = new GZIPInputStream(rawStream); + ) { + entity = new MekFileParser(gzs, cd.getAttribute(ATTR_FILENAME)).getEntity(); + } + } + } catch (Exception e) { + logger.error(e, "Failed to load unit from embedded data."); + } + + // Look for the entity in the unit cache if it couldn't be loaded. + if (entity == null) { + entity = getEntity(chassis, model); + } // Make sure we've got an Entity if (entity == null) {