Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Load units embedded into MUL file #6163

Merged
merged 1 commit into from
Nov 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
104 changes: 101 additions & 3 deletions megamek/src/megamek/common/EntityListFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>Entity</code>s to,
* and load a list of <code>Entity</code>s 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
Expand Down Expand Up @@ -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
Expand All @@ -528,7 +534,32 @@ else if (isDestroyed) {
* - Is thrown on any error.
*/
public static void saveTo(File file, ArrayList<Entity> list) throws IOException {
saveTo(file, list, 0);
saveTo(file, list, 0, false);
}

/**
* Save the <code>Entity</code>s in the list to the given file.
* <p>
* The <code>Entity</code>s' 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
* <code>Entity</code>s in the list will be written to the file.
* @param list
* - An <code>ArrayList</code> containing <code>Entity</code>s to be
* stored in a file.
* @param embedUnits
* - Set to <code>true</code> 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<Entity> list, boolean embedUnits) throws IOException {
saveTo(file, list, 0, embedUnits);
}

/**
Expand All @@ -537,6 +568,7 @@ public static void saveTo(File file, ArrayList<Entity> list) throws IOException
* The <code>Entity</code>s' 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
Expand All @@ -551,6 +583,32 @@ public static void saveTo(File file, ArrayList<Entity> list) throws IOException
* - Is thrown on any error.
*/
public static void saveTo(File file, ArrayList<Entity> list, int genericBattleValue) throws IOException {
saveTo(file, list, genericBattleValue, false);
}

/**
* Save the <code>Entity</code>s in the list to the given file.
* <p>
* The <code>Entity</code>s' 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
* <code>Entity</code>s in the list will be written to the file.
* @param list
* - A <code>ArrayList</code> containing <code>Entity</code>s to be
* stored in a file.
* @param genericBattleValue
* - An <code>Integer</code> representing the generic battle value. If it
* is greater than 0, it will be written into the XML.
* @param embedUnits
* - Set to <code>true</code> 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<Entity> 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));
Expand All @@ -563,7 +621,7 @@ public static void saveTo(File file, ArrayList<Entity> list, int genericBattleVa
output.write("<TotalGenericBattleValue>" + genericBattleValue + "</TotalGenericBattleValue>\n\n");
}

writeEntityList(output, list);
writeEntityList(output, list, embedUnits);

// Finish writing.
output.write("</" + MULParser.ELE_UNIT + ">\n");
Expand Down Expand Up @@ -731,6 +789,10 @@ private static void writeKills(Writer output, Hashtable<String, String> kills) t
}

public static void writeEntityList(Writer output, ArrayList<Entity> list) throws IOException {
writeEntityList(output, list, false);
}

public static void writeEntityList(Writer output, ArrayList<Entity> list, boolean embedUnits) throws IOException {
// Walk through the list of entities.
Iterator<Entity> items = list.iterator();
while (items.hasNext()) {
Expand Down Expand Up @@ -1176,6 +1238,42 @@ public static void writeEntityList(Writer output, ArrayList<Entity> 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) + "</" + MULParser.ELE_CONSTRUCTION_DATA + ">\n");
}


// Finish writing this entity to the file.
output.write(indentStr(indentLvl) + "</" + MULParser.ELE_ENTITY + ">\n\n");

Expand Down
41 changes: 29 additions & 12 deletions megamek/src/megamek/common/MULParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down