package com.kartoflane.superluminal2.utils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.HashMap;
import java.util.Locale;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.jdom2.Comment;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.input.JDOMParseException;
import com.kartoflane.superluminal2.components.Tuple;
import com.kartoflane.superluminal2.components.enums.Images;
import com.kartoflane.superluminal2.components.enums.LayoutObjects;
import com.kartoflane.superluminal2.components.enums.Races;
import com.kartoflane.superluminal2.components.enums.Systems;
import com.kartoflane.superluminal2.core.Database;
import com.kartoflane.superluminal2.core.DatabaseEntry;
import com.kartoflane.superluminal2.core.Manager;
import com.kartoflane.superluminal2.ftl.AugmentObject;
import com.kartoflane.superluminal2.ftl.DoorObject;
import com.kartoflane.superluminal2.ftl.DroneList;
import com.kartoflane.superluminal2.ftl.DroneObject;
import com.kartoflane.superluminal2.ftl.GibObject;
import com.kartoflane.superluminal2.ftl.GlowObject;
import com.kartoflane.superluminal2.ftl.GlowSet;
import com.kartoflane.superluminal2.ftl.GlowSet.Glows;
import com.kartoflane.superluminal2.ftl.ImageObject;
import com.kartoflane.superluminal2.ftl.MountObject;
import com.kartoflane.superluminal2.ftl.RoomObject;
import com.kartoflane.superluminal2.ftl.ShipObject;
import com.kartoflane.superluminal2.ftl.StationObject;
import com.kartoflane.superluminal2.ftl.SystemObject;
import com.kartoflane.superluminal2.ftl.WeaponList;
import com.kartoflane.superluminal2.ftl.WeaponObject;
import com.kartoflane.superluminal2.ui.ShipContainer;
/**
* This class contains utility methods used to save a ship in a form that mirrors
* the game's own files.
*
* @author kartoFlane
*
*/
public class ShipSaveUtils {
private static final Logger log = LogManager.getLogger(ShipSaveUtils.class);
public static void saveShipFTL(File saveFile, ShipContainer container) throws IllegalArgumentException, IOException {
if (saveFile == null)
throw new IllegalArgumentException("Destination file must not be null.");
if (saveFile.isDirectory())
throw new IllegalArgumentException("Not a file: " + saveFile.getName());
HashMap<String, byte[]> fileMap = saveShip(container);
IOUtils.writeZip(fileMap, saveFile);
}
public static void saveShipXML(File destination, ShipContainer container) throws IllegalArgumentException, IOException {
if (destination == null)
throw new IllegalArgumentException("Destination file must not be null.");
if (!destination.isDirectory())
throw new IllegalArgumentException("Not a directory: " + destination.getName());
HashMap<String, byte[]> fileMap = saveShip(container);
IOUtils.writeDir(fileMap, destination);
}
/**
* Saves the ship within the context of the specified database entry, as the specified file.
*
* @param destination the output file.
* @param entry the DatabaseEntry (runtime representation of an .ftl mod) within which the ship is to be saved.
* @param container the ShipContainer to be saved.
*/
public static void saveShipModFTL(File destination, DatabaseEntry entry, ShipContainer container)
throws IllegalArgumentException, IOException, JDOMParseException {
if (destination == null)
throw new IllegalArgumentException("Destination file must not be null.");
if (destination.isDirectory())
throw new IllegalArgumentException("Not a file: " + destination.getName());
HashMap<String, byte[]> entryMap = IOUtils.readEntry(entry);
IOUtils.merge(entryMap, container);
IOUtils.writeZip(entryMap, destination);
}
public static void saveShipModXML(File destination, DatabaseEntry entry, ShipContainer container)
throws IllegalArgumentException, IOException, JDOMParseException {
if (destination == null)
throw new IllegalArgumentException("Destination file must not be null.");
if (!destination.isDirectory())
throw new IllegalArgumentException("Not a directory: " + destination.getName());
HashMap<String, byte[]> entryMap = IOUtils.readEntry(entry);
IOUtils.merge(entryMap, container);
IOUtils.writeDir(entryMap, destination);
}
/**
* Saves the given ship as a HashMap, with keys denoting the inner pth and file name, and
* values being the file's contents as bytes.
*
* @param container
* the ship to be saved
* @return a HashMap that is a complete representation of the ship in the file system
*
* @throws IOException
*/
public static HashMap<String, byte[]> saveShip(ShipContainer container) throws IOException {
if (container == null)
throw new IllegalArgumentException("ShipContainer must not be null.");
ShipObject ship = container.getShipController().getGameObject();
if (ship == null)
throw new IllegalArgumentException("Ship object must not be null.");
container.updateGameObjects();
ship.coalesceRooms();
ship.coalesceGibs();
// Remember door links and recover them later -- linking doors automatically persists after saving
// is completed, which can cause bugs when the user moves the doors/rooms around and saves again
HashMap<DoorObject, Tuple<RoomObject, RoomObject>> doorLinkMap = new HashMap<DoorObject, Tuple<RoomObject, RoomObject>>();
for (DoorObject d : ship.getDoors())
doorLinkMap.put(d, new Tuple<RoomObject, RoomObject>(d.getLeftRoom(), d.getRightRoom()));
ship.linkDoors();
HashMap<String, byte[]> fileMap = new HashMap<String, byte[]>();
String fileName = null;
byte[] bytes = null;
// Create the files in memory
fileName = "data/" + Database.getInstance().getAssociatedFile(ship.getBlueprintName()) + ".append";
bytes = IOUtils.readDocument(generateBlueprintXML(ship)).getBytes();
fileMap.put(fileName, bytes);
fileName = "data/" + ship.getLayout() + ".txt";
bytes = generateLayoutTXT(ship).getBytes();
fileMap.put(fileName, bytes);
fileName = "data/" + ship.getLayout() + ".xml";
bytes = IOUtils.readDocument(generateLayoutXML(ship)).getBytes();
fileMap.put(fileName, bytes);
if (ship.isPlayerShip()) {
fileName = "data/rooms.xml.append";
bytes = IOUtils.readDocument(generateRoomsXML(ship)).getBytes();
fileMap.put(fileName, bytes);
}
// Recover door links
for (DoorObject d : ship.getDoors()) {
d.setLeftRoom(doorLinkMap.get(d).getKey());
d.setRightRoom(doorLinkMap.get(d).getValue());
}
// Copy images
for (Images img : Images.getShipImages()) {
if (!img.shouldSave(ship))
continue;
ImageObject object = ship.getImage(img);
String path = object.getImagePath();
if (path != null) {
if (isImageCorrupt(path))
continue;
InputStream is = null;
try {
is = Manager.getInputStream(path);
fileName = img.getDatRelativePath(ship) + img.getPrefix() + ship.getImageNamespace() + img.getSuffix() + ".png";
fileMap.put(fileName, IOUtils.readStream(is));
} catch (FileNotFoundException e) {
log.warn(String.format("File for %s image could not be found: %s", img, path));
} finally {
if (is != null)
is.close();
}
}
}
if (ship.isPlayerShip()) {
for (Systems sys : Systems.getSystems()) {
for (SystemObject system : ship.getSystems(sys)) {
String path = system.getInteriorPath();
if (path != null && system.isAssigned()) {
if (isImageCorrupt(path))
continue;
InputStream is = null;
try {
is = Manager.getInputStream(path);
fileName = "img/ship/interior/" + system.getInteriorNamespace() + ".png";
fileMap.put(fileName, IOUtils.readStream(is));
} catch (FileNotFoundException e) {
log.warn(String.format("File for %s interior image could not be found: %s", sys, path));
} finally {
if (is != null)
is.close();
}
}
if (sys.canContainGlow() && sys.canContainStation()) {
GlowObject glow = system.getGlow();
GlowSet set = glow.getGlowSet();
for (Glows glowId : Glows.getGlows()) {
path = set.getImage(glowId);
if (path != null) {
if (isImageCorrupt(path))
continue;
InputStream is = null;
try {
is = Manager.getInputStream(path);
fileName = "img/ship/interior/" + set.getIdentifier() + glowId.getSuffix() + ".png";
fileMap.put(fileName, IOUtils.readStream(is));
} catch (FileNotFoundException e) {
log.warn(String.format("File for %s's %s glow image could not be found: %s", glow.getIdentifier(), glowId, path));
} finally {
if (is != null)
is.close();
}
}
}
}
}
}
SystemObject cloaking = ship.getSystem(Systems.CLOAKING);
GlowSet cloakSet = cloaking.getGlow().getGlowSet();
if (cloakSet != Database.DEFAULT_GLOW_SET) {
String path = cloakSet.getImage(Glows.CLOAK);
if (path != null && !isImageCorrupt(path)) {
InputStream is = null;
try {
is = Manager.getInputStream(path);
fileName = "img/ship/interior/" + cloaking.getInteriorNamespace() + Glows.CLOAK.getSuffix() + ".png";
fileMap.put(fileName, IOUtils.readStream(is));
} catch (FileNotFoundException e) {
log.warn("File for cloaking glow image could not be found: " + path);
} finally {
if (is != null)
is.close();
}
}
}
}
for (int i = 1; i <= ship.getGibs().length; i++) {
GibObject gib = ship.getGibById(i);
if (gib == null)
throw new IllegalStateException("Missing gib object with id " + i);
String path = gib.getImagePath();
if (path != null) {
if (isImageCorrupt(path))
continue;
InputStream is = null;
try {
is = Manager.getInputStream(path);
String datRelativePath = ship.isPlayerShip() ? "img/ship/" : "img/ships_glow/";
fileName = datRelativePath + ship.getImageNamespace() + "_gib" + gib.getId() + ".png";
fileMap.put(fileName, IOUtils.readStream(is));
} catch (FileNotFoundException e) {
log.warn(String.format("File for gib #%s could not be found: %s", gib.getId(), path));
} finally {
if (is != null)
is.close();
}
}
}
return fileMap;
}
public static void saveLayoutTXT(ShipObject ship, File f) throws FileNotFoundException, IOException {
if (ship == null)
throw new IllegalArgumentException("Ship object must not be null.");
if (f == null)
throw new IllegalArgumentException("File must not be null.");
FileWriter writer = null;
try {
writer = new FileWriter(f);
writer.write(generateLayoutTXT(ship));
} finally {
if (writer != null)
writer.close();
}
}
public static void saveLayoutXML(ShipObject ship, File f) throws IllegalArgumentException, IOException {
if (ship == null)
throw new IllegalArgumentException("Ship object must not be null.");
if (f == null)
throw new IllegalArgumentException("File must not be null.");
IOUtils.writeFileXML(generateLayoutXML(ship), f);
}
/**
* This method generates the shipBlueprint tag that describes the ship passed as argument.
*
* @param ship
* the ship to be saved
* @return the XML document containing the shipBlueprint tag
*/
public static Document generateBlueprintXML(ShipObject ship) {
Document doc = new Document();
Element root = new Element("wrapper");
Element e = null;
String attr = null;
Element shipBlueprint = new Element("shipBlueprint");
attr = ship.getBlueprintName();
shipBlueprint.setAttribute("name", attr == null ? "" : attr);
attr = ship.getLayout();
shipBlueprint.setAttribute("layout", attr == null ? "" : attr);
attr = ship.getImageNamespace();
shipBlueprint.setAttribute("img", attr == null ? "" : attr);
// The ship's class name, used for flavor only on player ships,
// but on enemy ships it is used as the enemy ship's name
e = new Element("class");
attr = ship.getShipClass();
e.setText(attr == null ? "" : attr);
shipBlueprint.addContent(e);
// Name and description only affect player ships
if (ship.isPlayerShip()) {
e = new Element("name");
attr = ship.getShipName();
e.setText(attr == null ? "" : attr);
shipBlueprint.addContent(e);
e = new Element("desc");
attr = ship.getShipDescription();
e.setText(attr == null ? "" : attr);
shipBlueprint.addContent(e);
}
// Sector tags
// Enemy exclusive
else {
e = new Element("minSector");
e.setText("" + ship.getMinSector());
shipBlueprint.addContent(e);
e = new Element("maxSector");
e.setText("" + ship.getMaxSector());
shipBlueprint.addContent(e);
}
Element systemList = new Element("systemList");
for (Systems sys : Systems.getSystems()) {
for (SystemObject system : ship.getSystems(sys)) {
if (system.isAssigned()) {
Element sysEl = new Element(sys.toString().toLowerCase());
sysEl.setAttribute("power", "" + system.getLevelStart());
// Enemy ships' system have a 'max' attribute which determines the max level of the system
if (!ship.isPlayerShip())
sysEl.setAttribute("max", "" + system.getLevelMax());
sysEl.setAttribute("room", "" + system.getRoom().getId());
sysEl.setAttribute("start", "" + system.isAvailable());
// Artillery has a special 'weapon' attribute to determine which weapon is used as artillery weapon
if (sys == Systems.ARTILLERY) {
WeaponObject weapon = system.getWeapon();
// If none set, default to ARTILLERY_FED
if (weapon == Database.DEFAULT_WEAPON_OBJ) {
sysEl.setAttribute("weapon", "ARTILLERY_FED");
} else {
sysEl.setAttribute("weapon", weapon.getBlueprintName());
}
}
if (system.canContainInterior() && ship.isPlayerShip() && system.getInteriorNamespace() != null)
sysEl.setAttribute("img", system.getInteriorNamespace());
StationObject station = system.getStation();
if (sys.canContainStation() && ship.isPlayerShip()) {
Element slotEl = new Element("slot");
// Medbay and Clonebay slots don't have a direction - they're always NONE
if (sys != Systems.MEDBAY && sys != Systems.CLONEBAY) {
e = new Element("direction");
e.setText(station.getSlotDirection().toString());
slotEl.addContent(e); // Add <direction> to <slot>
}
e = new Element("number");
e.setText("" + station.getSlotId());
slotEl.addContent(e); // Add <number> to <slot>
sysEl.addContent(slotEl);
}
systemList.addContent(sysEl);
}
}
}
shipBlueprint.addContent(systemList);
e = new Element("weaponSlots");
e.setText("" + ship.getWeaponSlots());
shipBlueprint.addContent(e);
e = new Element("droneSlots");
e.setText("" + ship.getDroneSlots());
shipBlueprint.addContent(e);
Element weaponList = new Element("weaponList");
weaponList.setAttribute("missiles", "" + ship.getMissilesAmount());
weaponList.setAttribute("count", "" + ship.getWeaponSlots());
if (ship.getWeaponsByList()) {
// Weapons are randomly drafted from a list of weapons
// 'count' determines how many weapons are drafted
WeaponList list = ship.getWeaponList();
if (list != Database.DEFAULT_WEAPON_LIST)
weaponList.setAttribute("load", list.getBlueprintName());
}
else {
// Weapons declared explicitly, ie. listed by name
// Only the first 'count' weapons are loaded in-game
for (WeaponObject weapon : ship.getWeapons()) {
if (weapon == Database.DEFAULT_WEAPON_OBJ)
continue;
e = new Element("weapon");
e.setAttribute("name", weapon.getBlueprintName());
weaponList.addContent(e);
}
}
shipBlueprint.addContent(weaponList);
Element droneList = new Element("droneList");
droneList.setAttribute("drones", "" + ship.getDronePartsAmount());
droneList.setAttribute("count", "" + ship.getDroneSlots());
if (ship.getDronesByList()) {
// Drones are randomly drafted from a list of drones
// 'count' determines how many drones are drafted
DroneList list = ship.getDroneList();
if (list != Database.DEFAULT_DRONE_LIST)
droneList.setAttribute("load", list.getBlueprintName());
}
else {
// Drones declared explicitly, ie. listed by name
// Only the first 'count' drones are loaded in-game
for (DroneObject drone : ship.getDrones()) {
if (drone == Database.DEFAULT_DRONE_OBJ)
continue;
e = new Element("drone");
e.setAttribute("name", drone.getBlueprintName());
droneList.addContent(e);
}
}
shipBlueprint.addContent(droneList);
// Defines the ship's health points
e = new Element("health");
e.setAttribute("amount", "" + ship.getHealth());
shipBlueprint.addContent(e);
// Defines the amount of power the ship starts with
e = new Element("maxPower");
e.setAttribute("amount", "" + ship.getPower());
shipBlueprint.addContent(e);
if (ship.isPlayerShip()) {
// List every crew member individually to allow ordering of crew
for (Races race : ship.getCrew()) {
if (race == Races.NO_CREW)
continue;
e = new Element("crewCount");
e.setAttribute("amount", "1");
e.setAttribute("class", race.name().toLowerCase());
shipBlueprint.addContent(e);
}
} else {
for (Races race : Races.getRaces()) {
int amount = ship.getCrewMin(race);
int max = ship.getCrewMax(race);
e = new Element("crewCount");
e.setAttribute("amount", "" + amount);
e.setAttribute("max", "" + max);
e.setAttribute("class", race.name().toLowerCase());
// Don't print an empty tag
if (amount > 0 && (ship.isPlayerShip() || max > 0))
shipBlueprint.addContent(e);
}
// <boardingAI> tag, enemy exclusive
e = new Element("boardingAI");
e.setText(ship.getBoardingAI().toString().toLowerCase());
shipBlueprint.addContent(e);
}
for (AugmentObject aug : ship.getAugments()) {
if (aug == Database.DEFAULT_AUGMENT_OBJ)
continue;
e = new Element("aug");
e.setAttribute("name", aug.getBlueprintName());
shipBlueprint.addContent(e);
}
root.addContent(shipBlueprint);
doc.setRootElement(root);
return doc;
}
public static String generateLayoutTXT(ShipObject ship) {
StringBuilder buf = new StringBuilder();
buf.append(LayoutObjects.X_OFFSET);
buf.append("\r\n");
buf.append("" + ship.getXOffset());
buf.append("\r\n");
buf.append(LayoutObjects.Y_OFFSET);
buf.append("\r\n");
buf.append("" + ship.getYOffset());
buf.append("\r\n");
buf.append(LayoutObjects.HORIZONTAL);
buf.append("\r\n");
buf.append("" + ship.getHorizontal());
buf.append("\r\n");
buf.append(LayoutObjects.VERTICAL);
buf.append("\r\n");
buf.append("" + ship.getVertical());
buf.append("\r\n");
buf.append(LayoutObjects.ELLIPSE);
buf.append("\r\n");
Rectangle ellipse = ship.getEllipse();
buf.append("" + ellipse.width);
buf.append("\r\n");
buf.append("" + ellipse.height);
buf.append("\r\n");
buf.append("" + ellipse.x);
buf.append("\r\n");
buf.append("" + (ellipse.y - (ship.isPlayerShip() ? 0 : Database.ENEMY_SHIELD_Y_OFFSET)));
buf.append("\r\n");
for (RoomObject room : ship.getRooms()) {
buf.append(LayoutObjects.ROOM);
buf.append("\r\n");
buf.append("" + room.getId());
buf.append("\r\n");
buf.append("" + room.getX());
buf.append("\r\n");
buf.append("" + room.getY());
buf.append("\r\n");
buf.append("" + room.getW());
buf.append("\r\n");
buf.append("" + room.getH());
buf.append("\r\n");
}
RoomObject linked = null;
for (DoorObject door : ship.getDoors()) {
buf.append(LayoutObjects.DOOR);
buf.append("\r\n");
buf.append("" + door.getX());
buf.append("\r\n");
buf.append("" + door.getY());
buf.append("\r\n");
linked = door.getLeftRoom();
buf.append(linked == null ? "-1" : linked.getId());
buf.append("\r\n");
linked = door.getRightRoom();
buf.append(linked == null ? "-1" : linked.getId());
buf.append("\r\n");
buf.append("" + (door.isHorizontal() ? "0" : "1"));
buf.append("\r\n");
}
return buf.toString();
}
public static Document generateLayoutXML(ShipObject ship) {
Document doc = new Document();
Element root = new Element("wrapper");
Element e = null;
Comment c = new Comment("Copyright (c) 2012 by Subset Games. All rights reserved.");
root.addContent(c);
e = new Element("img");
Rectangle hullDimensions = ship.getHullDimensions();
e.setAttribute("x", "" + hullDimensions.x);
e.setAttribute("y", "" + hullDimensions.y);
e.setAttribute("w", "" + hullDimensions.width);
e.setAttribute("h", "" + hullDimensions.height);
root.addContent(e);
Element offsets = new Element("offsets");
e = new Element("floor");
e.setAttribute("x", "" + ship.getFloorOffset().x);
e.setAttribute("y", "" + ship.getFloorOffset().y);
offsets.addContent(e);
e = new Element("cloak");
e.setAttribute("x", "" + ship.getCloakOffset().x);
e.setAttribute("y", "" + ship.getCloakOffset().y);
offsets.addContent(e);
root.addContent(offsets);
Element weaponMounts = new Element("weaponMounts");
MountObject[] mounts = ship.getMounts();
for (int i = 0; i < mounts.length; i++) {
e = new Element("mount");
e.setAttribute("x", "" + mounts[i].getX());
e.setAttribute("y", "" + mounts[i].getY());
e.setAttribute("rotate", "" + mounts[i].isRotated());
e.setAttribute("mirror", "" + mounts[i].isMirrored());
e.setAttribute("gib", "" + mounts[i].getGib().getId());
e.setAttribute("slide", "" + mounts[i].getDirection().toString());
weaponMounts.addContent(e);
}
root.addContent(weaponMounts);
Element explosion = new Element("explosion");
DecimalFormat decimal = new DecimalFormat("0.00", new DecimalFormatSymbols(Locale.ENGLISH));
GibObject[] gibs = ship.getGibs();
for (int i = 0; i < gibs.length; i++) {
Element gib = new Element("gib" + (i + 1));
e = new Element("velocity");
e.setAttribute("min", "" + decimal.format(gibs[i].getVelocityMin()));
e.setAttribute("max", "" + decimal.format(gibs[i].getVelocityMax()));
gib.addContent(e);
e = new Element("direction");
e.setAttribute("min", "" + gibs[i].getDirectionMin());
e.setAttribute("max", "" + gibs[i].getDirectionMax());
gib.addContent(e);
e = new Element("angular");
e.setAttribute("min", "" + decimal.format(gibs[i].getAngularMin()));
e.setAttribute("max", "" + decimal.format(gibs[i].getAngularMax()));
gib.addContent(e);
e = new Element("x");
e.setText("" + gibs[i].getOffsetX());
gib.addContent(e);
e = new Element("y");
e.setText("" + gibs[i].getOffsetY());
gib.addContent(e);
explosion.addContent(gib);
}
root.addContent(explosion);
doc.setRootElement(root);
return doc;
}
public static Document generateRoomsXML(ShipObject ship) {
Document doc = new Document();
Element root = new Element("wrapper");
for (Systems sys : Systems.getSystems()) {
SystemObject system = ship.getSystem(sys);
if (system.isAssigned() && sys.canContainGlow() && sys.canContainStation()) {
GlowObject glow = system.getGlow();
Element glowEl = new Element("roomLayout");
String namespace = system.getInteriorNamespace();
if (namespace.startsWith("room_"))
namespace = namespace.replace("room_", "");
glowEl.setAttribute("name", namespace);
Element e = new Element("computerGlow");
if (!glow.getGlowSet().getIdentifier().equals("glow"))
e.setAttribute("name", glow.getGlowSet().getIdentifier());
e.setAttribute("x", "" + glow.getX());
e.setAttribute("y", "" + glow.getY());
e.setAttribute("dir", "" + glow.getDirection().name());
glowEl.addContent(e);
root.addContent(glowEl);
}
}
doc.setRootElement(root);
return doc;
}
/**
* Checks whether the image is corrupt.
*
* @param path
* path to the image
* @return true if the image is corrupt and should not be exported, false otherwise.
*/
private static boolean isImageCorrupt(String path) {
Image image = null;
try {
image = new Image(UIUtils.getDisplay(), path);
} catch (SWTException e) {
if (e.getCause() instanceof FileNotFoundException == false) {
log.warn(String.format("Image '%s' is corrupt and will not be exported.", path));
return true;
}
} finally {
if (image != null)
image.dispose();
}
return false;
}
}