diff --git a/Programs/SimExport/DoubleDictionary.cs b/Programs/SimExport/DoubleDictionary.cs new file mode 100644 index 00000000..2746817b --- /dev/null +++ b/Programs/SimExport/DoubleDictionary.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; + +namespace SimExport +{ + public class DoubleDictionary + { + Dictionary Dictionary1; + Dictionary Dictionary2; + object syncObject = new object(); + + public DoubleDictionary() + { + Dictionary1 = new Dictionary(); + Dictionary2 = new Dictionary(); + } + + public DoubleDictionary(int capacity) + { + Dictionary1 = new Dictionary(capacity); + Dictionary2 = new Dictionary(capacity); + } + + public void Add(TKey1 key1, TKey2 key2, TValue value) + { + lock (syncObject) + { + if (Dictionary1.ContainsKey(key1)) + { + if (!Dictionary2.ContainsKey(key2)) + throw new ArgumentException("key1 exists in the dictionary but not key2"); + } + else if (Dictionary2.ContainsKey(key2)) + { + if (!Dictionary1.ContainsKey(key1)) + throw new ArgumentException("key2 exists in the dictionary but not key1"); + } + + Dictionary1[key1] = value; + Dictionary2[key2] = value; + } + } + + public bool Remove(TKey1 key1, TKey2 key2) + { + lock (syncObject) + { + Dictionary1.Remove(key1); + return Dictionary2.Remove(key2); + } + } + + public void Clear() + { + lock (syncObject) + { + Dictionary1.Clear(); + Dictionary2.Clear(); + } + } + + public int Count + { + get { return Dictionary1.Count; } + } + + public bool ContainsKey(TKey1 key) + { + return Dictionary1.ContainsKey(key); + } + + public bool ContainsKey(TKey2 key) + { + return Dictionary2.ContainsKey(key); + } + + public bool TryGetValue(TKey1 key, out TValue value) + { + return Dictionary1.TryGetValue(key, out value); + } + + public bool TryGetValue(TKey2 key, out TValue value) + { + return Dictionary2.TryGetValue(key, out value); + } + + public void ForEach(Action action) + { + lock (syncObject) + { + foreach (TValue value in Dictionary1.Values) + action(value); + } + } + + public TValue this[TKey1 key1] + { + get { return Dictionary1[key1]; } + } + + public TValue this[TKey2 key2] + { + get { return Dictionary2[key2]; } + } + } +} diff --git a/Programs/SimExport/OarFile.cs b/Programs/SimExport/OarFile.cs new file mode 100644 index 00000000..c0104426 --- /dev/null +++ b/Programs/SimExport/OarFile.cs @@ -0,0 +1,269 @@ +?using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Xml; +using OpenMetaverse; + +namespace SimExport +{ + public class Linkset + { + public Primitive Parent; + public List Children = new List(); + } + + public class OarFile + { + enum ProfileShape : byte + { + Circle = 0, + Square = 1, + IsometricTriangle = 2, + EquilateralTriangle = 3, + RightTriangle = 4, + HalfCircle = 5 + } + + public static void PackageArchive(string directoryName, string filename) + { + const string ARCHIVE_XML = "\n"; + + TarArchiveWriter archive = new TarArchiveWriter(); + + // Create the archive.xml file + archive.AddFile("archive.xml", ARCHIVE_XML); + + // Add the assets + string[] files = Directory.GetFiles(directoryName + "/assets"); + foreach (string file in files) + archive.AddFile("assets/" + Path.GetFileName(file), File.ReadAllBytes(file)); + + // Add the objects + files = Directory.GetFiles(directoryName + "/objects"); + foreach (string file in files) + archive.AddFile("objects/" + Path.GetFileName(file), File.ReadAllBytes(file)); + + // Add the terrain(s) + files = Directory.GetFiles(directoryName + "/terrains"); + foreach (string file in files) + archive.AddFile("terrains/" + Path.GetFileName(file), File.ReadAllBytes(file)); + + archive.WriteTar(new GZipStream(new FileStream(filename, FileMode.Create), CompressionMode.Compress)); + } + + public static void SavePrims(DoubleDictionary prims, string path) + { + // Delete all of the old linkset files + try + { + Directory.Delete(path, true); + Directory.CreateDirectory(path); + } + catch (Exception ex) + { + Logger.Log("Failed saving prims: " + ex.Message, Helpers.LogLevel.Error); + return; + } + + // Copy the double dictionary to a temporary list for iterating + List primList = new List(); + prims.ForEach(delegate(Primitive prim) + { + primList.Add(prim); + }); + + foreach (Primitive p in primList) + { + if (p.ParentID == 0) + { + Linkset linkset = new Linkset(); + linkset.Parent = p; + + prims.ForEach(delegate(Primitive q) + { + if (q.ParentID == p.LocalID) + linkset.Children.Add(q); + }); + + SaveLinkset(linkset, path + "/Primitive_" + linkset.Parent.ID.ToString() + ".xml"); + } + } + } + + static void SaveLinkset(Linkset linkset, string filename) + { + try + { + using (StreamWriter stream = new StreamWriter(filename)) + { + XmlTextWriter writer = new XmlTextWriter(stream); + SOGToXml2(writer, linkset); + writer.Flush(); + } + } + catch (Exception ex) + { + Logger.Log("Failed saving linkset: " + ex.Message, Helpers.LogLevel.Error); + } + } + + static void SOGToXml2(XmlTextWriter writer, Linkset linkset) + { + writer.WriteStartElement(String.Empty, "SceneObjectGroup", String.Empty); + SOPToXml(writer, linkset.Parent, null); + writer.WriteStartElement(String.Empty, "OtherParts", String.Empty); + + foreach (Primitive child in linkset.Children) + SOPToXml(writer, child, linkset.Parent); + + writer.WriteEndElement(); + writer.WriteEndElement(); + } + + static void SOPToXml(XmlTextWriter writer, Primitive prim, Primitive 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.Properties.CreatorID); + WriteUUID(writer, "FolderID", prim.Properties.FolderID); + writer.WriteElementString("InventorySerial", prim.Properties.InventorySerial.ToString()); + writer.WriteStartElement("TaskInventory"); writer.WriteEndElement(); + writer.WriteElementString("ObjectFlags", ((int)prim.Flags).ToString()); + WriteUUID(writer, "UUID", prim.ID); + writer.WriteElementString("LocalId", prim.LocalID.ToString()); + writer.WriteElementString("Name", prim.Properties.Name); + writer.WriteElementString("Material", ((int)prim.PrimData.Material).ToString()); + writer.WriteElementString("RegionHandle", prim.RegionHandle.ToString()); + writer.WriteElementString("ScriptAccessPin", "0"); + + Vector3 groupPosition; + if (parent == null) + groupPosition = prim.Position; + else + groupPosition = parent.Position; + + WriteVector(writer, "GroupPosition", groupPosition); + WriteVector(writer, "OffsetPosition", groupPosition - prim.Position); + WriteQuaternion(writer, "RotationOffset", prim.Rotation); + WriteVector(writer, "Velocity", Vector3.Zero); + WriteVector(writer, "RotationalVelocity", Vector3.Zero); + WriteVector(writer, "AngularVelocity", prim.AngularVelocity); + WriteVector(writer, "Acceleration", Vector3.Zero); + writer.WriteElementString("Description", prim.Properties.Description); + writer.WriteStartElement("Color"); + writer.WriteElementString("R", prim.TextColor.R.ToString()); + writer.WriteElementString("G", prim.TextColor.G.ToString()); + writer.WriteElementString("B", prim.TextColor.B.ToString()); + writer.WriteElementString("A", prim.TextColor.G.ToString()); + writer.WriteEndElement(); + writer.WriteElementString("Text", prim.Text); + writer.WriteElementString("SitName", prim.Properties.SitName); + writer.WriteElementString("TouchName", prim.Properties.TouchName); + + uint linknum = 0; + //if (parent != null) + // linknum = prim.LocalID - parent.LocalID; + + writer.WriteElementString("LinkNum", linknum.ToString()); + writer.WriteElementString("ClickAction", ((int)prim.ClickAction).ToString()); + writer.WriteStartElement("Shape"); + + writer.WriteElementString("PathBegin", Primitive.PackBeginCut(prim.PrimData.PathBegin).ToString()); + writer.WriteElementString("PathCurve", ((byte)prim.PrimData.PathCurve).ToString()); + writer.WriteElementString("PathEnd", Primitive.PackEndCut(prim.PrimData.PathEnd).ToString()); + writer.WriteElementString("PathRadiusOffset", Primitive.PackPathTwist(prim.PrimData.PathRadiusOffset).ToString()); + writer.WriteElementString("PathRevolutions", Primitive.PackPathRevolutions(prim.PrimData.PathRevolutions).ToString()); + writer.WriteElementString("PathScaleX", Primitive.PackPathScale(prim.PrimData.PathScaleX).ToString()); + writer.WriteElementString("PathScaleY", Primitive.PackPathScale(prim.PrimData.PathScaleY).ToString()); + writer.WriteElementString("PathShearX", ((byte)Primitive.PackPathShear(prim.PrimData.PathShearX)).ToString()); + writer.WriteElementString("PathShearY", ((byte)Primitive.PackPathShear(prim.PrimData.PathShearY)).ToString()); + writer.WriteElementString("PathSkew", Primitive.PackPathTwist(prim.PrimData.PathSkew).ToString()); + writer.WriteElementString("PathTaperX", Primitive.PackPathTaper(prim.PrimData.PathTaperX).ToString()); + writer.WriteElementString("PathTaperY", Primitive.PackPathTaper(prim.PrimData.PathTaperY).ToString()); + writer.WriteElementString("PathTwist", Primitive.PackPathTwist(prim.PrimData.PathTwist).ToString()); + writer.WriteElementString("PathTwistBegin", Primitive.PackPathTwist(prim.PrimData.PathTwistBegin).ToString()); + writer.WriteElementString("PCode", ((byte)prim.PrimData.PCode).ToString()); + writer.WriteElementString("ProfileBegin", Primitive.PackBeginCut(prim.PrimData.ProfileBegin).ToString()); + writer.WriteElementString("ProfileEnd", Primitive.PackEndCut(prim.PrimData.ProfileEnd).ToString()); + writer.WriteElementString("ProfileHollow", Primitive.PackProfileHollow(prim.PrimData.ProfileHollow).ToString()); + WriteVector(writer, "Scale", prim.Scale); + writer.WriteElementString("State", prim.PrimData.State.ToString()); + + ProfileShape shape = (ProfileShape)prim.PrimData.ProfileCurve; + writer.WriteElementString("ProfileShape", shape.ToString()); + writer.WriteElementString("HollowShape", prim.PrimData.ProfileHole.ToString()); + writer.WriteElementString("ProfileCurve", prim.PrimData.profileCurve.ToString()); + + writer.WriteStartElement("TextureEntry"); + + byte[] te; + if (prim.Textures != null) + te = prim.Textures.ToBytes(); + else + te = new byte[0]; + + 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); + WriteVector(writer, "SitTargetPosition", Vector3.Zero); + WriteVector(writer, "SitTargetPositionLL", Vector3.Zero); + WriteQuaternion(writer, "SitTargetOrientationLL", new Quaternion(0f, 0f, 1f, 0f)); + writer.WriteElementString("ParentID", prim.ParentID.ToString()); + writer.WriteElementString("CreationDate", ((int)Utils.DateTimeToUnixTime(prim.Properties.CreationDate)).ToString()); + writer.WriteElementString("Category", ((int)prim.Properties.Category).ToString()); + writer.WriteElementString("SalePrice", prim.Properties.SalePrice.ToString()); + writer.WriteElementString("ObjectSaleType", ((int)prim.Properties.SaleType).ToString()); + writer.WriteElementString("OwnershipCost", prim.Properties.OwnershipCost.ToString()); + WriteUUID(writer, "GroupID", prim.GroupID); + WriteUUID(writer, "OwnerID", prim.OwnerID); + WriteUUID(writer, "LastOwnerID", prim.Properties.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()); + writer.WriteElementString("Y", vec.Y.ToString()); + writer.WriteElementString("Z", vec.Z.ToString()); + writer.WriteEndElement(); + } + + static void WriteQuaternion(XmlTextWriter writer, string name, Quaternion quat) + { + writer.WriteStartElement(name); + writer.WriteElementString("X", quat.X.ToString()); + writer.WriteElementString("Y", quat.Y.ToString()); + writer.WriteElementString("Z", quat.Z.ToString()); + writer.WriteElementString("W", quat.W.ToString()); + writer.WriteEndElement(); + } + } +} diff --git a/Programs/SimExport/Options.cs b/Programs/SimExport/Options.cs new file mode 100644 index 00000000..6f232124 --- /dev/null +++ b/Programs/SimExport/Options.cs @@ -0,0 +1,1103 @@ +// +// Options.cs +// +// Authors: +// Jonathan Pryor +// +// Copyright (C) 2008 Novell (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +// Compile With: +// gmcs -debug+ -r:System.Core Options.cs -o:NDesk.Options.dll +// gmcs -debug+ -d:LINQ -r:System.Core Options.cs -o:NDesk.Options.dll +// +// The LINQ version just changes the implementation of +// OptionSet.Parse(IEnumerable), and confers no semantic changes. + +// +// A Getopt::Long-inspired option parsing library for C#. +// +// NDesk.Options.OptionSet is built upon a key/value table, where the +// key is a option format string and the value is a delegate that is +// invoked when the format string is matched. +// +// Option format strings: +// Regex-like BNF Grammar: +// name: .+ +// type: [=:] +// sep: ( [^{}]+ | '{' .+ '}' )? +// aliases: ( name type sep ) ( '|' name type sep )* +// +// Each '|'-delimited name is an alias for the associated action. If the +// format string ends in a '=', it has a required value. If the format +// string ends in a ':', it has an optional value. If neither '=' or ':' +// is present, no value is supported. `=' or `:' need only be defined on one +// alias, but if they are provided on more than one they must be consistent. +// +// Each alias portion may also end with a "key/value separator", which is used +// to split option values if the option accepts > 1 value. If not specified, +// it defaults to '=' and ':'. If specified, it can be any character except +// '{' and '}' OR the *string* between '{' and '}'. If no separator should be +// used (i.e. the separate values should be distinct arguments), then "{}" +// should be used as the separator. +// +// Options are extracted either from the current option by looking for +// the option name followed by an '=' or ':', or is taken from the +// following option IFF: +// - The current option does not contain a '=' or a ':' +// - The current option requires a value (i.e. not a Option type of ':') +// +// The `name' used in the option format string does NOT include any leading +// option indicator, such as '-', '--', or '/'. All three of these are +// permitted/required on any named option. +// +// Option bundling is permitted so long as: +// - '-' is used to start the option group +// - all of the bundled options are a single character +// - at most one of the bundled options accepts a value, and the value +// provided starts from the next character to the end of the string. +// +// This allows specifying '-a -b -c' as '-abc', and specifying '-D name=value' +// as '-Dname=value'. +// +// Option processing is disabled by specifying "--". All options after "--" +// are returned by OptionSet.Parse() unchanged and unprocessed. +// +// Unprocessed options are returned from OptionSet.Parse(). +// +// Examples: +// int verbose = 0; +// OptionSet p = new OptionSet () +// .Add ("v", v => ++verbose) +// .Add ("name=|value=", v => Console.WriteLine (v)); +// p.Parse (new string[]{"-v", "--v", "/v", "-name=A", "/name", "B", "extra"}); +// +// The above would parse the argument string array, and would invoke the +// lambda expression three times, setting `verbose' to 3 when complete. +// It would also print out "A" and "B" to standard output. +// The returned array would contain the string "extra". +// +// C# 3.0 collection initializers are supported and encouraged: +// var p = new OptionSet () { +// { "h|?|help", v => ShowHelp () }, +// }; +// +// System.ComponentModel.TypeConverter is also supported, allowing the use of +// custom data types in the callback type; TypeConverter.ConvertFromString() +// is used to convert the value option to an instance of the specified +// type: +// +// var p = new OptionSet () { +// { "foo=", (Foo f) => Console.WriteLine (f.ToString ()) }, +// }; +// +// Random other tidbits: +// - Boolean options (those w/o '=' or ':' in the option format string) +// are explicitly enabled if they are followed with '+', and explicitly +// disabled if they are followed with '-': +// string a = null; +// var p = new OptionSet () { +// { "a", s => a = s }, +// }; +// p.Parse (new string[]{"-a"}); // sets v != null +// p.Parse (new string[]{"-a+"}); // sets v != null +// p.Parse (new string[]{"-a-"}); // sets v == null +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Runtime.Serialization; +using System.Security.Permissions; +using System.Text; +using System.Text.RegularExpressions; + +#if LINQ +using System.Linq; +#endif + +#if TEST +using NDesk.Options; +#endif + +namespace NDesk.Options { + + public class OptionValueCollection : IList, IList { + + List values = new List (); + OptionContext c; + + internal OptionValueCollection (OptionContext c) + { + this.c = c; + } + + #region ICollection + void ICollection.CopyTo (Array array, int index) {(values as ICollection).CopyTo (array, index);} + bool ICollection.IsSynchronized {get {return (values as ICollection).IsSynchronized;}} + object ICollection.SyncRoot {get {return (values as ICollection).SyncRoot;}} + #endregion + + #region ICollection + public void Add (string item) {values.Add (item);} + public void Clear () {values.Clear ();} + public bool Contains (string item) {return values.Contains (item);} + public void CopyTo (string[] array, int arrayIndex) {values.CopyTo (array, arrayIndex);} + public bool Remove (string item) {return values.Remove (item);} + public int Count {get {return values.Count;}} + public bool IsReadOnly {get {return false;}} + #endregion + + #region IEnumerable + IEnumerator IEnumerable.GetEnumerator () {return values.GetEnumerator ();} + #endregion + + #region IEnumerable + public IEnumerator GetEnumerator () {return values.GetEnumerator ();} + #endregion + + #region IList + int IList.Add (object value) {return (values as IList).Add (value);} + bool IList.Contains (object value) {return (values as IList).Contains (value);} + int IList.IndexOf (object value) {return (values as IList).IndexOf (value);} + void IList.Insert (int index, object value) {(values as IList).Insert (index, value);} + void IList.Remove (object value) {(values as IList).Remove (value);} + void IList.RemoveAt (int index) {(values as IList).RemoveAt (index);} + bool IList.IsFixedSize {get {return false;}} + object IList.this [int index] {get {return this [index];} set {(values as IList)[index] = value;}} + #endregion + + #region IList + public int IndexOf (string item) {return values.IndexOf (item);} + public void Insert (int index, string item) {values.Insert (index, item);} + public void RemoveAt (int index) {values.RemoveAt (index);} + + private void AssertValid (int index) + { + if (c.Option == null) + throw new InvalidOperationException ("OptionContext.Option is null."); + if (index >= c.Option.MaxValueCount) + throw new ArgumentOutOfRangeException ("index"); + if (c.Option.OptionValueType == OptionValueType.Required && + index >= values.Count) + throw new OptionException (string.Format ( + c.OptionSet.MessageLocalizer ("Missing required value for option '{0}'."), c.OptionName), + c.OptionName); + } + + public string this [int index] { + get { + AssertValid (index); + return index >= values.Count ? null : values [index]; + } + set { + values [index] = value; + } + } + #endregion + + public List ToList () + { + return new List (values); + } + + public string[] ToArray () + { + return values.ToArray (); + } + + public override string ToString () + { + return string.Join (", ", values.ToArray ()); + } + } + + public class OptionContext { + private Option option; + private string name; + private int index; + private OptionSet set; + private OptionValueCollection c; + + public OptionContext (OptionSet set) + { + this.set = set; + this.c = new OptionValueCollection (this); + } + + public Option Option { + get {return option;} + set {option = value;} + } + + public string OptionName { + get {return name;} + set {name = value;} + } + + public int OptionIndex { + get {return index;} + set {index = value;} + } + + public OptionSet OptionSet { + get {return set;} + } + + public OptionValueCollection OptionValues { + get {return c;} + } + } + + public enum OptionValueType { + None, + Optional, + Required, + } + + public abstract class Option { + string prototype, description; + string[] names; + OptionValueType type; + int count; + string[] separators; + + protected Option (string prototype, string description) + : this (prototype, description, 1) + { + } + + protected Option (string prototype, string description, int maxValueCount) + { + if (prototype == null) + throw new ArgumentNullException ("prototype"); + if (prototype.Length == 0) + throw new ArgumentException ("Cannot be the empty string.", "prototype"); + if (maxValueCount < 0) + throw new ArgumentOutOfRangeException ("maxValueCount"); + + this.prototype = prototype; + this.names = prototype.Split ('|'); + this.description = description; + this.count = maxValueCount; + this.type = ParsePrototype (); + + if (this.count == 0 && type != OptionValueType.None) + throw new ArgumentException ( + "Cannot provide maxValueCount of 0 for OptionValueType.Required or " + + "OptionValueType.Optional.", + "maxValueCount"); + if (this.type == OptionValueType.None && maxValueCount > 1) + throw new ArgumentException ( + string.Format ("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount), + "maxValueCount"); + if (Array.IndexOf (names, "<>") >= 0 && + ((names.Length == 1 && this.type != OptionValueType.None) || + (names.Length > 1 && this.MaxValueCount > 1))) + throw new ArgumentException ( + "The default option handler '<>' cannot require values.", + "prototype"); + } + + public string Prototype {get {return prototype;}} + public string Description {get {return description;}} + public OptionValueType OptionValueType {get {return type;}} + public int MaxValueCount {get {return count;}} + + public string[] GetNames () + { + return (string[]) names.Clone (); + } + + public string[] GetValueSeparators () + { + if (separators == null) + return new string [0]; + return (string[]) separators.Clone (); + } + + protected static T Parse (string value, OptionContext c) + { + TypeConverter conv = TypeDescriptor.GetConverter (typeof (T)); + T t = default (T); + try { + if (value != null) + t = (T) conv.ConvertFromString (value); + } + catch (Exception e) { + throw new OptionException ( + string.Format ( + c.OptionSet.MessageLocalizer ("Could not convert string `{0}' to type {1} for option `{2}'."), + value, typeof (T).Name, c.OptionName), + c.OptionName, e); + } + return t; + } + + internal string[] Names {get {return names;}} + internal string[] ValueSeparators {get {return separators;}} + + static readonly char[] NameTerminator = new char[]{'=', ':'}; + + private OptionValueType ParsePrototype () + { + char type = '\0'; + List seps = new List (); + for (int i = 0; i < names.Length; ++i) { + string name = names [i]; + if (name.Length == 0) + throw new ArgumentException ("Empty option names are not supported.", "prototype"); + + int end = name.IndexOfAny (NameTerminator); + if (end == -1) + continue; + names [i] = name.Substring (0, end); + if (type == '\0' || type == name [end]) + type = name [end]; + else + throw new ArgumentException ( + string.Format ("Conflicting option types: '{0}' vs. '{1}'.", type, name [end]), + "prototype"); + AddSeparators (name, end, seps); + } + + if (type == '\0') + return OptionValueType.None; + + if (count <= 1 && seps.Count != 0) + throw new ArgumentException ( + string.Format ("Cannot provide key/value separators for Options taking {0} value(s).", count), + "prototype"); + if (count > 1) { + if (seps.Count == 0) + this.separators = new string[]{":", "="}; + else if (seps.Count == 1 && seps [0].Length == 0) + this.separators = null; + else + this.separators = seps.ToArray (); + } + + return type == '=' ? OptionValueType.Required : OptionValueType.Optional; + } + + private static void AddSeparators (string name, int end, ICollection seps) + { + int start = -1; + for (int i = end+1; i < name.Length; ++i) { + switch (name [i]) { + case '{': + if (start != -1) + throw new ArgumentException ( + string.Format ("Ill-formed name/value separator found in \"{0}\".", name), + "prototype"); + start = i+1; + break; + case '}': + if (start == -1) + throw new ArgumentException ( + string.Format ("Ill-formed name/value separator found in \"{0}\".", name), + "prototype"); + seps.Add (name.Substring (start, i-start)); + start = -1; + break; + default: + if (start == -1) + seps.Add (name [i].ToString ()); + break; + } + } + if (start != -1) + throw new ArgumentException ( + string.Format ("Ill-formed name/value separator found in \"{0}\".", name), + "prototype"); + } + + public void Invoke (OptionContext c) + { + OnParseComplete (c); + c.OptionName = null; + c.Option = null; + c.OptionValues.Clear (); + } + + protected abstract void OnParseComplete (OptionContext c); + + public override string ToString () + { + return Prototype; + } + } + + [Serializable] + public class OptionException : Exception { + private string option; + + public OptionException () + { + } + + public OptionException (string message, string optionName) + : base (message) + { + this.option = optionName; + } + + public OptionException (string message, string optionName, Exception innerException) + : base (message, innerException) + { + this.option = optionName; + } + + protected OptionException (SerializationInfo info, StreamingContext context) + : base (info, context) + { + this.option = info.GetString ("OptionName"); + } + + public string OptionName { + get {return this.option;} + } + + [SecurityPermission (SecurityAction.LinkDemand, SerializationFormatter = true)] + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + info.AddValue ("OptionName", option); + } + } + + public delegate void OptionAction (TKey key, TValue value); + + public class OptionSet : KeyedCollection + { + public OptionSet () + : this (delegate (string f) {return f;}) + { + } + + public OptionSet (Converter localizer) + { + this.localizer = localizer; + } + + Converter localizer; + + public Converter MessageLocalizer { + get {return localizer;} + } + + protected override string GetKeyForItem (Option item) + { + if (item == null) + throw new ArgumentNullException ("option"); + if (item.Names != null && item.Names.Length > 0) + return item.Names [0]; + // This should never happen, as it's invalid for Option to be + // constructed w/o any names. + throw new InvalidOperationException ("Option has no names!"); + } + + [Obsolete ("Use KeyedCollection.this[string]")] + protected Option GetOptionForName (string option) + { + if (option == null) + throw new ArgumentNullException ("option"); + try { + return base [option]; + } + catch (KeyNotFoundException) { + return null; + } + } + + protected override void InsertItem (int index, Option item) + { + base.InsertItem (index, item); + AddImpl (item); + } + + protected override void RemoveItem (int index) + { + base.RemoveItem (index); + Option p = Items [index]; + // KeyedCollection.RemoveItem() handles the 0th item + for (int i = 1; i < p.Names.Length; ++i) { + Dictionary.Remove (p.Names [i]); + } + } + + protected override void SetItem (int index, Option item) + { + base.SetItem (index, item); + RemoveItem (index); + AddImpl (item); + } + + private void AddImpl (Option option) + { + if (option == null) + throw new ArgumentNullException ("option"); + List added = new List (option.Names.Length); + try { + // KeyedCollection.InsertItem/SetItem handle the 0th name. + for (int i = 1; i < option.Names.Length; ++i) { + Dictionary.Add (option.Names [i], option); + added.Add (option.Names [i]); + } + } + catch (Exception) { + foreach (string name in added) + Dictionary.Remove (name); + throw; + } + } + + public new OptionSet Add (Option option) + { + base.Add (option); + return this; + } + + sealed class ActionOption : Option { + Action action; + + public ActionOption (string prototype, string description, int count, Action action) + : base (prototype, description, count) + { + if (action == null) + throw new ArgumentNullException ("action"); + this.action = action; + } + + protected override void OnParseComplete (OptionContext c) + { + action (c.OptionValues); + } + } + + public OptionSet Add (string prototype, Action action) + { + return Add (prototype, null, action); + } + + public OptionSet Add (string prototype, string description, Action action) + { + if (action == null) + throw new ArgumentNullException ("action"); + Option p = new ActionOption (prototype, description, 1, + delegate (OptionValueCollection v) { action (v [0]); }); + base.Add (p); + return this; + } + + public OptionSet Add (string prototype, OptionAction action) + { + return Add (prototype, null, action); + } + + public OptionSet Add (string prototype, string description, OptionAction action) + { + if (action == null) + throw new ArgumentNullException ("action"); + Option p = new ActionOption (prototype, description, 2, + delegate (OptionValueCollection v) {action (v [0], v [1]);}); + base.Add (p); + return this; + } + + sealed class ActionOption : Option { + Action action; + + public ActionOption (string prototype, string description, Action action) + : base (prototype, description, 1) + { + if (action == null) + throw new ArgumentNullException ("action"); + this.action = action; + } + + protected override void OnParseComplete (OptionContext c) + { + action (Parse (c.OptionValues [0], c)); + } + } + + sealed class ActionOption : Option { + OptionAction action; + + public ActionOption (string prototype, string description, OptionAction action) + : base (prototype, description, 2) + { + if (action == null) + throw new ArgumentNullException ("action"); + this.action = action; + } + + protected override void OnParseComplete (OptionContext c) + { + action ( + Parse (c.OptionValues [0], c), + Parse (c.OptionValues [1], c)); + } + } + + public OptionSet Add (string prototype, Action action) + { + return Add (prototype, null, action); + } + + public OptionSet Add (string prototype, string description, Action action) + { + return Add (new ActionOption (prototype, description, action)); + } + + public OptionSet Add (string prototype, OptionAction action) + { + return Add (prototype, null, action); + } + + public OptionSet Add (string prototype, string description, OptionAction action) + { + return Add (new ActionOption (prototype, description, action)); + } + + protected virtual OptionContext CreateOptionContext () + { + return new OptionContext (this); + } + +#if LINQ + public List Parse (IEnumerable arguments) + { + bool process = true; + OptionContext c = CreateOptionContext (); + c.OptionIndex = -1; + var def = GetOptionForName ("<>"); + var unprocessed = + from argument in arguments + where ++c.OptionIndex >= 0 && (process || def != null) + ? process + ? argument == "--" + ? (process = false) + : !Parse (argument, c) + ? def != null + ? Unprocessed (null, def, c, argument) + : true + : false + : def != null + ? Unprocessed (null, def, c, argument) + : true + : true + select argument; + List r = unprocessed.ToList (); + if (c.Option != null) + c.Option.Invoke (c); + return r; + } +#else + public List Parse (IEnumerable arguments) + { + OptionContext c = CreateOptionContext (); + c.OptionIndex = -1; + bool process = true; + List unprocessed = new List (); + Option def = Contains ("<>") ? this ["<>"] : null; + foreach (string argument in arguments) { + ++c.OptionIndex; + if (argument == "--") { + process = false; + continue; + } + if (!process) { + Unprocessed (unprocessed, def, c, argument); + continue; + } + if (!Parse (argument, c)) + Unprocessed (unprocessed, def, c, argument); + } + if (c.Option != null) + c.Option.Invoke (c); + return unprocessed; + } +#endif + + private static bool Unprocessed (ICollection extra, Option def, OptionContext c, string argument) + { + if (def == null) { + extra.Add (argument); + return false; + } + c.OptionValues.Add (argument); + c.Option = def; + c.Option.Invoke (c); + return false; + } + + private readonly Regex ValueOption = new Regex ( + @"^(?--|-|/)(?[^:=]+)((?[:=])(?.*))?$"); + + protected bool GetOptionParts (string argument, out string flag, out string name, out string sep, out string value) + { + if (argument == null) + throw new ArgumentNullException ("argument"); + + flag = name = sep = value = null; + Match m = ValueOption.Match (argument); + if (!m.Success) { + return false; + } + flag = m.Groups ["flag"].Value; + name = m.Groups ["name"].Value; + if (m.Groups ["sep"].Success && m.Groups ["value"].Success) { + sep = m.Groups ["sep"].Value; + value = m.Groups ["value"].Value; + } + return true; + } + + protected virtual bool Parse (string argument, OptionContext c) + { + if (c.Option != null) { + ParseValue (argument, c); + return true; + } + + string f, n, s, v; + if (!GetOptionParts (argument, out f, out n, out s, out v)) + return false; + + Option p; + if (Contains (n)) { + p = this [n]; + c.OptionName = f + n; + c.Option = p; + switch (p.OptionValueType) { + case OptionValueType.None: + c.OptionValues.Add (n); + c.Option.Invoke (c); + break; + case OptionValueType.Optional: + case OptionValueType.Required: + ParseValue (v, c); + break; + } + return true; + } + // no match; is it a bool option? + if (ParseBool (argument, n, c)) + return true; + // is it a bundled option? + if (ParseBundledValue (f, string.Concat (n + s + v), c)) + return true; + + return false; + } + + private void ParseValue (string option, OptionContext c) + { + if (option != null) + foreach (string o in c.Option.ValueSeparators != null + ? option.Split (c.Option.ValueSeparators, StringSplitOptions.None) + : new string[]{option}) { + c.OptionValues.Add (o); + } + if (c.OptionValues.Count == c.Option.MaxValueCount || + c.Option.OptionValueType == OptionValueType.Optional) + c.Option.Invoke (c); + else if (c.OptionValues.Count > c.Option.MaxValueCount) { + throw new OptionException (localizer (string.Format ( + "Error: Found {0} option values when expecting {1}.", + c.OptionValues.Count, c.Option.MaxValueCount)), + c.OptionName); + } + } + + private bool ParseBool (string option, string n, OptionContext c) + { + Option p; + string rn; + if (n.Length >= 1 && (n [n.Length-1] == '+' || n [n.Length-1] == '-') && + Contains ((rn = n.Substring (0, n.Length-1)))) { + p = this [rn]; + string v = n [n.Length-1] == '+' ? option : null; + c.OptionName = option; + c.Option = p; + c.OptionValues.Add (v); + p.Invoke (c); + return true; + } + return false; + } + + private bool ParseBundledValue (string f, string n, OptionContext c) + { + if (f != "-") + return false; + for (int i = 0; i < n.Length; ++i) { + Option p; + string opt = f + n [i].ToString (); + string rn = n [i].ToString (); + if (!Contains (rn)) { + if (i == 0) + return false; + throw new OptionException (string.Format (localizer ( + "Cannot bundle unregistered option '{0}'."), opt), opt); + } + p = this [rn]; + switch (p.OptionValueType) { + case OptionValueType.None: + Invoke (c, opt, n, p); + break; + case OptionValueType.Optional: + case OptionValueType.Required: { + string v = n.Substring (i+1); + c.Option = p; + c.OptionName = opt; + ParseValue (v.Length != 0 ? v : null, c); + return true; + } + default: + throw new InvalidOperationException ("Unknown OptionValueType: " + p.OptionValueType); + } + } + return true; + } + + private static void Invoke (OptionContext c, string name, string value, Option option) + { + c.OptionName = name; + c.Option = option; + c.OptionValues.Add (value); + option.Invoke (c); + } + + private const int OptionWidth = 29; + + public void WriteOptionDescriptions (TextWriter o) + { + foreach (Option p in this) { + int written = 0; + if (!WriteOptionPrototype (o, p, ref written)) + continue; + + if (written < OptionWidth) + o.Write (new string (' ', OptionWidth - written)); + else { + o.WriteLine (); + o.Write (new string (' ', OptionWidth)); + } + + List lines = GetLines (localizer (GetDescription (p.Description))); + o.WriteLine (lines [0]); + string prefix = new string (' ', OptionWidth+2); + for (int i = 1; i < lines.Count; ++i) { + o.Write (prefix); + o.WriteLine (lines [i]); + } + } + } + + bool WriteOptionPrototype (TextWriter o, Option p, ref int written) + { + string[] names = p.Names; + + int i = GetNextOptionIndex (names, 0); + if (i == names.Length) + return false; + + if (names [i].Length == 1) { + Write (o, ref written, " -"); + Write (o, ref written, names [0]); + } + else { + Write (o, ref written, " --"); + Write (o, ref written, names [0]); + } + + for ( i = GetNextOptionIndex (names, i+1); + i < names.Length; i = GetNextOptionIndex (names, i+1)) { + Write (o, ref written, ", "); + Write (o, ref written, names [i].Length == 1 ? "-" : "--"); + Write (o, ref written, names [i]); + } + + if (p.OptionValueType == OptionValueType.Optional || + p.OptionValueType == OptionValueType.Required) { + if (p.OptionValueType == OptionValueType.Optional) { + Write (o, ref written, localizer ("[")); + } + Write (o, ref written, localizer ("=" + GetArgumentName (0, p.MaxValueCount, p.Description))); + string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0 + ? p.ValueSeparators [0] + : " "; + for (int c = 1; c < p.MaxValueCount; ++c) { + Write (o, ref written, localizer (sep + GetArgumentName (c, p.MaxValueCount, p.Description))); + } + if (p.OptionValueType == OptionValueType.Optional) { + Write (o, ref written, localizer ("]")); + } + } + return true; + } + + static int GetNextOptionIndex (string[] names, int i) + { + while (i < names.Length && names [i] == "<>") { + ++i; + } + return i; + } + + static void Write (TextWriter o, ref int n, string s) + { + n += s.Length; + o.Write (s); + } + + private static string GetArgumentName (int index, int maxIndex, string description) + { + if (description == null) + return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1); + string[] nameStart; + if (maxIndex == 1) + nameStart = new string[]{"{0:", "{"}; + else + nameStart = new string[]{"{" + index + ":"}; + for (int i = 0; i < nameStart.Length; ++i) { + int start, j = 0; + do { + start = description.IndexOf (nameStart [i], j); + } while (start >= 0 && j != 0 ? description [j++ - 1] == '{' : false); + if (start == -1) + continue; + int end = description.IndexOf ("}", start); + if (end == -1) + continue; + return description.Substring (start + nameStart [i].Length, end - start - nameStart [i].Length); + } + return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1); + } + + private static string GetDescription (string description) + { + if (description == null) + return string.Empty; + StringBuilder sb = new StringBuilder (description.Length); + int start = -1; + for (int i = 0; i < description.Length; ++i) { + switch (description [i]) { + case '{': + if (i == start) { + sb.Append ('{'); + start = -1; + } + else if (start < 0) + start = i + 1; + break; + case '}': + if (start < 0) { + if ((i+1) == description.Length || description [i+1] != '}') + throw new InvalidOperationException ("Invalid option description: " + description); + ++i; + sb.Append ("}"); + } + else { + sb.Append (description.Substring (start, i - start)); + start = -1; + } + break; + case ':': + if (start < 0) + goto default; + start = i + 1; + break; + default: + if (start < 0) + sb.Append (description [i]); + break; + } + } + return sb.ToString (); + } + + private static List GetLines (string description) + { + List lines = new List (); + if (string.IsNullOrEmpty (description)) { + lines.Add (string.Empty); + return lines; + } + int length = 80 - OptionWidth - 2; + int start = 0, end; + do { + end = GetLineEnd (start, length, description); + bool cont = false; + if (end < description.Length) { + char c = description [end]; + if (c == '-' || (char.IsWhiteSpace (c) && c != '\n')) + ++end; + else if (c != '\n') { + cont = true; + --end; + } + } + lines.Add (description.Substring (start, end - start)); + if (cont) { + lines [lines.Count-1] += "-"; + } + start = end; + if (start < description.Length && description [start] == '\n') + ++start; + } while (end < description.Length); + return lines; + } + + private static int GetLineEnd (int start, int length, string description) + { + int end = Math.Min (start + length, description.Length); + int sep = -1; + for (int i = start; i < end; ++i) { + switch (description [i]) { + case ' ': + case '\t': + case '\v': + case '-': + case ',': + case '.': + case ';': + sep = i; + break; + case '\n': + return i; + } + } + if (sep == -1 || end == description.Length) + return end; + return sep; + } + } +} + diff --git a/Programs/SimExport/SimExport.cs b/Programs/SimExport/SimExport.cs new file mode 100644 index 00000000..b4954acb --- /dev/null +++ b/Programs/SimExport/SimExport.cs @@ -0,0 +1,443 @@ +?using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Xml; +using OpenMetaverse; + +namespace SimExport +{ + public class SimExport + { + GridClient client; + TexturePipeline texturePipeline; + volatile bool running; + + int totalPrims = -1; + object totalPrimsLock = new object(); + DoubleDictionary prims = new DoubleDictionary(); + Dictionary selectedPrims = new Dictionary(); + Dictionary texturesFinished = new Dictionary(); + BlockingQueue primsAwaitingSelect = new BlockingQueue(); + string filename; + string directoryname; + + public SimExport(string firstName, string lastName, string password, string loginServer, string regionName, string filename) + { + this.filename = filename; + directoryname = Path.GetFileNameWithoutExtension(filename); + + try + { + if (!Directory.Exists(directoryname)) Directory.CreateDirectory(filename); + if (!Directory.Exists(directoryname + "/assets")) Directory.CreateDirectory(directoryname + "/assets"); + if (!Directory.Exists(directoryname + "/objects")) Directory.CreateDirectory(directoryname + "/objects"); + if (!Directory.Exists(directoryname + "/terrains")) Directory.CreateDirectory(directoryname + "/terrains"); + + CheckTextures(); + } + catch (Exception ex) { Logger.Log(ex.Message, Helpers.LogLevel.Error); return; } + + running = true; + + client = new GridClient(); + texturePipeline = new TexturePipeline(client); + texturePipeline.OnDownloadFinished += new TexturePipeline.DownloadFinishedCallback(texturePipeline_OnDownloadFinished); + + //Settings.LOG_LEVEL = Helpers.LogLevel.Info; + client.Settings.MULTIPLE_SIMS = false; + client.Settings.PARCEL_TRACKING = true; + client.Settings.ALWAYS_REQUEST_PARCEL_ACL = true; + client.Settings.ALWAYS_REQUEST_PARCEL_DWELL = false; + client.Settings.ALWAYS_REQUEST_OBJECTS = true; + client.Settings.STORE_LAND_PATCHES = true; + client.Settings.SEND_AGENT_UPDATES = true; + client.Settings.DISABLE_AGENT_UPDATE_DUPLICATE_CHECK = true; + + client.Network.OnCurrentSimChanged += Network_OnCurrentSimChanged; + client.Objects.OnNewPrim += Objects_OnNewPrim; + client.Objects.OnNewFoliage += Objects_OnNewPrim; + client.Objects.OnObjectKilled += Objects_OnObjectKilled; + client.Objects.OnObjectProperties += Objects_OnObjectProperties; + client.Objects.OnObjectUpdated += Objects_OnObjectUpdated; + client.Parcels.OnSimParcelsDownloaded += new ParcelManager.SimParcelsDownloaded(Parcels_OnSimParcelsDownloaded); + + LoginParams loginParams = client.Network.DefaultLoginParams(firstName, lastName, password, "SimExport", "0.0.1"); + loginParams.URI = loginServer; + loginParams.Start = NetworkManager.StartLocation(regionName, 128, 128, 40); + + if (client.Network.Login(loginParams)) + { + Run(); + } + else + { + Logger.Log(String.Format("Login failed ({0}: {1}", client.Network.LoginErrorKey, client.Network.LoginMessage), + Helpers.LogLevel.Error); + } + } + + void CheckTextures() + { + lock (texturesFinished) + { + string[] files = Directory.GetFiles(directoryname + "/assets", "*.jp2"); + + foreach (string file in files) + { + // Parse the UUID out of the filename + UUID id; + if (UUID.TryParse(Path.GetFileNameWithoutExtension(file).Substring(0, 36), out id)) + texturesFinished[id] = id; + } + } + + Logger.Log(String.Format("Found {0} previously downloaded texture assets", texturesFinished.Count), + Helpers.LogLevel.Info); + } + + void texturePipeline_OnDownloadFinished(UUID id, bool success) + { + if (success) + { + // Save this texture to the hard drive + ImageDownload image = texturePipeline.GetTextureToRender(id); + try + { + File.WriteAllBytes(directoryname + "/assets/" + id.ToString() + "_texture.jp2", image.AssetData); + lock (texturesFinished) texturesFinished[id] = id; + } + catch (Exception ex) + { + Logger.Log("Failed to save texture: " + ex.Message, Helpers.LogLevel.Error); + } + } + else + { + Logger.Log("Texture failed to download: " + id.ToString(), Helpers.LogLevel.Warning); + } + } + + void Run() + { + // Start the thread that monitors the queue of prims that need ObjectSelect packets sent + Thread thread = new Thread(new ThreadStart(MonitorPrimsAwaitingSelect)); + thread.Start(); + + while (running) + { + string command = Console.ReadLine(); + + switch (command) + { + case "queue": + Logger.Log(String.Format("Client Outbox contains {0} packets, ObjectSelect queue contains {1} prims", + client.Network.OutboxCount, primsAwaitingSelect.Count), Helpers.LogLevel.Info); + break; + case "prims": + Logger.Log(String.Format("Prims captured: {0}, Total: {1}", prims.Count, totalPrims), Helpers.LogLevel.Info); + break; + case "parcels": + if (!client.Network.CurrentSim.IsParcelMapFull()) + { + Logger.Log("Downloading sim parcel information and prim totals", Helpers.LogLevel.Info); + client.Parcels.RequestAllSimParcels(client.Network.CurrentSim, false, 10); + } + else + { + Logger.Log("Sim parcel information has been retrieved", Helpers.LogLevel.Info); + } + break; + case "camera": + Thread cameraThread = new Thread(new ThreadStart(MoveCamera)); + cameraThread.Start(); + Logger.Log("Started random camera movement thread", Helpers.LogLevel.Info); + break; + case "movement": + Vector3 destination = RandomPosition(); + Logger.Log("Teleporting to " + destination.ToString(), Helpers.LogLevel.Info); + client.Self.Teleport(client.Network.CurrentSim.Handle, destination, RandomPosition()); + break; + case "textures": + Logger.Log(String.Format("Current texture requests: {0}, queued texture requests: {1}, completed textures: {2}", + texturePipeline.CurrentCount, texturePipeline.QueuedCount, texturesFinished.Count), Helpers.LogLevel.Info); + break; + case "terrain": + TerrainPatch[] patches; + if (client.Terrain.SimPatches.TryGetValue(client.Network.CurrentSim.Handle, out patches)) + { + int count = 0; + for (int i = 0; i < patches.Length; i++) + { + if (patches[i] != null) + ++count; + } + + Logger.Log(count + " terrain patches have been received for the current simulator", Helpers.LogLevel.Info); + } + else + { + Logger.Log("No terrain information received for the current simulator", Helpers.LogLevel.Info); + } + break; + case "saveterrain": + if (client.Terrain.SimPatches.TryGetValue(client.Network.CurrentSim.Handle, out patches)) + { + try + { + using (FileStream stream = new FileStream(directoryname + "/terrains/heightmap.r32", FileMode.Create, FileAccess.Write)) + { + for (int y = 0; y < 256; y++) + { + for (int x = 0; x < 256; x++) + { + int xBlock = x / 16; + int yBlock = y / 16; + int xOff = x - (xBlock * 16); + int yOff = y - (yBlock * 16); + + TerrainPatch patch = patches[yBlock * 16 + xBlock]; + float t = 0f; + + if (patch != null) + t = patch.Data[yOff * 16 + xOff]; + else + Logger.Log(String.Format("Skipping missing patch at {0},{1}", xBlock, yBlock), + Helpers.LogLevel.Warning); + + stream.Write(BitConverter.GetBytes(t), 0, 4); + } + } + } + } + catch (Exception ex) + { + Logger.Log("Failed saving terrain: " + ex.Message, Helpers.LogLevel.Error); + } + } + else + { + Logger.Log("No terrain information received for the current simulator", Helpers.LogLevel.Info); + } + break; + case "save": + Logger.Log(String.Format("Preparing to serialize {0} objects", prims.Count), Helpers.LogLevel.Info); + OarFile.SavePrims(prims, directoryname + "/objects"); + Logger.Log("Saving " + directoryname, Helpers.LogLevel.Info); + OarFile.PackageArchive(directoryname, filename); + Logger.Log("Done", Helpers.LogLevel.Info); + break; + case "quit": + End(); + break; + } + } + } + + + Random random = new Random(); + + Vector3 RandomPosition() + { + float x = (float)(random.NextDouble() * 256d); + float y = (float)(random.NextDouble() * 128d); + float z = (float)(random.NextDouble() * 256d); + + return new Vector3(x, y, z); + } + + void MoveCamera() + { + while (running) + { + if (client.Network.Connected) + { + // TWEAK: Randomize far distance to force an interest list recomputation + float far = (float)(random.NextDouble() * 252d + 4d); + + // Random small movements + AgentManager.ControlFlags flags = AgentManager.ControlFlags.NONE; + if (far < 96f) + flags |= AgentManager.ControlFlags.AGENT_CONTROL_TURN_LEFT; + else if (far < 196f) + flags |= AgentManager.ControlFlags.AGENT_CONTROL_TURN_RIGHT; + else if (far < 212f) + flags |= AgentManager.ControlFlags.AGENT_CONTROL_UP_POS; + else + flags |= AgentManager.ControlFlags.AGENT_CONTROL_UP_NEG; + + // Randomly change the camera position + Vector3 pos = RandomPosition(); + + client.Self.Movement.SendManualUpdate( + flags, pos, Vector3.UnitZ, Vector3.UnitX, Vector3.UnitY, Quaternion.Identity, Quaternion.Identity, far, + AgentManager.AgentFlags.None, AgentManager.AgentState.None, false); + } + + Thread.Sleep(500); + } + } + + void End() + { + texturePipeline.Shutdown(); + + if (client.Network.Connected) + { + if (Program.Verbosity > 0) + Logger.Log("Logging out", Helpers.LogLevel.Info); + + client.Network.Logout(); + } + + running = false; + } + + void MonitorPrimsAwaitingSelect() + { + while (running) + { + try + { + Primitive prim = primsAwaitingSelect.Dequeue(250); + + if (!prims.ContainsKey(prim.LocalID) && prim != null) + { + client.Objects.SelectObject(client.Network.CurrentSim, prim.LocalID); + Thread.Sleep(20); // Hacky rate limiting + } + } + catch (InvalidOperationException) + { + } + } + } + + void Network_OnCurrentSimChanged(Simulator PreviousSimulator) + { + if (Program.Verbosity > 0) + Logger.Log("Moved into simulator " + client.Network.CurrentSim.ToString(), Helpers.LogLevel.Info); + } + + void Parcels_OnSimParcelsDownloaded(Simulator simulator, InternalDictionary simParcels, int[,] parcelMap) + { + lock (totalPrimsLock) + { + totalPrims = 0; + simParcels.ForEach( + delegate(Parcel parcel) { totalPrims += parcel.TotalPrims; }); + + if (Program.Verbosity > 0) + Logger.Log(String.Format("Counted {0} total prims in this simulator", totalPrims), Helpers.LogLevel.Info); + } + } + + void Objects_OnNewPrim(Simulator simulator, Primitive prim, ulong regionHandle, ushort timeDilation) + { + prims.Add(prim.LocalID, prim.ID, prim); + primsAwaitingSelect.Enqueue(prim); + UpdateTextureQueue(prim.Textures); + } + + void UpdateTextureQueue(Primitive.TextureEntry te) + { + if (te != null) + { + for (int i = 0; i < te.FaceTextures.Length; i++) + { + if (te.FaceTextures[i] != null && !texturesFinished.ContainsKey(te.FaceTextures[i].TextureID)) + texturePipeline.RequestTexture(te.FaceTextures[i].TextureID); + } + } + } + + void Objects_OnObjectUpdated(Simulator simulator, ObjectUpdate update, ulong regionHandle, ushort timeDilation) + { + if (!update.Avatar) + { + Primitive prim; + + if (prims.TryGetValue(update.LocalID, out prim)) + { + lock (prim) + { + if (Program.Verbosity > 1) + Logger.Log("Updating state for " + prim.ID.ToString(), Helpers.LogLevel.Info); + + prim.Acceleration = update.Acceleration; + prim.AngularVelocity = update.AngularVelocity; + prim.CollisionPlane = update.CollisionPlane; + prim.Position = update.Position; + prim.Rotation = update.Rotation; + prim.PrimData.State = update.State; + prim.Textures = update.Textures; + prim.Velocity = update.Velocity; + } + + UpdateTextureQueue(prim.Textures); + } + } + } + + void Objects_OnObjectProperties(Simulator simulator, Primitive.ObjectProperties props) + { + Primitive prim; + + if (prims.TryGetValue(props.ObjectID, out prim)) + { + if (Program.Verbosity > 2) + Logger.Log("Received properties for " + props.ObjectID.ToString(), Helpers.LogLevel.Info); + + lock (prim) + prim.Properties = props; + } + else + { + Logger.Log("Received object properties for untracked object " + props.ObjectID.ToString(), + Helpers.LogLevel.Warning); + } + } + + void Objects_OnObjectKilled(Simulator simulator, uint objectID) + { + ; + } + } + + public class Program + { + public static int Verbosity = 0; + + static void Main(string[] args) + { + string loginServer = Settings.AGNI_LOGIN_SERVER; + string filename = "simexport.tgz"; + string regionName = null, firstName = null, lastName = null, password = null; + bool showhelp = false; + + NDesk.Options.OptionSet argParser = new NDesk.Options.OptionSet() + .Add("s|login-server=", "URL of the login server (default is '" + loginServer + "')", delegate(string v) { loginServer = v; }) + .Add("r|region-name=", "name of the region to export", delegate(string v) { regionName = v; }) + .Add("f|firstname=", "first name of the bot to log in", delegate(string v) { firstName = v; }) + .Add("l|lastname=", "last name of the bot to log in", delegate(string v) { lastName = v; }) + .Add("p|password=", "password of the bot to log in", delegate(string v) { password = v; }) + .Add("o|output=", "filename of the OAR to write (default is 'simexport.tgz')", delegate(string v) { filename = v; }) + .Add("h|?|help", delegate(string v) { showhelp = (v != null); }) + .Add("v|verbose", delegate(string v) { if (v != null) ++Verbosity; }); + argParser.Parse(args); + + if (!showhelp && !String.IsNullOrEmpty(regionName) && + !String.IsNullOrEmpty(firstName) && !String.IsNullOrEmpty(lastName) && !String.IsNullOrEmpty(password)) + { + SimExport exporter = new SimExport(firstName, lastName, password, loginServer, regionName, filename); + } + else + { + Console.WriteLine("Usage: SimExport.exe [OPTION]..."); + Console.WriteLine("An interactive client for exporting assets"); + Console.WriteLine("Options:"); + argParser.WriteOptionDescriptions(Console.Out); + } + } + } +} diff --git a/Programs/SimExport/TarArchiveWriter.cs b/Programs/SimExport/TarArchiveWriter.cs new file mode 100644 index 00000000..fae31b02 --- /dev/null +++ b/Programs/SimExport/TarArchiveWriter.cs @@ -0,0 +1,195 @@ +?/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * 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. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSim Project 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 DEVELOPERS ``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 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.Text; + +namespace SimExport +{ + /// + /// Produces a tar archive in tar v7 format + /// + public class TarArchiveWriter + { + protected Dictionary m_files = new Dictionary(); + protected static ASCIIEncoding m_asciiEncoding = new ASCIIEncoding(); + + /// + /// Add a directory to the tar archive. We can only handle one path level right now! + /// + /// + public void AddDir(string dirName) + { + // Directories are signalled by a final / + if (!dirName.EndsWith("/")) + dirName += "/"; + + AddFile(dirName, new byte[0]); + } + + /// + /// Add a file to the tar archive + /// + /// + /// + public void AddFile(string filePath, string data) + { + AddFile(filePath, m_asciiEncoding.GetBytes(data)); + } + + /// + /// Add a file to the tar archive + /// + /// + /// + public void AddFile(string filePath, byte[] data) + { + m_files[filePath] = data; + } + + /// + /// Write the raw tar archive data to a stream. The stream will be closed on completion. + /// + /// Stream to which to write the data + /// + public void WriteTar(Stream s) + { + BinaryWriter bw = new BinaryWriter(s); + + foreach (string filePath in m_files.Keys) + { + byte[] header = new byte[512]; + byte[] data = m_files[filePath]; + + // file path field (100) + byte[] nameBytes = m_asciiEncoding.GetBytes(filePath); + int nameSize = (nameBytes.Length >= 100) ? 100 : nameBytes.Length; + Array.Copy(nameBytes, header, nameSize); + + // file mode (8) + byte[] modeBytes = m_asciiEncoding.GetBytes("0000777"); + Array.Copy(modeBytes, 0, header, 100, 7); + + // owner user id (8) + byte[] ownerIdBytes = m_asciiEncoding.GetBytes("0000764"); + Array.Copy(ownerIdBytes, 0, header, 108, 7); + + // group user id (8) + byte[] groupIdBytes = m_asciiEncoding.GetBytes("0000764"); + Array.Copy(groupIdBytes, 0, header, 116, 7); + + // file size in bytes (12) + int fileSize = data.Length; + //m_log.DebugFormat("[TAR ARCHIVE WRITER]: File size of {0} is {1}", filePath, fileSize); + + byte[] fileSizeBytes = ConvertDecimalToPaddedOctalBytes(fileSize, 11); + + Array.Copy(fileSizeBytes, 0, header, 124, 11); + + // last modification time (12) + byte[] lastModTimeBytes = m_asciiEncoding.GetBytes("11017037332"); + Array.Copy(lastModTimeBytes, 0, header, 136, 11); + + // link indicator (1) + //header[156] = m_asciiEncoding.GetBytes("0")[0]; + if (filePath.EndsWith("/")) + { + header[156] = m_asciiEncoding.GetBytes("5")[0]; + } + else + { + header[156] = 0; + } + + Array.Copy(m_asciiEncoding.GetBytes("0000000"), 0, header, 329, 7); + Array.Copy(m_asciiEncoding.GetBytes("0000000"), 0, header, 337, 7); + + // check sum for header block (8) [calculated last] + Array.Copy(m_asciiEncoding.GetBytes(" "), 0, header, 148, 8); + + int checksum = 0; + foreach (byte b in header) + { + checksum += b; + } + + //m_log.DebugFormat("[TAR ARCHIVE WRITER]: Decimal header checksum is {0}", checksum); + + byte[] checkSumBytes = ConvertDecimalToPaddedOctalBytes(checksum, 6); + + Array.Copy(checkSumBytes, 0, header, 148, 6); + + header[154] = 0; + + // Write out header + bw.Write(header); + + // Write out data + bw.Write(data); + + if (data.Length % 512 != 0) + { + int paddingRequired = 512 - (data.Length % 512); + + //m_log.DebugFormat("[TAR ARCHIVE WRITER]: Padding data with {0} bytes", paddingRequired); + + byte[] padding = new byte[paddingRequired]; + bw.Write(padding); + } + } + + // Write two consecutive 0 blocks to end the archive + byte[] finalZeroPadding = new byte[1024]; + bw.Write(finalZeroPadding); + + bw.Flush(); + bw.Close(); + } + + public static byte[] ConvertDecimalToPaddedOctalBytes(int d, int padding) + { + string oString = ""; + + while (d > 0) + { + oString = Convert.ToString((byte)'0' + d & 7) + oString; + d >>= 3; + } + + while (oString.Length < padding) + { + oString = "0" + oString; + } + + byte[] oBytes = m_asciiEncoding.GetBytes(oString); + + return oBytes; + } + } +} diff --git a/Programs/SimExport/TexturePipeline.cs b/Programs/SimExport/TexturePipeline.cs new file mode 100644 index 00000000..df644785 --- /dev/null +++ b/Programs/SimExport/TexturePipeline.cs @@ -0,0 +1,346 @@ +?using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using OpenMetaverse; + +/* + * the onnewprim function will add missing texture uuids to the download queue, + * and a separate thread will pull entries off that queue. + * if they exist in the cache it will add that texture to a dictionary that the rendering loop accesses, + * otherwise it will start the download. + * the ondownloaded function will put the new texture in the same dictionary + * + * + * Easy Start: + * subscribe to OnImageRenderReady event + * send request with RequestTexture() + * + * when OnImageRenderReady fires: + * request image data with GetTextureToRender() using key returned in OnImageRenderReady event + * (optionally) use RemoveFromPipeline() with key to cleanup dictionary + */ + +namespace SimExport +{ + class TaskInfo + { + public UUID RequestID; + public int RequestNbr; + + + public TaskInfo(UUID reqID, int reqNbr) + { + RequestID = reqID; + RequestNbr = reqNbr; + } + } + + /// + /// Texture request download handler, allows a configurable number of download slots + /// + public class TexturePipeline + { + private static GridClient Client; + + // queue for requested images + private Queue RequestQueue; + + // list of current requests in process + private Dictionary CurrentRequests; + + private static AutoResetEvent[] resetEvents; + + private static int[] threadpoolSlots; + + /// + /// For keeping track of active threads available/downloading textures + /// + public static int[] ThreadpoolSlots + { + get { lock (threadpoolSlots) { return threadpoolSlots; } } + set { lock (threadpoolSlots) { threadpoolSlots = value; } } + } + + public int QueuedCount { get { return RequestQueue.Count; } } + public int CurrentCount { get { return CurrentRequests.Count; } } + + // storage for images ready to render + private Dictionary RenderReady; + + // maximum allowed concurrent requests at once + const int MAX_TEXTURE_REQUESTS = 10; + + /// + /// + /// + /// + /// + public delegate void DownloadFinishedCallback(UUID id, bool success); + /// + /// + /// + /// + /// + /// + public delegate void DownloadProgressCallback(UUID image, int recieved, int total); + + /// Fired when a texture download completes + public event DownloadFinishedCallback OnDownloadFinished; + /// + public event DownloadProgressCallback OnDownloadProgress; + + private Thread downloadMaster; + private bool Running; + + private AssetManager.ImageReceivedCallback DownloadCallback; + private AssetManager.ImageReceiveProgressCallback DownloadProgCallback; + + /// + /// Default constructor + /// + /// Reference to SecondLife client + public TexturePipeline(GridClient client) + { + Running = true; + + RequestQueue = new Queue(); + CurrentRequests = new Dictionary(MAX_TEXTURE_REQUESTS); + + RenderReady = new Dictionary(); + + resetEvents = new AutoResetEvent[MAX_TEXTURE_REQUESTS]; + threadpoolSlots = new int[MAX_TEXTURE_REQUESTS]; + + // pre-configure autoreset events/download slots + for (int i = 0; i < MAX_TEXTURE_REQUESTS; i++) + { + resetEvents[i] = new AutoResetEvent(false); + threadpoolSlots[i] = -1; + } + + Client = client; + + DownloadCallback = new AssetManager.ImageReceivedCallback(Assets_OnImageReceived); + DownloadProgCallback = new AssetManager.ImageReceiveProgressCallback(Assets_OnImageReceiveProgress); + Client.Assets.OnImageReceived += DownloadCallback; + Client.Assets.OnImageReceiveProgress += DownloadProgCallback; + + // Fire up the texture download thread + downloadMaster = new Thread(new ThreadStart(DownloadThread)); + downloadMaster.Start(); + } + + public void Shutdown() + { + Client.Assets.OnImageReceived -= DownloadCallback; + Client.Assets.OnImageReceiveProgress -= DownloadProgCallback; + + RequestQueue.Clear(); + + for (int i = 0; i < resetEvents.Length; i++) + if (resetEvents[i] != null) + resetEvents[i].Set(); + + Running = false; + } + + /// + /// Request a texture be downloaded, once downloaded OnImageRenderReady event will be fired + /// containing texture key which can be used to retrieve texture with GetTextureToRender method + /// + /// id of Texture to request + public void RequestTexture(UUID textureID) + { + if (Client.Assets.Cache.HasImage(textureID)) + { + // Add to rendering dictionary + lock (RenderReady) + { + if (!RenderReady.ContainsKey(textureID)) + { + RenderReady.Add(textureID, Client.Assets.Cache.GetCachedImage(textureID)); + + // Let any subscribers know about it + if (OnDownloadFinished != null) + { + OnDownloadFinished(textureID, true); + } + } + else + { + // This image has already been served up, ignore this request + } + } + } + else + { + lock (RequestQueue) + { + // Make sure we aren't already downloading the texture + if (!RequestQueue.Contains(textureID) && !CurrentRequests.ContainsKey(textureID)) + { + RequestQueue.Enqueue(textureID); + } + } + } + } + + /// + /// retrieve texture information from dictionary + /// + /// Texture ID + /// ImageDownload object + public ImageDownload GetTextureToRender(UUID textureID) + { + ImageDownload renderable = new ImageDownload(); + lock (RenderReady) + { + if (RenderReady.ContainsKey(textureID)) + { + renderable = RenderReady[textureID]; + } + else + { + Logger.Log("Requested texture data for texture that does not exist in dictionary", Helpers.LogLevel.Warning); + } + return renderable; + } + } + + /// + /// Remove no longer necessary texture from dictionary + /// + /// + public void RemoveFromPipeline(UUID textureID) + { + lock (RenderReady) + { + if (RenderReady.ContainsKey(textureID)) + RenderReady.Remove(textureID); + } + } + + /// + /// Master Download Thread, Queues up downloads in the threadpool + /// + private void DownloadThread() + { + int reqNbr; + + while (Running) + { + if (RequestQueue.Count > 0) + { + reqNbr = -1; + // find available slot for reset event + for (int i = 0; i < threadpoolSlots.Length; i++) + { + if (threadpoolSlots[i] == -1) + { + threadpoolSlots[i] = 1; + reqNbr = i; + break; + } + } + + if (reqNbr != -1) + { + UUID requestID; + lock (RequestQueue) + requestID = RequestQueue.Dequeue(); + + Logger.DebugLog(String.Format("Sending Worker thread new download request {0}", reqNbr)); + ThreadPool.QueueUserWorkItem(new WaitCallback(textureRequestDoWork), new TaskInfo(requestID, reqNbr)); + + continue; + } + } + + // Queue was empty, let's give up some CPU time + Thread.Sleep(500); + } + } + + void textureRequestDoWork(Object threadContext) + { + TaskInfo ti = (TaskInfo)threadContext; + + lock (CurrentRequests) + { + if (CurrentRequests.ContainsKey(ti.RequestID)) + { + threadpoolSlots[ti.RequestNbr] = -1; + return; + } + else + { + CurrentRequests.Add(ti.RequestID, ti.RequestNbr); + } + } + + Logger.DebugLog(String.Format("Worker {0} Requesting {1}", ti.RequestNbr, ti.RequestID)); + + resetEvents[ti.RequestNbr].Reset(); + Client.Assets.RequestImage(ti.RequestID, ImageType.Normal); + + // don't release this worker slot until texture is downloaded or timeout occurs + if (!resetEvents[ti.RequestNbr].WaitOne(30 * 1000, false)) + { + // Timed out + Logger.Log("Worker " + ti.RequestNbr + " Timeout waiting for Texture " + ti.RequestID + " to Download", Helpers.LogLevel.Warning); + + lock (CurrentRequests) + CurrentRequests.Remove(ti.RequestID); + } + + // free up this download slot + threadpoolSlots[ti.RequestNbr] = -1; + } + + private void Assets_OnImageReceived(ImageDownload image, AssetTexture asset) + { + // Free up this slot in the ThreadPool + lock (CurrentRequests) + { + int requestNbr; + if (asset != null && CurrentRequests.TryGetValue(image.ID, out requestNbr)) + { + Logger.DebugLog(String.Format("Worker {0} Downloaded texture {1}", requestNbr, image.ID)); + resetEvents[requestNbr].Set(); + CurrentRequests.Remove(image.ID); + } + } + + if (image.Success) + { + lock (RenderReady) + { + if (!RenderReady.ContainsKey(image.ID)) + { + // Add to rendering dictionary + RenderReady.Add(image.ID, image); + } + } + } + else + { + Console.WriteLine(String.Format("Download of texture {0} failed. NotFound={1}", image.ID, image.NotFound)); + } + + // Let any subscribers know about it + if (OnDownloadFinished != null) + { + OnDownloadFinished(image.ID, image.Success); + } + } + + private void Assets_OnImageReceiveProgress(UUID image, int lastPacket, int recieved, int total) + { + if (OnDownloadProgress != null) + { + OnDownloadProgress(image, recieved, total); + } + } + } +} diff --git a/prebuild.xml b/prebuild.xml index 0163089b..4127f25d 100644 --- a/prebuild.xml +++ b/prebuild.xml @@ -578,6 +578,29 @@ + + + + + ../../bin/ + + + + + ../../bin/ + + + + ../../bin/ + + + + + + + + +