/* * Copyright (c) 2009, openmetaverse.org * All rights reserved. * * - Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * - Neither the name of the openmetaverse.org nor the names * of its contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Text; using System.Xml; using OpenMetaverse; namespace OpenMetaverse.Assets { public static class OarFile { public delegate void AssetLoadedCallback(Asset asset, long bytesRead, long totalBytes); public delegate void TerrainLoadedCallback(float[,] terrain, long bytesRead, long totalBytes); public delegate void SceneObjectLoadedCallback(AssetPrim linkset, long bytesRead, long totalBytes); public delegate void SettingsLoadedCallback(string regionName, RegionSettings settings); #region Archive Loading public static void UnpackageArchive(string filename, AssetLoadedCallback assetCallback, TerrainLoadedCallback terrainCallback, SceneObjectLoadedCallback objectCallback, SettingsLoadedCallback settingsCallback) { int successfulAssetRestores = 0; int failedAssetRestores = 0; try { using (FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read)) { using (GZipStream loadStream = new GZipStream(fileStream, CompressionMode.Decompress)) { TarArchiveReader archive = new TarArchiveReader(loadStream); string filePath; byte[] data; TarArchiveReader.TarEntryType entryType; while ((data = archive.ReadEntry(out filePath, out entryType)) != null) { if (filePath.StartsWith(ArchiveConstants.OBJECTS_PATH)) { // Deserialize the XML bytes if (objectCallback != null) LoadObjects(data, objectCallback, fileStream.Position, fileStream.Length); } else if (filePath.StartsWith(ArchiveConstants.ASSETS_PATH)) { if (assetCallback != null) { if (LoadAsset(filePath, data, assetCallback, fileStream.Position, fileStream.Length)) successfulAssetRestores++; else failedAssetRestores++; } } else if (filePath.StartsWith(ArchiveConstants.TERRAINS_PATH)) { if (terrainCallback != null) LoadTerrain(filePath, data, terrainCallback, fileStream.Position, fileStream.Length); } else if (filePath.StartsWith(ArchiveConstants.SETTINGS_PATH)) { if (settingsCallback != null) LoadRegionSettings(filePath, data, settingsCallback); } } archive.Close(); } } } catch (Exception e) { Logger.Log("[OarFile] Error loading OAR file: " + e.Message, Helpers.LogLevel.Error); return; } if (failedAssetRestores > 0) Logger.Log(String.Format("[OarFile]: Failed to load {0} assets", failedAssetRestores), Helpers.LogLevel.Warning); } private static bool LoadAsset(string assetPath, byte[] data, AssetLoadedCallback assetCallback, long bytesRead, long totalBytes) { // Right now we're nastily obtaining the UUID from the filename string filename = assetPath.Remove(0, ArchiveConstants.ASSETS_PATH.Length); int i = filename.LastIndexOf(ArchiveConstants.ASSET_EXTENSION_SEPARATOR); if (i == -1) { Logger.Log(String.Format( "[OarFile]: Could not find extension information in asset path {0} since it's missing the separator {1}. Skipping", assetPath, ArchiveConstants.ASSET_EXTENSION_SEPARATOR), Helpers.LogLevel.Warning); return false; } string extension = filename.Substring(i); UUID uuid; UUID.TryParse(filename.Remove(filename.Length - extension.Length), out uuid); if (ArchiveConstants.EXTENSION_TO_ASSET_TYPE.ContainsKey(extension)) { AssetType assetType = ArchiveConstants.EXTENSION_TO_ASSET_TYPE[extension]; Asset asset = null; switch (assetType) { case AssetType.Animation: asset = new AssetAnimation(uuid, data); break; case AssetType.Bodypart: asset = new AssetBodypart(uuid, data); break; case AssetType.Clothing: asset = new AssetClothing(uuid, data); break; case AssetType.Gesture: asset = new AssetGesture(uuid, data); break; case AssetType.Landmark: asset = new AssetLandmark(uuid, data); break; case AssetType.LSLBytecode: asset = new AssetScriptBinary(uuid, data); break; case AssetType.LSLText: asset = new AssetScriptText(uuid, data); break; case AssetType.Notecard: asset = new AssetNotecard(uuid, data); break; case AssetType.Object: asset = new AssetPrim(uuid, data); break; case AssetType.Sound: asset = new AssetSound(uuid, data); break; case AssetType.Texture: asset = new AssetTexture(uuid, data); break; default: Logger.Log("[OarFile] Unhandled asset type " + assetType, Helpers.LogLevel.Error); break; } if (asset != null) { assetCallback(asset, bytesRead, totalBytes); return true; } } Logger.Log("[OarFile] Failed to load asset", Helpers.LogLevel.Warning); return false; } private static bool LoadRegionSettings(string filePath, byte[] data, SettingsLoadedCallback settingsCallback) { RegionSettings settings = null; bool loaded = false; try { using (MemoryStream stream = new MemoryStream(data)) settings = RegionSettings.FromStream(stream); loaded = true; } catch (Exception ex) { Logger.Log("[OarFile] Failed to parse region settings file " + filePath + ": " + ex.Message, Helpers.LogLevel.Warning); } // Parse the region name out of the filename string regionName = Path.GetFileNameWithoutExtension(filePath); if (loaded) settingsCallback(regionName, settings); return loaded; } private static bool LoadTerrain(string filePath, byte[] data, TerrainLoadedCallback terrainCallback, long bytesRead, long totalBytes) { float[,] terrain = new float[256, 256]; bool loaded = false; switch (Path.GetExtension(filePath)) { case ".r32": case ".f32": // RAW32 if (data.Length == 256 * 256 * 4) { int pos = 0; for (int y = 0; y < 256; y++) { for (int x = 0; x < 256; x++) { terrain[y, x] = Utils.Clamp(Utils.BytesToFloat(data, pos), 0.0f, 255.0f); pos += 4; } } loaded = true; } else { Logger.Log("[OarFile] RAW32 terrain file " + filePath + " has the wrong number of bytes: " + data.Length, Helpers.LogLevel.Warning); } break; case ".ter": // Terragen case ".raw": // LLRAW case ".jpg": case ".jpeg": // JPG case ".bmp": // BMP case ".png": // PNG case ".gif": // GIF case ".tif": case ".tiff": // TIFF default: Logger.Log("[OarFile] Unrecognized terrain format in " + filePath, Helpers.LogLevel.Warning); break; } if (loaded) terrainCallback(terrain, bytesRead, totalBytes); return loaded; } public static void LoadObjects(byte[] objectData, SceneObjectLoadedCallback objectCallback, long bytesRead, long totalBytes) { XmlDocument doc = new XmlDocument(); using (XmlTextReader reader = new XmlTextReader(new MemoryStream(objectData))) { reader.WhitespaceHandling = WhitespaceHandling.None; doc.Load(reader); } XmlNode rootNode = doc.FirstChild; if (rootNode.LocalName.Equals("scene")) { foreach (XmlNode node in rootNode.ChildNodes) { AssetPrim linkset = new AssetPrim(node.OuterXml); if (linkset != null) objectCallback(linkset, bytesRead, totalBytes); } } else { AssetPrim linkset = new AssetPrim(rootNode.OuterXml); if (linkset != null) objectCallback(linkset, bytesRead, totalBytes); } } #endregion Archive Loading #region Archive Saving public static void PackageArchive(string directoryName, string filename) { const string ARCHIVE_XML = "\n"; TarArchiveWriter archive = new TarArchiveWriter(new GZipStream(new FileStream(filename, FileMode.Create), CompressionMode.Compress)); // Create the archive.xml file archive.WriteFile("archive.xml", ARCHIVE_XML); // Add the assets string[] files = Directory.GetFiles(directoryName + "/" + ArchiveConstants.ASSETS_PATH); foreach (string file in files) archive.WriteFile(ArchiveConstants.ASSETS_PATH + Path.GetFileName(file), File.ReadAllBytes(file)); // Add the objects files = Directory.GetFiles(directoryName + "/" + ArchiveConstants.OBJECTS_PATH); foreach (string file in files) archive.WriteFile(ArchiveConstants.OBJECTS_PATH + Path.GetFileName(file), File.ReadAllBytes(file)); // Add the terrain(s) files = Directory.GetFiles(directoryName + "/" + ArchiveConstants.TERRAINS_PATH); foreach (string file in files) archive.WriteFile(ArchiveConstants.TERRAINS_PATH + Path.GetFileName(file), File.ReadAllBytes(file)); archive.Close(); } public static void SavePrims(IList prims, string primsPath, string assetsPath, string textureCacheFolder) { Dictionary textureList = new Dictionary(); // Delete all of the old linkset files try { Directory.Delete(primsPath, true); } catch (Exception) { } // Create a new folder for the linkset files try { Directory.CreateDirectory(primsPath); } catch (Exception ex) { Logger.Log("Failed saving prims: " + ex.Message, Helpers.LogLevel.Error); return; } foreach (AssetPrim assetPrim in prims) { SavePrim(assetPrim, Path.Combine(primsPath, "Primitive_" + assetPrim.Parent.ID + ".xml")); CollectTextures(assetPrim.Parent, textureList); if (assetPrim.Children != null) { foreach (PrimObject child in assetPrim.Children) CollectTextures(child, textureList); } } SaveTextures(new List(textureList.Keys), assetsPath, textureCacheFolder); } static void CollectTextures(PrimObject prim, Dictionary textureList) { if (prim.Textures != null) { // Add all of the textures on this prim to the save list if (prim.Textures.DefaultTexture != null) textureList[prim.Textures.DefaultTexture.TextureID] = prim.Textures.DefaultTexture.TextureID; if (prim.Textures.FaceTextures != null) { for (int i = 0; i < prim.Textures.FaceTextures.Length; i++) { Primitive.TextureEntryFace face = prim.Textures.FaceTextures[i]; if (face != null) textureList[face.TextureID] = textureList[face.TextureID]; } } } } public static void SaveTextures(IList textures, string assetsPath, string textureCacheFolder) { int count = 0; // Delete the assets folder try { Directory.Delete(assetsPath, true); } catch (Exception) { } // Create a new assets folder try { Directory.CreateDirectory(assetsPath); } catch (Exception ex) { Logger.Log("Failed saving assets: " + ex.Message, Helpers.LogLevel.Error); return; } // Create a map of all of the textures in the cache string[] files = Directory.GetFiles(textureCacheFolder, "*.texture", SearchOption.TopDirectoryOnly); Dictionary idToFiles = new Dictionary(files.Length); for (int i = 0; i < files.Length; i++) { string file = files[i]; UUID id; if (UUID.TryParse(Path.GetFileNameWithoutExtension(file), out id)) idToFiles[id] = file; } for (int i = 0; i < textures.Count; i++) { UUID texture = textures[i]; if (idToFiles.ContainsKey(texture)) { try { File.Copy(idToFiles[texture], Path.Combine(assetsPath, texture.ToString() + "_texture.jp2")); ++count; } catch (Exception ex) { Logger.Log("Failed to save texture " + texture.ToString() + ": " + ex.Message, Helpers.LogLevel.Error); } } else { Logger.Log("Skipping missing texture " + texture.ToString(), Helpers.LogLevel.Warning); } } Logger.Log("Copied " + count + " textures to the asset archive folder", Helpers.LogLevel.Info); } static void SavePrim(AssetPrim prim, string filename) { try { using (StreamWriter stream = new StreamWriter(filename)) { XmlTextWriter writer = new XmlTextWriter(stream); SOGToXml2(writer, prim); writer.Flush(); } } catch (Exception ex) { Logger.Log("Failed saving linkset: " + ex.Message, Helpers.LogLevel.Error); } } public static void SOGToXml2(XmlTextWriter writer, AssetPrim prim) { writer.WriteStartElement(String.Empty, "SceneObjectGroup", String.Empty); SOPToXml(writer, prim.Parent, null); writer.WriteStartElement(String.Empty, "OtherParts", String.Empty); foreach (PrimObject child in prim.Children) SOPToXml(writer, child, prim.Parent); writer.WriteEndElement(); writer.WriteEndElement(); } static void SOPToXml(XmlTextWriter writer, PrimObject prim, PrimObject parent) { writer.WriteStartElement("SceneObjectPart"); writer.WriteAttributeString("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); writer.WriteAttributeString("xmlns:xsd", "http://www.w3.org/2001/XMLSchema"); WriteUUID(writer, "CreatorID", prim.CreatorID); WriteUUID(writer, "FolderID", prim.FolderID); writer.WriteElementString("InventorySerial", (prim.Inventory != null) ? prim.Inventory.Serial.ToString() : "0"); // FIXME: Task inventory writer.WriteStartElement("TaskInventory"); writer.WriteEndElement(); PrimFlags flags = PrimFlags.None; if (prim.UsePhysics) flags |= PrimFlags.Physics; if (prim.Phantom) flags |= PrimFlags.Phantom; if (prim.DieAtEdge) flags |= PrimFlags.DieAtEdge; if (prim.ReturnAtEdge) flags |= PrimFlags.ReturnAtEdge; if (prim.Temporary) flags |= PrimFlags.Temporary; if (prim.Sandbox) flags |= PrimFlags.Sandbox; writer.WriteElementString("ObjectFlags", ((int)flags).ToString()); WriteUUID(writer, "UUID", prim.ID); writer.WriteElementString("LocalId", prim.LocalID.ToString()); writer.WriteElementString("Name", prim.Name); writer.WriteElementString("Material", ((int)prim.Material).ToString()); writer.WriteElementString("RegionHandle", prim.RegionHandle.ToString()); writer.WriteElementString("ScriptAccessPin", prim.RemoteScriptAccessPIN.ToString()); Vector3 groupPosition; if (parent == null) groupPosition = prim.Position; else groupPosition = parent.Position; WriteVector(writer, "GroupPosition", groupPosition); if (prim.ParentID == 0) WriteVector(writer, "OffsetPosition", Vector3.Zero); else WriteVector(writer, "OffsetPosition", prim.Position); WriteQuaternion(writer, "RotationOffset", prim.Rotation); WriteVector(writer, "Velocity", prim.Velocity); WriteVector(writer, "RotationalVelocity", Vector3.Zero); WriteVector(writer, "AngularVelocity", prim.AngularVelocity); WriteVector(writer, "Acceleration", prim.Acceleration); writer.WriteElementString("Description", prim.Description); writer.WriteStartElement("Color"); writer.WriteElementString("R", prim.TextColor.R.ToString(Utils.EnUsCulture)); writer.WriteElementString("G", prim.TextColor.G.ToString(Utils.EnUsCulture)); writer.WriteElementString("B", prim.TextColor.B.ToString(Utils.EnUsCulture)); writer.WriteElementString("A", prim.TextColor.G.ToString(Utils.EnUsCulture)); writer.WriteEndElement(); writer.WriteElementString("Text", prim.Text); writer.WriteElementString("SitName", prim.SitName); writer.WriteElementString("TouchName", prim.TouchName); writer.WriteElementString("LinkNum", prim.LinkNumber.ToString()); writer.WriteElementString("ClickAction", prim.ClickAction.ToString()); writer.WriteStartElement("Shape"); writer.WriteElementString("PathBegin", Primitive.PackBeginCut(prim.Shape.PathBegin).ToString()); writer.WriteElementString("PathCurve", prim.Shape.PathCurve.ToString()); writer.WriteElementString("PathEnd", Primitive.PackEndCut(prim.Shape.PathEnd).ToString()); writer.WriteElementString("PathRadiusOffset", Primitive.PackPathTwist(prim.Shape.PathRadiusOffset).ToString()); writer.WriteElementString("PathRevolutions", Primitive.PackPathRevolutions(prim.Shape.PathRevolutions).ToString()); writer.WriteElementString("PathScaleX", Primitive.PackPathScale(prim.Shape.PathScaleX).ToString()); writer.WriteElementString("PathScaleY", Primitive.PackPathScale(prim.Shape.PathScaleY).ToString()); writer.WriteElementString("PathShearX", ((byte)Primitive.PackPathShear(prim.Shape.PathShearX)).ToString()); writer.WriteElementString("PathShearY", ((byte)Primitive.PackPathShear(prim.Shape.PathShearY)).ToString()); writer.WriteElementString("PathSkew", Primitive.PackPathTwist(prim.Shape.PathSkew).ToString()); writer.WriteElementString("PathTaperX", Primitive.PackPathTaper(prim.Shape.PathTaperX).ToString()); writer.WriteElementString("PathTaperY", Primitive.PackPathTaper(prim.Shape.PathTaperY).ToString()); writer.WriteElementString("PathTwist", Primitive.PackPathTwist(prim.Shape.PathTwist).ToString()); writer.WriteElementString("PathTwistBegin", Primitive.PackPathTwist(prim.Shape.PathTwistBegin).ToString()); writer.WriteElementString("PCode", prim.PCode.ToString()); writer.WriteElementString("ProfileBegin", Primitive.PackBeginCut(prim.Shape.ProfileBegin).ToString()); writer.WriteElementString("ProfileEnd", Primitive.PackEndCut(prim.Shape.ProfileEnd).ToString()); writer.WriteElementString("ProfileHollow", Primitive.PackProfileHollow(prim.Shape.ProfileHollow).ToString()); WriteVector(writer, "Scale", prim.Scale); writer.WriteElementString("State", prim.State.ToString()); AssetPrim.ProfileShape shape = (AssetPrim.ProfileShape)(prim.Shape.ProfileCurve & 0x0F); HoleType hole = (HoleType)(prim.Shape.ProfileCurve & 0xF0); writer.WriteElementString("ProfileShape", shape.ToString()); writer.WriteElementString("HollowShape", hole.ToString()); writer.WriteElementString("ProfileCurve", prim.Shape.ProfileCurve.ToString()); writer.WriteStartElement("TextureEntry"); byte[] te; if (prim.Textures != null) te = prim.Textures.GetBytes(); else te = Utils.EmptyBytes; writer.WriteBase64(te, 0, te.Length); writer.WriteEndElement(); // FIXME: ExtraParams writer.WriteStartElement("ExtraParams"); writer.WriteEndElement(); writer.WriteEndElement(); WriteVector(writer, "Scale", prim.Scale); writer.WriteElementString("UpdateFlag", "0"); WriteVector(writer, "SitTargetOrientation", Vector3.UnitZ); // TODO: Is this really a vector and not a quaternion? WriteVector(writer, "SitTargetPosition", prim.SitOffset); WriteVector(writer, "SitTargetPositionLL", prim.SitOffset); WriteQuaternion(writer, "SitTargetOrientationLL", prim.SitRotation); writer.WriteElementString("ParentID", prim.ParentID.ToString()); writer.WriteElementString("CreationDate", ((int)Utils.DateTimeToUnixTime(prim.CreationDate)).ToString()); writer.WriteElementString("Category", "0"); writer.WriteElementString("SalePrice", prim.SalePrice.ToString()); writer.WriteElementString("ObjectSaleType", ((int)prim.SaleType).ToString()); writer.WriteElementString("OwnershipCost", "0"); WriteUUID(writer, "GroupID", prim.GroupID); WriteUUID(writer, "OwnerID", prim.OwnerID); WriteUUID(writer, "LastOwnerID", prim.LastOwnerID); writer.WriteElementString("BaseMask", ((uint)PermissionMask.All).ToString()); writer.WriteElementString("OwnerMask", ((uint)PermissionMask.All).ToString()); writer.WriteElementString("GroupMask", ((uint)PermissionMask.All).ToString()); writer.WriteElementString("EveryoneMask", ((uint)PermissionMask.All).ToString()); writer.WriteElementString("NextOwnerMask", ((uint)PermissionMask.All).ToString()); writer.WriteElementString("Flags", "None"); WriteUUID(writer, "SitTargetAvatar", UUID.Zero); writer.WriteEndElement(); } static void WriteUUID(XmlTextWriter writer, string name, UUID id) { writer.WriteStartElement(name); writer.WriteElementString("UUID", id.ToString()); writer.WriteEndElement(); } static void WriteVector(XmlTextWriter writer, string name, Vector3 vec) { writer.WriteStartElement(name); writer.WriteElementString("X", vec.X.ToString(Utils.EnUsCulture)); writer.WriteElementString("Y", vec.Y.ToString(Utils.EnUsCulture)); writer.WriteElementString("Z", vec.Z.ToString(Utils.EnUsCulture)); writer.WriteEndElement(); } static void WriteQuaternion(XmlTextWriter writer, string name, Quaternion quat) { writer.WriteStartElement(name); writer.WriteElementString("X", quat.X.ToString(Utils.EnUsCulture)); writer.WriteElementString("Y", quat.Y.ToString(Utils.EnUsCulture)); writer.WriteElementString("Z", quat.Z.ToString(Utils.EnUsCulture)); writer.WriteElementString("W", quat.W.ToString(Utils.EnUsCulture)); writer.WriteEndElement(); } #endregion Archive Saving } }