package com.kartoflane.superluminal2.core; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.TreeSet; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; import net.vhati.ftldat.FTLDat.FTLPack; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.input.JDOMParseException; import com.kartoflane.superluminal2.components.enums.DroneTypes; import com.kartoflane.superluminal2.components.enums.WeaponTypes; import com.kartoflane.superluminal2.components.interfaces.Predicate; import com.kartoflane.superluminal2.ftl.AnimationObject; import com.kartoflane.superluminal2.ftl.AugmentObject; import com.kartoflane.superluminal2.ftl.BlueprintList; import com.kartoflane.superluminal2.ftl.DroneList; import com.kartoflane.superluminal2.ftl.DroneObject; import com.kartoflane.superluminal2.ftl.GlowObject; import com.kartoflane.superluminal2.ftl.GlowSet; import com.kartoflane.superluminal2.ftl.GlowSet.Glows; import com.kartoflane.superluminal2.ftl.ShipMetadata; import com.kartoflane.superluminal2.ftl.WeaponList; import com.kartoflane.superluminal2.ftl.WeaponObject; import com.kartoflane.superluminal2.utils.DataUtils; import com.kartoflane.superluminal2.utils.IOUtils; import com.kartoflane.superluminal2.utils.IOUtils.DecodeResult; /** * A class representing a database entry that can be installed in the {@link com.kartoflane.superluminal2.core.Database Database} to modify its * contents.<br> * Database entries are basically the editor's representation of .ftl files. * * @author kartoFlane * */ public class DatabaseEntry { private static final Logger log = LogManager.getLogger(DatabaseEntry.class); private final File file; private final ZipFile archive; private final FTLPack data; private final FTLPack resource; private ArrayList<ShipMetadata> shipMetadata = new ArrayList<ShipMetadata>(); private Map<String, AnimationObject> animationMap = new HashMap<String, AnimationObject>(); private Map<String, WeaponObject> weaponMap = new HashMap<String, WeaponObject>(); private Map<String, DroneObject> droneMap = new HashMap<String, DroneObject>(); private Map<String, AugmentObject> augmentMap = new HashMap<String, AugmentObject>(); private Map<String, GlowObject> glowMap = new HashMap<String, GlowObject>(); private Map<String, GlowSet> glowSetMap = new HashMap<String, GlowSet>(); private Map<String, WeaponList> weaponListMap = new HashMap<String, WeaponList>(); private Map<String, DroneList> droneListMap = new HashMap<String, DroneList>(); /** Temporary map to hold anim sheets, since they need to be loaded before weaponAnims, which reference them */ private HashMap<String, Element> animSheetMap = new HashMap<String, Element>(); /** * Creates a DatabaseEntry representing an installed mod.<br> * The entry then has to be loaded using {@link #load()} * * @param f * the .ftl or .zip file from which the data will be read * @throws ZipException * when the file is not a zip archive * @throws IOException * when an IO error occurs */ public DatabaseEntry(File f) throws ZipException, IOException { file = f; archive = new ZipFile(f); data = null; resource = null; } /** * Creates the default DatabaseEntry, which serves as the core of the database. * * @param data * the data.dat archive * @param resource * the resource.dat archive */ public DatabaseEntry(FTLPack data, FTLPack resource) { file = new File("DatabaseCore"); archive = null; this.data = data; this.resource = resource; } /** * @return the name of the database entry */ public String getName() { return file == null ? "" : file.getName(); } public File getFile() { return file; } /** * @return true if the entry contains the innerPath, false otherwise */ public boolean contains(String innerPath) { if (innerPath == null) throw new IllegalArgumentException("Inner path must not be null."); if (archive == null) { boolean result = data.contains(innerPath); if (!result) result = resource.contains(innerPath); return result; } else return archive.getEntry(innerPath) != null; } /** * @param innerPath * the inner path of the sought file * @return the stream * * @throws FileNotFoundException * when the inner path was not found in the entry * @throws IOException * when an IO error occurs */ public InputStream getInputStream(String innerPath) throws FileNotFoundException, IOException { if (innerPath == null) throw new IllegalArgumentException("Inner path must not be null."); if (archive == null) { if (innerPath.endsWith(".txt") || innerPath.endsWith(".xml") || innerPath.endsWith(".xml.append") || innerPath.endsWith(".append.xml") || innerPath.endsWith(".xml.rawappend") || innerPath.endsWith(".rawappend.xml")) return data.getInputStream(innerPath); else return resource.getInputStream(innerPath); } else { ZipEntry ze = archive.getEntry(innerPath); if (ze == null) throw new FileNotFoundException("Inner path not found: " + innerPath); return archive.getInputStream(ze); } } /** * @return a list of all inner paths */ public ArrayList<String> list() { ArrayList<String> result = new ArrayList<String>(); if (archive == null) { result.addAll(data.list()); result.addAll(resource.list()); } else { Enumeration<? extends ZipEntry> entries = archive.entries(); while (entries.hasMoreElements()) { ZipEntry ze = entries.nextElement(); if (!ze.isDirectory()) result.add(ze.getName()); } } return result; } /** * Clears all data that was loaded and cached in this entry. */ public void clear() { shipMetadata.clear(); animationMap.clear(); weaponMap.clear(); droneMap.clear(); augmentMap.clear(); glowMap.clear(); glowSetMap.clear(); weaponListMap.clear(); droneListMap.clear(); animSheetMap.clear(); System.gc(); } /** * Closes this entry and releases any system resources associated with the stream. */ public void close() throws IOException { if (archive == null) { data.close(); resource.close(); } else archive.close(); } public void dispose() throws IOException { close(); clear(); } public void store(AnimationObject anim) { animationMap.put(anim.getIdentifier(), anim); } public void store(AugmentObject augment) { augmentMap.put(augment.getIdentifier(), augment); } public void store(BlueprintList<?> list) { if (list instanceof WeaponList) { weaponListMap.put(list.getIdentifier(), (WeaponList) list); } else if (list instanceof DroneList) { droneListMap.put(list.getIdentifier(), (DroneList) list); } else { // Not interested in any other lists } } public void store(DroneObject drone) { droneMap.put(drone.getIdentifier(), drone); } public void store(GlowObject glow) { glowMap.put(glow.getIdentifier(), glow); } public void store(GlowSet set) { glowSetMap.put(set.getIdentifier(), set); } public void store(ShipMetadata metadata) { shipMetadata.add(metadata); } public void store(WeaponObject weapon) { weaponMap.put(weapon.getIdentifier(), weapon); } /** * @param animName * the animName of the sought animation (eg. laser_burst_1) * @return */ public AnimationObject getAnimation(String animName) { return animationMap.get(animName); } /** * @param blueprint * the blueprint name of the sought augment * @return the augment with the given blueprint, or null if not found */ public AugmentObject getAugment(String blueprint) { return augmentMap.get(blueprint); } /** * @return an array of all augments in this entry */ public AugmentObject[] getAugments() { return augmentMap.values().toArray(new AugmentObject[0]); } /** * @return an array of all drone lists in this entry */ public DroneList[] getDroneLists() { return droneListMap.values().toArray(new DroneList[0]); } /** * @param the * name of the blueprint list * @return the blueprint list with the given name */ public DroneList getDroneList(String name) { return droneListMap.get(name); } /** * @param blueprint * the blueprint name of the sought drone * @return the drone with the given blueprint name, or null if not found */ public DroneObject getDrone(String blueprint) { return droneMap.get(blueprint); } /** * @param type * the desired type of the drones * @return a list containing all drones of the given type in this entry */ public ArrayList<DroneObject> getDronesByType(DroneTypes type) { ArrayList<DroneObject> typeDrones = new ArrayList<DroneObject>(); for (DroneObject drone : droneMap.values()) { if (drone.getType() == type) typeDrones.add(drone); } return typeDrones; } /** * @param id * the name of the glow object (eg. pilot_1) * @return the glow object wit the given name, or null if not found */ public GlowObject getGlow(String id) { return glowMap.get(id); } /** * @return an array of all glow objects in this entry */ public GlowObject[] getGlows() { return glowMap.values().toArray(new GlowObject[0]); } /** * @param id * the namespace of the glow image set (eg. computer1_glow) * @return the glow image set with the given namespace, or null if not found */ public GlowSet getGlowSet(String id) { return glowSetMap.get(id); } /** * @return an array of all glow sets in this entry */ public GlowSet[] getGlowSets() { return glowSetMap.values().toArray(new GlowSet[0]); } /** * @return an array of all ships in this entry */ public ShipMetadata[] getShipMetadata() { return shipMetadata.toArray(new ShipMetadata[0]); } /** * @return an array of all weapon lists in this entry */ public WeaponList[] getWeaponLists() { return weaponListMap.values().toArray(new WeaponList[0]); } /** * @param the * name of the blueprint list * @return the blueprint list with the given name */ public WeaponList getWeaponList(String name) { return weaponListMap.get(name); } /** * @param blueprint * the blueprint name of the sought weapon * @return the weapon with the given blueprint name, or null if not found */ public WeaponObject getWeapon(String blueprint) { return weaponMap.get(blueprint); } /** * @param type * the desired type of the weapons * @return a list containing all weapons of the given type in this entry */ public ArrayList<WeaponObject> getWeaponsByType(WeaponTypes type) { ArrayList<WeaponObject> typeWeapons = new ArrayList<WeaponObject>(); for (WeaponObject weapon : weaponMap.values()) { if (weapon.getType() == type) typeWeapons.add(weapon); } return typeWeapons; } /** * Loads the contents of the database entry.<br> * This method should not be called directly. Use {@link Database#addEntry(DatabaseEntry)} instead. * * <pre> * Loaded data: * - weapon anim, animSheets (only temporarily), weapon sprites * - ship blueprints * - weapon blueprints * - drone blueprints * - augment blueprints * - weapon and drone lists * - roomLayout tags (glow objects) from rooms.xml * - glow images (glow sets) in img/ship/interior, eg. pilot_glow1-3.png, etc * </pre> */ public void load() { // Animations need to be loaded before weapons, since they reference them preloadAnims(); loadGlowSets(); String[] extensions = { ".xml", ".xml.append", ".append.xml", ".xml.rawappend", ".rawappend.xml" }; InputStream is = null; String[] blueprintFiles = { "data/blueprints", "data/autoBlueprints", "data/dlcBlueprints", "data/dlcBlueprintsOverwrite" }; for (String innerPath : blueprintFiles) { boolean found = false; for (String ext : extensions) { try { is = getInputStream(innerPath + ext); DecodeResult dr = IOUtils.decodeText(is, null); ArrayList<Element> elements = DataUtils.findTagsNamed(dr.text, "shipBlueprint"); for (Element e : elements) { try { store(DataUtils.loadShipMetadata(e)); } catch (IllegalArgumentException ex) { log.warn(getName() + ": could not load ship metadata: " + ex.getMessage()); } } elements.clear(); elements = null; elements = DataUtils.findTagsNamed(dr.text, "weaponBlueprint"); for (Element e : elements) { try { store(DataUtils.loadWeapon(e)); } catch (IllegalArgumentException ex) { log.warn(getName() + ": could not load weapon: " + ex.getMessage()); } } elements.clear(); elements = null; elements = DataUtils.findTagsNamed(dr.text, "droneBlueprint"); for (Element e : elements) { try { store(DataUtils.loadDrone(e)); } catch (IllegalArgumentException ex) { log.warn(getName() + ": could not load drone: " + ex.getMessage()); } } elements.clear(); elements = null; elements = DataUtils.findTagsNamed(dr.text, "augBlueprint"); for (Element e : elements) { try { store(DataUtils.loadAugment(e)); } catch (IllegalArgumentException ex) { log.warn(getName() + ": could not load augment: " + ex.getMessage()); } } found = true; } catch (FileNotFoundException e) { // Spammy and not very useful. // log.trace(String.format("Inner path '%s' could not be found.", innerPath + ext)); } catch (IOException e) { log.error(String.format("%s: an error occured while loading file '%s': ", getName(), innerPath), e); found = true; } catch (JDOMParseException e) { log.error(String.format("%s: an error occured while parsing file '%s': ", getName(), innerPath), e); found = true; } finally { try { if (is != null) is.close(); } catch (IOException e) { log.error(getName() + ": an error occured while closing stream", e); } } } if (!found) log.trace(String.format("%s did not contain file %s", getName(), innerPath)); } for (String innerPath : blueprintFiles) { for (String ext : extensions) { // Lists reference their contents directly, so they have to be loaded in a second pass try { is = getInputStream(innerPath + ext); DecodeResult dr = IOUtils.decodeText(is, null); ArrayList<Element> elements = DataUtils.findTagsNamed(dr.text, "blueprintList"); for (Element e : elements) { try { store(DataUtils.loadList(e)); } catch (IllegalArgumentException ex) { log.warn(getName() + ": could not load blueprint list: " + ex.getMessage()); } } elements.clear(); elements = null; } catch (FileNotFoundException e) { // Spammy and not very useful. // log.trace(String.format("Inner path '%s' could not be found.", innerPath + ext)); } catch (IOException e) { log.error(String.format("%s: an error occured while loading file '%s': ", getName(), innerPath), e); } catch (JDOMParseException e) { log.error(String.format("%s: an error occured while parsing file '%s': ", getName(), innerPath), e); } finally { try { if (is != null) is.close(); } catch (IOException e) { log.error(getName() + ": an error occured while closing stream", e); } } } } // Scan rooms.xml alone for (String ext : extensions) { try { is = getInputStream("data/rooms" + ext); DecodeResult dr = IOUtils.decodeText(is, null); ArrayList<Element> elements = DataUtils.findTagsNamed(dr.text, "roomLayout"); for (Element e : elements) { try { store(DataUtils.loadGlow(e)); } catch (IllegalArgumentException ex) { log.warn(getName() + ": could not load glow object: " + ex.getMessage()); } } elements.clear(); elements = null; } catch (FileNotFoundException e) { // log.trace(String.format("Inner path '%s' could not be found.", "data/rooms" + ext)); } catch (IOException e) { log.error(String.format("%s: an error occured while loading file '%s': ", getName(), "data/rooms"), e); } catch (JDOMParseException e) { log.error(String.format("%s: an error occured while parsing file '%s': ", getName(), "data/rooms"), e); } finally { try { if (is != null) is.close(); } catch (IOException e) { log.error(getName() + ": an error occured while closing stream", e); } } } // Clear anim sheets, as they're no longer needed animSheetMap.clear(); log.trace(getName() + " was loaded successfully."); } private void preloadAnims() { String[] extensions = { ".xml", ".xml.append", ".append.xml", ".xml.rawappend", ".rawappend.xml" }; String[] animPaths = new String[] { "data/animations", "data/dlcAnimations" }; for (String innerPath : animPaths) { boolean found = false; for (String ext : extensions) { InputStream is = null; try { is = getInputStream(innerPath + ext); DecodeResult dr = IOUtils.decodeText(is, null); Document doc = null; doc = IOUtils.parseXML(dr.text); Element root = doc.getRootElement(); // Preload anim sheets for (Element e : root.getChildren("animSheet")) { String name = e.getAttributeValue("name"); // If older entries are allowed to be overwritten by newer ones, bomb weapons // load the bomb projectile images instead of weapon images, since their sheets // share the same name if (name != null && !animSheetMap.containsKey(name)) animSheetMap.put(name, e); } // Load and store weaponAnims for (Element e : root.getChildren("weaponAnim")) { try { store(DataUtils.loadAnim(this, e)); } catch (IllegalArgumentException ex) { log.warn(getName() + ": could not load animation: " + ex.getMessage()); } } found = true; } catch (FileNotFoundException e) { // log.trace(String.format("Inner path '%s' could not be found.", innerPath + ext)); } catch (IOException e) { log.error(String.format("%s: an error occured while loading file '%s': ", getName(), innerPath), e); found = true; } catch (JDOMParseException e) { log.error(String.format("%s: an error occured while parsing file '%s': ", getName(), innerPath), e); found = true; } finally { try { if (is != null) is.close(); } catch (IOException e) { log.error(getName() + ": an error occured while closing stream", e); } } } if (!found) log.trace(String.format("%s did not contain file %s", getName(), innerPath)); } } /** * This method should not be used. It's been made public only to be * accessible by {@link DataUtils#loadAnim(DatabaseEntry, Element)}, * and is used only during the preloading of anim sheets. */ public Element getAnimSheetElement(String anim) { return animSheetMap.get(anim); } private void loadGlowSets() { final Pattern glowPtrn = Pattern.compile("[0-9]\\.png"); Predicate<String> filter = new Predicate<String>() { @Override public boolean accept(String path) { return path.contains("img/ship/interior/") && (glowPtrn.matcher(path).find() || path.endsWith("_glow.png")); } }; TreeSet<String> eligiblePaths = new TreeSet<String>(); for (String path : list()) { if (filter.accept(path)) eligiblePaths.add(path); } for (String s1 : eligiblePaths) { if (s1.endsWith("_glow.png")) { String namespace = s1.replaceAll("_glow.png", ""); namespace = namespace.replace("img/ship/interior/", ""); GlowSet set = new GlowSet(namespace); set.setImage(Glows.CLOAK, "db:" + s1); glowSetMap.put(set.getIdentifier(), set); } else if (s1.endsWith("1.png")) { String namespace = s1.replaceAll("[0-9]\\.png", ""); String s2 = find(eligiblePaths, namespace + "2.png"); String s3 = find(eligiblePaths, namespace + "3.png"); if (s1 != null && s2 != null && s3 != null) { namespace = namespace.replace("img/ship/interior/", ""); GlowSet set = new GlowSet(namespace); set.setImage(Glows.BLUE, "db:" + s1); set.setImage(Glows.GREEN, "db:" + s2); set.setImage(Glows.YELLOW, "db:" + s3); glowSetMap.put(set.getIdentifier(), set); } } } } private static String find(TreeSet<String> list, String name) { for (String s : list) { if (s.equals(name)) return s; // Break if the current string is greater than the sought one else if (s.compareTo(name) > 0) break; } return null; } @Override public int hashCode() { return file.hashCode(); } @Override public boolean equals(Object o) { if (o instanceof DatabaseEntry) { DatabaseEntry other = (DatabaseEntry) o; return file.equals(other.file); } else { return super.equals(o); } } @Override public String toString() { return getName(); } }