using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Xml; using Oni.Imaging; using Oni.Metadata; using Oni.Sound; namespace Oni.Xml { internal class XmlImporter : Importer { private static readonly Func floatConverter = XmlConvert.ToSingle; private static readonly Func byteConverter = XmlConvert.ToByte; protected XmlReader xml; private readonly string[] args; private string baseDir; private string filePath; private bool firstInstance; private Dictionary localRefs; private Dictionary externalRefs; private ImporterDescriptor currentDescriptor; private BinaryWriter currentWriter; #region protected struct RawArray protected struct RawArray { private int offset; private int count; public RawArray(int offset, int count) { this.offset = offset; this.count = count; } public int Offset => offset; public int Count => count; } #endregion public XmlImporter(string[] args) { this.args = args; } public override void Import(string filePath, string outputDirPath) { this.filePath = filePath; BeginImport(); using (xml = CreateXmlReader(filePath)) { while (xml.IsStartElement()) { switch (xml.LocalName) { case "Objects": ReadObjects(); break; case "Texture": ReadTexture(); break; case "ImpactEffects": ReadImpactEffects(); break; case "SoundAnimation": ReadSoundAnimation(); break; case "TextureMaterials": ReadTextureMaterials(); break; case "Particle": ReadParticle(); break; case "AmbientSound": case "ImpulseSound": case "SoundGroup": ReadSoundData(); break; case "Animation": ReadAnimation(); break; default: ReadInstance(); break; } } } Write(outputDirPath); } public override void BeginImport() { base.BeginImport(); baseDir = Path.GetDirectoryName(filePath); localRefs = new Dictionary(StringComparer.Ordinal); externalRefs = new Dictionary(StringComparer.Ordinal); firstInstance = true; } private static XmlReader CreateXmlReader(string filePath) { var settings = new XmlReaderSettings() { CloseInput = true, IgnoreWhitespace = true, IgnoreProcessingInstructions = true, IgnoreComments = true }; var xml = XmlReader.Create(filePath, settings); try { if (!xml.Read()) throw new InvalidDataException("Not an Oni XML file"); xml.MoveToContent(); if (!xml.IsStartElement("Oni")) throw new InvalidDataException("Not an Oni XML file"); if (xml.IsEmptyElement) throw new InvalidDataException("No instances found"); xml.ReadStartElement(); xml.MoveToContent(); } catch { #if NETCORE xml.Dispose(); #else xml.Close(); #endif throw; } return xml; } private void ReadInstance() { var xmlid = xml.GetAttribute("id"); var tagName = xml.GetAttribute("type"); if (tagName == null) tagName = xml.LocalName; var tag = (TemplateTag)Enum.Parse(typeof(TemplateTag), tagName); var metadata = InstanceMetadata.GetMetadata(InstanceFileHeader.OniPCTemplateChecksum); var template = metadata.GetTemplate(tag); string name = null; if (firstInstance) { name = Path.GetFileNameWithoutExtension(filePath); if (!name.StartsWith(tagName, StringComparison.Ordinal)) name = tagName + name; firstInstance = false; } var writer = BeginXmlInstance(tag, name, xmlid); template.Type.Accept(new XmlToBinaryVisitor(this, xml, writer)); EndXmlInstance(); } private void ReadAnimation() { var name = Path.GetFileNameWithoutExtension(filePath); var writer = BeginXmlInstance(TemplateTag.TRAM, name, "0"); var animation = Totoro.AnimationXmlReader.Read(xml, Path.GetDirectoryName(filePath)); Totoro.AnimationDatWriter.Write(animation, this, writer); EndXmlInstance(); } private void ReadParticle() { var name = Path.GetFileNameWithoutExtension(filePath); if (!name.StartsWith("BINA3RAP", StringComparison.Ordinal)) name = "BINA3RAP" + name; xml.ReadStartElement(); int rawDataOffset = RawWriter.Align32(); RawWriter.Write((int)BinaryTag.PAR3); RawWriter.Write(0); ParticleXmlImporter.Import(xml, RawWriter); int rawDataLength = RawWriter.Position - rawDataOffset; RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8); var writer = BeginXmlInstance(TemplateTag.BINA, name, "0"); writer.Write(rawDataLength); writer.Write(rawDataOffset); EndXmlInstance(); xml.ReadEndElement(); } private void ReadSoundData() { var name = Path.GetFileNameWithoutExtension(filePath); int rawDataOffset = RawWriter.Align32(); var osbdImporter = new OsbdXmlImporter(xml, RawWriter); osbdImporter.Import(); int rawDataLength = RawWriter.Position - rawDataOffset; RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8); var writer = BeginXmlInstance(TemplateTag.OSBD, name, "0"); writer.Write(rawDataLength); writer.Write(rawDataOffset); EndXmlInstance(); xml.ReadEndElement(); } private void ReadObjects() { var name = Path.GetFileNameWithoutExtension(filePath); if (!name.StartsWith("BINACJBO", StringComparison.Ordinal)) name = "BINACJBO" + name; xml.ReadStartElement(); int rawDataOffset = RawWriter.Align32(); RawWriter.Write((int)BinaryTag.OBJC); RawWriter.Write(0); ObjcXmlImporter.Import(xml, RawWriter); int rawDataLength = RawWriter.Position - rawDataOffset; RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8); var writer = BeginXmlInstance(TemplateTag.BINA, name, "0"); writer.Write(rawDataLength); writer.Write(rawDataOffset); EndXmlInstance(); xml.ReadEndElement(); } private void ReadTextureMaterials() { var name = Path.GetFileNameWithoutExtension(filePath); if (!name.StartsWith("BINADBMT", StringComparison.Ordinal)) name = "BINADBMT" + name; xml.ReadStartElement(); int rawDataOffset = RawWriter.Align32(); RawWriter.Write((int)BinaryTag.TMBD); RawWriter.Write(0); TmbdXmlImporter.Import(xml, RawWriter); int rawDataLength = RawWriter.Position - rawDataOffset; RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8); var writer = BeginXmlInstance(TemplateTag.BINA, name, "0"); writer.Write(rawDataLength); writer.Write(rawDataOffset); EndXmlInstance(); xml.ReadEndElement(); } private void ReadImpactEffects() { var name = Path.GetFileNameWithoutExtension(filePath); if (!name.StartsWith("BINAEINO", StringComparison.Ordinal)) name = "BINAEINO" + name; xml.ReadStartElement(); int rawDataOffset = RawWriter.Align32(); RawWriter.Write((int)BinaryTag.ONIE); RawWriter.Write(0); OnieXmlImporter.Import(xml, RawWriter); int rawDataLength = RawWriter.Position - rawDataOffset; RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8); var writer = BeginXmlInstance(TemplateTag.BINA, name, "0"); writer.Write(rawDataLength); writer.Write(rawDataOffset); EndXmlInstance(); xml.ReadEndElement(); } private void ReadSoundAnimation() { var name = Path.GetFileNameWithoutExtension(filePath); if (!name.StartsWith("BINADBAS", StringComparison.Ordinal)) name = "BINADBAS" + name; int rawDataOffset = RawWriter.Align32(); RawWriter.Write((int)BinaryTag.SABD); RawWriter.Write(0); SabdXmlImporter.Import(xml, RawWriter); int rawDataLength = RawWriter.Position - rawDataOffset; RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8); var writer = BeginXmlInstance(TemplateTag.BINA, name, "0"); writer.Write(rawDataLength); writer.Write(rawDataOffset); EndXmlInstance(); } private void ReadTexture() { var textureImporter = new Motoko.TextureXmlImporter(this, xml, filePath); textureImporter.Import(); } public BinaryWriter BeginXmlInstance(TemplateTag tag, string name, string xmlid) { if (!localRefs.TryGetValue(xmlid, out currentDescriptor)) { currentDescriptor = ImporterFile.CreateInstance(tag, name); localRefs.Add(xmlid, currentDescriptor); } else if (currentDescriptor.Tag != tag) { throw new InvalidDataException(string.Format("{0} was expected to be of type {1} but it's type is {2}", xmlid, tag, currentDescriptor.Tag)); } currentWriter = currentDescriptor.OpenWrite(); return currentWriter; } public void EndXmlInstance() { currentWriter.Dispose(); } #region private class XmlToBinaryVisitor private class XmlToBinaryVisitor : IMetaTypeVisitor { private readonly XmlImporter importer; private readonly XmlReader xml; private readonly BinaryWriter writer; public XmlToBinaryVisitor(XmlImporter importer, XmlReader xml, BinaryWriter writer) { this.importer = importer; this.xml = xml; this.writer = writer; } #region IMetaTypeVisitor Members void IMetaTypeVisitor.VisitEnum(MetaEnum type) { type.XmlToBinary(xml, writer); } void IMetaTypeVisitor.VisitByte(MetaByte type) { writer.Write(XmlConvert.ToByte(xml.ReadElementContentAsString())); } void IMetaTypeVisitor.VisitInt16(MetaInt16 type) { writer.Write(XmlConvert.ToInt16(xml.ReadElementContentAsString())); } void IMetaTypeVisitor.VisitUInt16(MetaUInt16 type) { writer.Write(XmlConvert.ToUInt16(xml.ReadElementContentAsString())); } void IMetaTypeVisitor.VisitInt32(MetaInt32 type) { writer.Write(xml.ReadElementContentAsInt()); } void IMetaTypeVisitor.VisitUInt32(MetaUInt32 type) { writer.Write(XmlConvert.ToUInt32(xml.ReadElementContentAsString())); } void IMetaTypeVisitor.VisitInt64(MetaInt64 type) { writer.Write(xml.ReadElementContentAsLong()); } void IMetaTypeVisitor.VisitUInt64(MetaUInt64 type) { writer.Write(XmlConvert.ToUInt64(xml.ReadElementContentAsString())); } void IMetaTypeVisitor.VisitFloat(MetaFloat type) { writer.Write(xml.ReadElementContentAsFloat()); } void IMetaTypeVisitor.VisitColor(MetaColor type) { byte[] values = xml.ReadElementContentAsArray(byteConverter); if (values.Length > 3) writer.Write(new Color(values[0], values[1], values[2], values[3])); else writer.Write(new Color(values[0], values[1], values[2])); } void IMetaTypeVisitor.VisitVector2(MetaVector2 type) { writer.Write(xml.ReadElementContentAsVector2()); } void IMetaTypeVisitor.VisitVector3(MetaVector3 type) { writer.Write(xml.ReadElementContentAsVector3()); } void IMetaTypeVisitor.VisitMatrix4x3(MetaMatrix4x3 type) { writer.Write(xml.ReadElementContentAsArray(floatConverter, 12)); } void IMetaTypeVisitor.VisitPlane(MetaPlane type) { writer.Write(xml.ReadElementContentAsArray(floatConverter, 4)); } void IMetaTypeVisitor.VisitQuaternion(MetaQuaternion type) { writer.Write(xml.ReadElementContentAsArray(floatConverter, 4)); } void IMetaTypeVisitor.VisitBoundingSphere(MetaBoundingSphere type) { ReadFields(type.Fields); } void IMetaTypeVisitor.VisitBoundingBox(MetaBoundingBox type) { ReadFields(type.Fields); } void IMetaTypeVisitor.VisitRawOffset(MetaRawOffset type) { //writer.Write(xml.ReadElementContentAsInt()); throw new NotImplementedException(); } void IMetaTypeVisitor.VisitSepOffset(MetaSepOffset type) { //writer.Write(xml.ReadElementContentAsInt()); throw new NotImplementedException(); } void IMetaTypeVisitor.VisitString(MetaString type) { writer.Write(xml.ReadElementContentAsString(), type.Count); } void IMetaTypeVisitor.VisitPadding(MetaPadding type) { writer.Write(type.FillByte, type.Count); } void IMetaTypeVisitor.VisitPointer(MetaPointer type) { var xmlid = xml.ReadElementContentAsString(); if (xmlid != null) xmlid = xmlid.Trim(); if (string.IsNullOrEmpty(xmlid)) { writer.Write(0); return; } writer.Write(importer.ResolveReference(xmlid, type.Tag)); } void IMetaTypeVisitor.VisitStruct(MetaStruct type) { ReadFields(type.Fields); } void IMetaTypeVisitor.VisitArray(MetaArray type) { int count = ReadArray(type.ElementType, type.Count); if (count < type.Count) writer.Skip((type.Count - count) * type.ElementType.Size); } void IMetaTypeVisitor.VisitVarArray(MetaVarArray type) { int countFieldPosition = writer.Position; int count; if (type.CountField.Type == MetaType.Int16) { writer.WriteInt16(0); count = ReadArray(type.ElementType, UInt16.MaxValue); } else { writer.Write(0); count = ReadArray(type.ElementType, Int32.MaxValue); } int position = writer.Position; writer.Position = countFieldPosition; if (type.CountField.Type == MetaType.Int16) writer.WriteUInt16(count); else writer.Write(count); writer.Position = position; } #endregion private void ReadFields(IEnumerable fields) { xml.ReadStartElement(); xml.MoveToContent(); foreach (var field in fields) { try { field.Type.Accept(this); } catch (Exception ex) { throw new InvalidOperationException(string.Format("Cannot read field '{0}'", field.Name), ex); } } xml.ReadEndElement(); } protected void ReadStruct(MetaStruct s) { foreach (var field in s.Fields) { try { field.Type.Accept(this); } catch (Exception ex) { throw new InvalidOperationException(string.Format("Cannot read field '{0}'", field.Name), ex); } } } private int ReadArray(MetaType elementType, int maxCount) { if (xml.IsEmptyElement) { xml.Read(); return 0; } xml.ReadStartElement(); xml.MoveToContent(); string elementName = xml.LocalName; int count = 0; for (; count < maxCount && xml.IsStartElement(elementName); count++) elementType.Accept(this); xml.ReadEndElement(); return count; } protected int ReadRawElement(string name, MetaType elementType) { if (!xml.IsStartElement(name)) return 0; if (xml.IsEmptyElement) { xml.ReadStartElement(); return 0; } int rawOffset = importer.RawWriter.Align32(); elementType.Accept(new RawXmlImporter(xml, importer.RawWriter)); return rawOffset; } protected RawArray ReadRawArray(string name, MetaType elementType) { if (!xml.IsStartElement(name)) return new RawArray(); if (xml.IsEmptyElement) { xml.ReadStartElement(); return new RawArray(); } xml.ReadStartElement(); int rawOffset = importer.RawWriter.Align32(); var rawImporter = new RawXmlImporter(xml, importer.RawWriter); int elementCount = 0; while (xml.IsStartElement(elementType.Name)) { elementType.Accept(rawImporter); elementCount++; } xml.ReadEndElement(); return new RawArray(rawOffset, elementCount); } } #endregion private ImporterDescriptor ResolveReference(string xmlid, TemplateTag tag) { ImporterDescriptor descriptor; if (xmlid[0] == '#') descriptor = ResolveLocalReference(xmlid.Substring(1), tag); else descriptor = ResolveExternalReference(xmlid, tag); return descriptor; } private ImporterDescriptor ResolveLocalReference(string xmlid, TemplateTag tag) { ImporterDescriptor descriptor; if (!localRefs.TryGetValue(xmlid, out descriptor)) { descriptor = ImporterFile.CreateInstance(tag); localRefs.Add(xmlid, descriptor); } else if (tag != TemplateTag.NONE && tag != descriptor.Tag) { throw new InvalidDataException(string.Format("{0} was expected to be of type {1} but it's type is {2}", xmlid, tag, descriptor.Tag)); } return descriptor; } private ImporterDescriptor ResolveExternalReference(string xmlid, TemplateTag tag) { ImporterDescriptor descriptor; if (!externalRefs.TryGetValue(xmlid, out descriptor)) { if (xmlid.EndsWith(".xml", StringComparison.Ordinal) || xmlid.EndsWith(".dae", StringComparison.Ordinal) || xmlid.EndsWith(".obj", StringComparison.Ordinal) || xmlid.EndsWith(".tga", StringComparison.Ordinal)) { string filePath = Path.Combine(baseDir, xmlid); if (!File.Exists(filePath)) throw new InvalidDataException(string.Format("Cannot find referenced file '{0}'", filePath)); if (tag == TemplateTag.TRCM) { var bodyImporter = new Totoro.BodyDaeImporter(args); descriptor = bodyImporter.Import(filePath, ImporterFile); } else if (tag == TemplateTag.M3GM && (currentDescriptor.Tag == TemplateTag.ONWC || currentDescriptor.Tag == TemplateTag.CONS || currentDescriptor.Tag == TemplateTag.DOOR || currentDescriptor.Tag == TemplateTag.OFGA)) { var geometryImporter = new Motoko.GeometryImporter(args); descriptor = geometryImporter.Import(filePath, ImporterFile); } else { AddDependency(filePath, tag); var name = Importer.DecodeFileName(Path.GetFileNameWithoutExtension(filePath)); descriptor = ImporterFile.CreateInstance(tag, name); } } else { if (tag != TemplateTag.NONE) { // // If the link has a known type tag then make sure the xml id // is prefixed with the tag name. // var typeName = tag.ToString(); if (!xmlid.StartsWith(typeName, StringComparison.Ordinal)) xmlid = typeName + xmlid; } else { // // IGPG contains a link that can point to either TXMP or PSpc. // In this case the xml id should be already prefixed with // the tag name because we have no way to guess what type the link is. // var tagName = xmlid.Substring(0, 4); tag = (TemplateTag)Enum.Parse(typeof(TemplateTag), tagName); } descriptor = ImporterFile.CreateInstance(tag, xmlid); } externalRefs.Add(xmlid, descriptor); } return descriptor; } } }