RomRaider/src/main/java/com/romraider/xml/ConversionLayer/XDFConversionLayer.java

627 lines
24 KiB
Java

/*
* RomRaider Open-Source Tuning, Logging and Reflashing
* Copyright (C) 2006-2022 RomRaider.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.romraider.xml.ConversionLayer;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ResourceBundle;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.log4j.Logger;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import com.romraider.util.HexUtil;
import com.romraider.util.ResourceUtil;
public class XDFConversionLayer extends ConversionLayer {
protected static final ResourceBundle rb = new ResourceUtil().getBundle(
XDFConversionLayer.class.getName());
private static final Logger LOGGER = Logger.getLogger(XDFConversionLayer.class);
private HashMap < Integer, String > categoryMap = new HashMap < Integer, String > ();
private HashMap < Integer, Element > tableMap = new HashMap < Integer, Element > ();
private LinkedList < EmbedInfoData > embedsToSolve = new LinkedList < EmbedInfoData > ();
int bitCount;
private boolean signed;
private int offset;
private int numDigits;
// private String dataType;
private boolean lsbFirst;
// Defaults
String defaultDataType;
private class EmbedInfoData {
Element tableNodeRR;
Node axisNode;
Node flagsNodeTable;
}
@Override
public String getDefinitionPickerInfo() {
return rb.getString("LOADINGWARNING");
}
@Override
public String getRegexFileNameFilter() {
return "^.*xdf";
}
@Override
public Document convertToDocumentTree(File f) throws Exception {
Document doc = null;
FileInputStream fileStream = null;
BufferedReader br = null;
try {
// Check first if its an older definition
// which is not xml based
br = new BufferedReader(new FileReader(f));
String firstLine = br.readLine();
if (firstLine.equalsIgnoreCase("XDF")) {
br.close();
throw new SAXException(rb.getString("ONLYXML"));
} else {
br.close();
}
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setXIncludeAware(true);
DocumentBuilder docBuilder = factory.newDocumentBuilder();
fileStream = new FileInputStream(f);
Document XMLdoc = docBuilder.parse(fileStream, f.getAbsolutePath());
doc = convertXDFDocument(XMLdoc);
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fileStream != null)
fileStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return doc;
}
private Element solveLinkObj(int key, Node targetTable) {
if (tableMap.containsKey(key)) {
Element sourceTableRR = tableMap.get(key);
return (Element) sourceTableRR.cloneNode(true);
} else {
return null;
}
}
private Element parseAxis(Document doc, Element tableNodeRR, Node axisNode, Node flagsNodeTable) {
Node idNode = axisNode.getAttributes().getNamedItem("id");
String id = "";
if (idNode != null) {
id = idNode.getNodeValue();
}
Element targetTable = null;
Element scaling = null;
boolean hasEmbedInfo = false;
String staticTable = "";
int nodeCountAxis = axisNode.getChildNodes().getLength();
Node n;
// Check first if we need to copy attributes from the base table
for (int i = 0; i < nodeCountAxis; i++) {
n = axisNode.getChildNodes().item(i);
if (n.getNodeName().equalsIgnoreCase("embedinfo") && n.getAttributes().getNamedItem("linkobjid") != null) {
Integer key = HexUtil.hexToInt(n.getAttributes().getNamedItem("linkobjid").getNodeValue());
Element refTable = solveLinkObj(key, targetTable);
// Table was already parsed
if (refTable != null) {
targetTable = refTable;
int nodeCountRefTable = targetTable.getChildNodes().getLength();
targetTable.removeAttribute("type");
targetTable.removeAttribute("category");
// Find scaling child and delete child tables
LinkedList < Node > nodesToRemove = new LinkedList <Node> ();
for (int j = 0; j < nodeCountRefTable; j++) {
Node tN = targetTable.getChildNodes().item(j);
if (tN.getNodeName().equalsIgnoreCase("table") ||
tN.getNodeName().equalsIgnoreCase("description")) {
nodesToRemove.add(tN);
} else if (tN.getNodeName().equalsIgnoreCase("scaling")) {
scaling = (Element) tN;
}
}
for (Node nodeToRemove: nodesToRemove)
targetTable.removeChild(nodeToRemove);
}
// Referenced Table is not yet parsed
else {
EmbedInfoData e = new EmbedInfoData();
e.axisNode = axisNode;
e.flagsNodeTable = flagsNodeTable;
e.tableNodeRR = tableNodeRR;
embedsToSolve.add(e);
return null;
}
hasEmbedInfo = true;
break;
}
}
if (targetTable == null) {
targetTable = id.equalsIgnoreCase("z") || id.isEmpty() ? tableNodeRR : doc.createElement("table");
}
if (scaling == null) {
scaling = doc.createElement("scaling");
targetTable.appendChild(scaling);
}
Node addressNode = null;
LinkedList<String> staticCells = new LinkedList<String>();
int indexCount = -1;
int numDigitsStatic = -1;
int localNumDigits = -1;
for (int i = 0; i < nodeCountAxis; i++) {
n = axisNode.getChildNodes().item(i);
if (n.getNodeName().equalsIgnoreCase("units")) {
scaling.setAttribute("units", n.getTextContent());
} else if (n.getNodeName().equalsIgnoreCase("indexcount")) {
indexCount = Integer.parseInt(n.getTextContent());
targetTable.setAttribute("size" + id.toLowerCase(), "" + indexCount);
}
if (!hasEmbedInfo) {
if (n.getNodeName().equalsIgnoreCase("embeddeddata")) {
addressNode = n.getAttributes().getNamedItem("mmedaddress");
if (addressNode != null) {
String address = addressNode.getNodeValue();
targetTable.setAttribute("storageaddress", address);
}
Node flagsNode = n.getAttributes().getNamedItem("mmedtypeflags");
Node sizeBitsNode = n.getAttributes().getNamedItem("mmedelementsizebits");
boolean signedLocal = signed;
boolean lsbFirstLocal = lsbFirst;
int flags = 0;
int sizeBits = 0;
if (flagsNode == null)
flagsNode = flagsNodeTable;
if (flagsNode != null) {
try {
flags = HexUtil.hexToInt(flagsNode.getNodeValue());
if ((flags & (0x01)) > 0) {
signedLocal = true;
} else if ((flags & 0x01) == 0) {
signedLocal = false;
}
if ((flags & 0x02) > 0) {
lsbFirstLocal = false;
} else {
lsbFirstLocal = true;
}
if ((flags & 0x04) > 0) {
targetTable.setAttribute("swapxy", "true");
}
} catch (NumberFormatException e) {
// TODO: Not sure how to handle this yet...
LOGGER.error("Failed to parse flag " + flagsNode.getNodeValue());
}
}
if (sizeBitsNode != null) {
sizeBits = Integer.parseInt(sizeBitsNode.getNodeValue());
} else {
sizeBits = bitCount;
}
targetTable.setAttribute("storagetype", (signedLocal ? "" : "u") + "int" + sizeBits);
targetTable.setAttribute("endian", lsbFirstLocal ? "big" : "little");
} else if (!hasEmbedInfo && n.getNodeName().equalsIgnoreCase("math")) {
String formula = n.getAttributes().getNamedItem("equation").getNodeValue();
formula = formula.replace("X", "x").replace(",", ".");
scaling.setAttribute("expression", formula);
} else if (n.getNodeName().equalsIgnoreCase("decimalpl")) {
try {
localNumDigits = Math.abs(Integer.parseInt(n.getTextContent()));
} catch (NumberFormatException e) {
//Do nothing
}
} else if (n.getNodeName().equalsIgnoreCase("label")) {
String label = n.getAttributes().getNamedItem("value").getNodeValue();
staticCells.add(label);
}
}
}
boolean isStatic = staticCells.size() == indexCount && indexCount > 1;
if (isStatic) {
staticTable = "Static ";
targetTable.setAttribute("size" + id, "" + staticCells.size());
targetTable.removeAttribute("endian");
targetTable.removeAttribute("storagetype");
for(String label : staticCells)
{
Element data = doc.createElement("data");
data.setTextContent(label);
targetTable.appendChild(data);
if (numDigitsStatic == -1) {
// Assume the format from the static data
String split[] = label.split("\\.");
if (split.length > 1) {
numDigitsStatic = split[1].length();
} else {
numDigitsStatic = 0;
}
}
}
}
// Case 1: Static table and no num digits set == Deduce from text
// Case 2: Non static table and digits are set
// Case 3: Non static table and digits arent sent --> use defaults
int digits = isStatic && localNumDigits == -1 ? numDigitsStatic : (localNumDigits == -1 ? numDigits : localNumDigits);
if (digits == 0)
scaling.setAttribute("format", "0");
else
scaling.setAttribute("format", "0." + new String(new char[digits]).replace("\0", "0"));
if (id.equalsIgnoreCase("z"))
return null;
else {
if (!id.isEmpty())
targetTable.setAttribute("type", staticTable + id.toUpperCase() + " Axis");
return targetTable;
}
}
private Element parseTable(Document doc, Node romNode, Node tableNode) {
int nodeCountTable = tableNode.getChildNodes().getLength();
Node n;
Element tableNodeRR = doc.createElement("table");
Node uniqueIDNode = tableNode.getAttributes().getNamedItem("uniqueid");
Node flagsNode = tableNode.getAttributes().getNamedItem("flags");
if (uniqueIDNode != null) {
tableMap.put(HexUtil.hexToInt(uniqueIDNode.getNodeValue()), tableNodeRR);
}
LinkedList < String > categories = new LinkedList < String > ();
for (int i = 0; i < nodeCountTable; i++) {
n = tableNode.getChildNodes().item(i);
if (n.getNodeName().equalsIgnoreCase("title")) {
// TunerPro can currently not edit axis directly, but we can
// These tables contain the axis, which we can skip
if (n.getTextContent().endsWith("(autogen)")) {
return null;
}
tableNodeRR.setAttribute("name", n.getTextContent());
}
if (n.getNodeName().equalsIgnoreCase("description")) {
Element desc = doc.createElement("description");
desc.setTextContent(n.getTextContent());
tableNodeRR.appendChild(desc);
} else if (n.getNodeName().equalsIgnoreCase("categorymem")) {
int category = Integer.parseInt(n.getAttributes().getNamedItem("category").getNodeValue());
if (categoryMap.containsKey(category - 1)) {
categories.add(categoryMap.get(category - 1));
}
} else if (n.getNodeName().equalsIgnoreCase("xdfaxis")) {
Element axis = parseAxis(doc, tableNodeRR, n, flagsNode);
if (axis != null) {
tableNodeRR.appendChild(axis);
}
}
}
tableNodeRR.setAttribute("category", convertToRRCategoryString(categories));
return tableNodeRR;
}
private Element getScalingNodeForTable(Element tableNodeRR) {
for (int i = 0; i < tableNodeRR.getChildNodes().getLength(); i++) {
Element n = (Element) tableNodeRR.getChildNodes().item(i);
if (n.getNodeName().equalsIgnoreCase("scaling")) {
return n;
}
}
return null;
}
private void postProcessTable(Element tableNodeRR) {
int validAxis = 0;
int nodeCountTable = tableNodeRR.getChildNodes().getLength();
LinkedList <Element> nodesToRemove = new LinkedList <Element> ();
for (int i = 0; i < nodeCountTable; i++) {
Element n = (Element) tableNodeRR.getChildNodes().item(i);
if (n.getNodeName().equalsIgnoreCase("table")) {
if (n.hasAttribute("storageaddress") || n.getAttributeNode("type").getValue().contains("Static")) {
validAxis++;
// Use the sizes of the X and Y axis
// for the main table
Attr sizex = n.getAttributeNode("sizex");
Attr sizey = n.getAttributeNode("sizey");
if (sizex != null)
tableNodeRR.setAttributeNode((Attr) sizex.cloneNode(false));
else if (sizey != null)
tableNodeRR.setAttributeNode((Attr) sizey.cloneNode(false));
} else {
Element scalingNode = getScalingNodeForTable(tableNodeRR);
Element axisScalingNode = getScalingNodeForTable(n);
// 2D Tables work different in XDFs
// We have to use the unit of the "missing" axis for the main table
if (scalingNode != null && axisScalingNode != null && !scalingNode.hasAttribute("units")) {
scalingNode.setAttribute("units", axisScalingNode.getAttribute("units"));
}
nodesToRemove.add(n);
}
}
}
for (Element n: nodesToRemove) {
tableNodeRR.removeChild(n);
}
tableNodeRR.setAttribute("type", (validAxis + 1) + "D");
}
private String convertToRRCategoryString(List < String > categories) {
String category = "";
for (int i = 0; i < categories.size(); i++) {
String cat = categories.get(i);
category += cat;
if (i < categories.size() - 1)
category += "//";
}
return category;
}
private Node parseXDFHeader(Document doc, Node romNode, Node header) {
int nodeCountHeader = header.getChildNodes().getLength();
Node n;
Node romIDNode = doc.createElement("romid");
for (int i = 0; i < nodeCountHeader; i++) {
n = header.getChildNodes().item(i);
if (n.getNodeName().equalsIgnoreCase("CATEGORY")) {
categoryMap.put(HexUtil.hexToInt(n.getAttributes().getNamedItem("index").getNodeValue()),
n.getAttributes().getNamedItem("name").getNodeValue());
} else if (n.getNodeName().equalsIgnoreCase("flags")) {
// TODO
} else if (n.getNodeName().equalsIgnoreCase("author")) {
String author = n.getTextContent();
Node ecuID = doc.createElement("author");
ecuID.setTextContent(author);
romIDNode.appendChild(ecuID);
} else if (n.getNodeName().equalsIgnoreCase("fileversion")) {
String version = n.getTextContent();
Node ecuID = doc.createElement("version");
ecuID.setTextContent(version);
romIDNode.appendChild(ecuID);
} else if (n.getNodeName().equalsIgnoreCase("deftitle")) {
String title = n.getTextContent();
Node ecuID = doc.createElement("xmlid");
ecuID.setTextContent(title);
romIDNode.appendChild(ecuID);
} else if (n.getNodeName().equalsIgnoreCase("description")) {
// TODO
} else if (n.getNodeName().equalsIgnoreCase("BASEOFFSET")) {
Node offsetNode = n.getAttributes().getNamedItem("offset");
if (offsetNode != null) {
offset = Integer.parseInt(offsetNode.getNodeValue());
if (!n.getAttributes().getNamedItem("subtract").getNodeValue().equals("0")) {
offset *= -1;
}
}
} else if (n.getNodeName().equalsIgnoreCase("DEFAULTS")) {
if (!n.getAttributes().getNamedItem("float").getNodeValue().equalsIgnoreCase("0")) {
// dataType = "float";
} else {
bitCount = Integer.parseInt(n.getAttributes().getNamedItem("datasizeinbits").getNodeValue());
signed = !n.getAttributes().getNamedItem("signed").getNodeValue().equalsIgnoreCase("0");
// dataType = (signed ? "" : "u") + "int" + bitCount;
lsbFirst = !n.getAttributes().getNamedItem("lsbfirst").getNodeValue().equalsIgnoreCase("0");
numDigits = HexUtil.hexToInt(n.getAttributes().getNamedItem("sigdigits").getNodeValue());
}
} else if (n.getNodeName().equalsIgnoreCase("REGION")) {
// Ignored currently: type, startAddress, regionFlags, name, desc
// TODO: Start address probably matters....
int fileSize = HexUtil.hexToInt(n.getAttributes().getNamedItem("size").getNodeValue());
Node fileSizeN = doc.createElement("filesize");
fileSizeN.setTextContent(fileSize + "b");
}
}
// XDFs dont have an identification component
// So we just load it, no questions asked
Node idAddress = doc.createElement("internalidaddress");
Node idString = doc.createElement("internalidstring");
idString.setTextContent("force");
idAddress.setTextContent("-1");
Element offsetNode = doc.createElement("offset");
offsetNode.setTextContent("0x" + Integer.toHexString(offset));
romIDNode.appendChild(offsetNode);
romIDNode.appendChild(idAddress);
romIDNode.appendChild(idString);
return romIDNode;
}
private Document convertXDFDocument(Document xdfDoc) throws SAXException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = null;
try {
builder = dbf.newDocumentBuilder();
} catch (ParserConfigurationException e) {
e.printStackTrace();
}
// New RomRaider document
Document doc = builder.newDocument();
Node baseNode = xdfDoc;
Node romNode = null;
int nodeCount = 0;
int nodeCountBase = baseNode.getChildNodes().getLength();
for (int i = 0; i < nodeCountBase; i++) {
Node n = baseNode.getChildNodes().item(i);
if (n.getNodeName().equalsIgnoreCase("XDFFORMAT")) {
baseNode = n;
break;
}
}
if (baseNode == xdfDoc) {
throw new SAXException(rb.getString("NOXDFFORMAT"));
}
nodeCount = baseNode.getChildNodes().getLength();
Node header = null;
// Find XDF Header first
for (int i = 0; i < nodeCount; i++) {
Node n = baseNode.getChildNodes().item(i);
if (n.getNodeName().equalsIgnoreCase("XDFHEADER")) {
// Create the initial document
Node roms = doc.createElement("roms");
romNode = doc.createElement("rom");
doc.appendChild(roms);
roms.appendChild(romNode);
header = n;
Node headerNode = parseXDFHeader(doc, romNode, header);
romNode.appendChild(headerNode);
break;
}
}
if (header == null) {
throw new SAXException(rb.getString("NOXDFHEADER"));
}
LinkedList <Element> tables = new LinkedList <Element> ();
// Go through all tables and create RR tables
for (int i = 0; i < nodeCount; i++) {
Node n = baseNode.getChildNodes().item(i);
if (n.getNodeName().equalsIgnoreCase("XDFTABLE")) {
Element table = parseTable(doc, romNode, n);
if (table != null) {
tables.add(table);
}
}
// A constant is a mix between a table and an axis
// So parse it as both
else if (n.getNodeName().equalsIgnoreCase("XDFCONSTANT")) {
Element table = parseTable(doc, romNode, n);
parseAxis(doc, table, n, null);
if (table != null) {
tables.add(table);
}
}
}
// Some references could not be solved because we didnt parse the table yet
// So we have to do these now
for (EmbedInfoData e: embedsToSolve) {
Element axis = parseAxis(doc, e.tableNodeRR, e.axisNode, e.flagsNodeTable);
if (axis != null)
e.tableNodeRR.appendChild(axis);
}
// Final cleanup and add tables to ROM
for (Element t: tables) {
postProcessTable(t);
romNode.appendChild(t);
}
categoryMap.clear();
tableMap.clear();
embedsToSolve.clear();
return doc;
}
}