From e8273fa8ad85323f18fb67ecf6d5f07eced87178 Mon Sep 17 00:00:00 2001 From: Oren Hurvitz Date: Tue, 26 Nov 2013 10:37:32 +0200 Subject: [PATCH 01/15] - Materials: support the viewer removing the material (in which case matsMap["Material"] is missing) - Reduced logging --- .../Materials/MaterialsDemoModule.cs | 75 ++++++++++--------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/OpenSim/Region/OptionalModules/Materials/MaterialsDemoModule.cs b/OpenSim/Region/OptionalModules/Materials/MaterialsDemoModule.cs index d8f5563624..44b1a4aab3 100644 --- a/OpenSim/Region/OptionalModules/Materials/MaterialsDemoModule.cs +++ b/OpenSim/Region/OptionalModules/Materials/MaterialsDemoModule.cs @@ -104,7 +104,7 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule if (!m_enabled) return; - m_log.DebugFormat("[MaterialsDemoModule]: INITIALIZED MODULE"); + m_log.DebugFormat("[MaterialsDemoModule]: Initialized"); } public void Close() @@ -112,7 +112,7 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule if (!m_enabled) return; - m_log.DebugFormat("[MaterialsDemoModule]: CLOSED MODULE"); + //m_log.DebugFormat("[MaterialsDemoModule]: CLOSED MODULE"); } public void AddRegion(Scene scene) @@ -120,7 +120,7 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule if (!m_enabled) return; - m_log.DebugFormat("[MaterialsDemoModule]: REGION {0} ADDED", scene.RegionInfo.RegionName); + //m_log.DebugFormat("[MaterialsDemoModule]: REGION {0} ADDED", scene.RegionInfo.RegionName); m_scene = scene; m_scene.EventManager.OnRegisterCaps += OnRegisterCaps; @@ -166,7 +166,7 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule m_scene.EventManager.OnObjectAddedToScene -= EventManager_OnObjectAddedToScene; // m_scene.EventManager.OnGatherUuids -= GatherMaterialsUuids; - m_log.DebugFormat("[MaterialsDemoModule]: REGION {0} REMOVED", scene.RegionInfo.RegionName); + //m_log.DebugFormat("[MaterialsDemoModule]: REGION {0} REMOVED", scene.RegionInfo.RegionName); } public void RegionLoaded(Scene scene) @@ -195,7 +195,8 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule if (part.DynAttrs == null) { - m_log.Warn("[MaterialsDemoModule]: NULL DYNATTRS :( "); + //m_log.Warn("[MaterialsDemoModule]: NULL DYNATTRS :( "); + return; } lock (part.DynAttrs) @@ -216,11 +217,11 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule return; } - m_log.Info("[MaterialsDemoModule]: OSMaterials: " + OSDParser.SerializeJsonString(OSMaterials)); + //m_log.Info("[MaterialsDemoModule]: OSMaterials: " + OSDParser.SerializeJsonString(OSMaterials)); if (matsArr == null) { - m_log.Info("[MaterialsDemoModule]: matsArr is null :( "); + //m_log.Info("[MaterialsDemoModule]: matsArr is null :( "); return; } @@ -238,7 +239,7 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule } catch (Exception e) { - m_log.Warn("[MaterialsDemoModule]: exception decoding persisted material: " + e.ToString()); + m_log.Warn("[MaterialsDemoModule]: exception decoding persisted material ", e); } } } @@ -299,7 +300,7 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule } catch (Exception e) { - m_log.Warn("[MaterialsDemoModule]: exception in StoreMaterialsForPart(): " + e.ToString()); + m_log.Warn("[MaterialsDemoModule]: exception in StoreMaterialsForPart() ", e); } } @@ -307,7 +308,7 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule string param, IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) { - m_log.Debug("[MaterialsDemoModule]: POST cap handler"); + //m_log.Debug("[MaterialsDemoModule]: POST cap handler"); OSDMap req = (OSDMap)OSDParser.DeserializeLLSDXml(request); OSDMap resp = new OSDMap(); @@ -341,7 +342,7 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule { if (m_knownMaterials.ContainsKey(id)) { - m_log.Info("[MaterialsDemoModule]: request for known material ID: " + id.ToString()); + //m_log.Info("[MaterialsDemoModule]: request for known material ID: " + id.ToString()); OSDMap matMap = new OSDMap(); matMap["ID"] = OSD.FromBinary(id.GetBytes()); @@ -374,34 +375,40 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule { foreach (OSDMap matsMap in matsArr) { - m_log.Debug("[MaterialsDemoModule]: processing matsMap: " + OSDParser.SerializeJsonString(matsMap)); + //m_log.Debug("[MaterialsDemoModule]: processing matsMap: " + OSDParser.SerializeJsonString(matsMap)); - uint matLocalID = 0; - try { matLocalID = matsMap["ID"].AsUInteger(); } + uint primLocalID = 0; + try { primLocalID = matsMap["ID"].AsUInteger(); } catch (Exception e) { m_log.Warn("[MaterialsDemoModule]: cannot decode \"ID\" from matsMap: " + e.Message); } - m_log.Debug("[MaterialsDemoModule]: matLocalId: " + matLocalID.ToString()); - + //m_log.Debug("[MaterialsDemoModule]: primLocalID: " + primLocalID.ToString()); OSDMap mat = null; try { mat = matsMap["Material"] as OSDMap; } catch (Exception e) { m_log.Warn("[MaterialsDemoModule]: cannot decode \"Material\" from matsMap: " + e.Message); } - m_log.Debug("[MaterialsDemoModule]: mat: " + OSDParser.SerializeJsonString(mat)); - - UUID id = HashOsd(mat); - lock (m_knownMaterials) - m_knownMaterials[id] = mat; - + //m_log.Debug("[MaterialsDemoModule]: mat: " + OSDParser.SerializeJsonString(mat)); - var sop = m_scene.GetSceneObjectPart(matLocalID); + UUID id; + if (mat == null) + { + id = UUID.Zero; + } + else + { + id = HashOsd(mat); + lock (m_knownMaterials) + m_knownMaterials[id] = mat; + } + + var sop = m_scene.GetSceneObjectPart(primLocalID); if (sop == null) - m_log.Debug("[MaterialsDemoModule]: null SOP for localId: " + matLocalID.ToString()); + m_log.Debug("[MaterialsDemoModule]: null SOP for localId: " + primLocalID.ToString()); else { var te = new Primitive.TextureEntry(sop.Shape.TextureEntry, 0, sop.Shape.TextureEntry.Length); if (te == null) { - m_log.Debug("[MaterialsDemoModule]: null TextureEntry for localId: " + matLocalID.ToString()); + m_log.Debug("[MaterialsDemoModule]: null TextureEntry for localId: " + primLocalID.ToString()); } else { @@ -434,7 +441,7 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule te.DefaultTexture.MaterialID = id; } - m_log.Debug("[MaterialsDemoModule]: setting material ID for face " + face.ToString() + " to " + id.ToString()); + //m_log.DebugFormat("[MaterialsDemoModule]: in \"{0}\", setting material ID for face {1} to {2}", sop.Name, face, id); //we cant use sop.UpdateTextureEntry(te); because it filters so do it manually @@ -455,7 +462,7 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule } catch (Exception e) { - m_log.Warn("[MaterialsDemoModule]: exception processing received material: " + e.Message); + m_log.Warn("[MaterialsDemoModule]: exception processing received material ", e); } } } @@ -465,10 +472,10 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule } catch (Exception e) { - m_log.Warn("[MaterialsDemoModule]: exception decoding zipped CAP payload: " + e.Message); + m_log.Warn("[MaterialsDemoModule]: exception decoding zipped CAP payload ", e); //return ""; } - m_log.Debug("[MaterialsDemoModule]: knownMaterials.Count: " + m_knownMaterials.Count.ToString()); + //m_log.Debug("[MaterialsDemoModule]: knownMaterials.Count: " + m_knownMaterials.Count.ToString()); } @@ -476,8 +483,8 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule string response = OSDParser.SerializeLLSDXmlString(resp); //m_log.Debug("[MaterialsDemoModule]: cap request: " + request); - m_log.Debug("[MaterialsDemoModule]: cap request (zipped portion): " + ZippedOsdBytesToString(req["Zipped"].AsBinary())); - m_log.Debug("[MaterialsDemoModule]: cap response: " + response); + //m_log.Debug("[MaterialsDemoModule]: cap request (zipped portion): " + ZippedOsdBytesToString(req["Zipped"].AsBinary())); + //m_log.Debug("[MaterialsDemoModule]: cap response: " + response); return response; } @@ -486,7 +493,7 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule string param, IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) { - m_log.Debug("[MaterialsDemoModule]: GET cap handler"); + //m_log.Debug("[MaterialsDemoModule]: GET cap handler"); OSDMap resp = new OSDMap(); int matsCount = 0; @@ -506,7 +513,7 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule } resp["Zipped"] = ZCompressOSD(allOsd, false); - m_log.Debug("[MaterialsDemoModule]: matsCount: " + matsCount.ToString()); + //m_log.Debug("[MaterialsDemoModule]: matsCount: " + matsCount.ToString()); return OSDParser.SerializeLLSDXmlString(resp); } @@ -654,4 +661,4 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule // } // } } -} \ No newline at end of file +} From ca0336d8349382ddb46df4c7e7f6377c64151f25 Mon Sep 17 00:00:00 2001 From: Oren Hurvitz Date: Thu, 5 Dec 2013 14:18:59 +0200 Subject: [PATCH 02/15] Renamed MaterialsDemoModule to MaterialsModule --- .../Materials/{MaterialsDemoModule.cs => MaterialsModule.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename OpenSim/Region/OptionalModules/Materials/{MaterialsDemoModule.cs => MaterialsModule.cs} (99%) diff --git a/OpenSim/Region/OptionalModules/Materials/MaterialsDemoModule.cs b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs similarity index 99% rename from OpenSim/Region/OptionalModules/Materials/MaterialsDemoModule.cs rename to OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs index 44b1a4aab3..e70715485e 100644 --- a/OpenSim/Region/OptionalModules/Materials/MaterialsDemoModule.cs +++ b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs @@ -88,7 +88,7 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule public class MaterialsDemoModule : INonSharedRegionModule { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); - + public string Name { get { return "MaterialsDemoModule"; } } public Type ReplaceableInterface { get { return null; } } From 3018b2c5d7c9de0e8da6d158f0848c840b7864ab Mon Sep 17 00:00:00 2001 From: Oren Hurvitz Date: Fri, 6 Dec 2013 16:21:11 +0200 Subject: [PATCH 03/15] Materials module: a) Store materials as assets; b) Finalized it (removed the "Demo" label; removed most of the logging); c) Enabled by default Changed UuidGatherer to use 'sbyte' to identify assets instead of 'AssetType'. This lets UuidGatherer handle Materials, which are defined in a different enum from 'AssetType'. --- OpenSim/Framework/SLUtil.cs | 40 +- .../Serialization/ArchiveConstants.cs | 3 + .../CoreModules/Asset/FlotsamAssetCache.cs | 4 +- .../Archiver/InventoryArchiveWriteRequest.cs | 4 +- .../EntityTransfer/HGEntityTransferModule.cs | 4 +- .../InventoryAccess/HGAssetMapper.cs | 8 +- .../World/Archiver/ArchiveWriteRequest.cs | 12 +- .../World/Archiver/AssetsRequest.cs | 11 +- .../Scenes/Tests/UuidGathererTests.cs | 12 +- .../Region/Framework/Scenes/UuidGatherer.cs | 138 ++--- .../Materials/MaterialsModule.cs | 528 +++++++----------- bin/OpenSim.ini.example | 6 + 12 files changed, 312 insertions(+), 458 deletions(-) diff --git a/OpenSim/Framework/SLUtil.cs b/OpenSim/Framework/SLUtil.cs index cb73e8f762..92491050b3 100644 --- a/OpenSim/Framework/SLUtil.cs +++ b/OpenSim/Framework/SLUtil.cs @@ -39,8 +39,32 @@ namespace OpenSim.Framework { // private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + /// + /// Asset types used only in OpenSim. + /// To avoid clashing with the code numbers used in Second Life, use only negative numbers here. + /// + public enum OpenSimAssetType : sbyte + { + Material = -2 + } + + #region SL / file extension / content-type conversions + /// + /// Returns the Enum entry corresponding to the given code, regardless of whether it belongs + /// to the AssetType or OpenSimAssetType enums. + /// + public static object AssetTypeFromCode(sbyte assetType) + { + if (Enum.IsDefined(typeof(OpenMetaverse.AssetType), assetType)) + return (OpenMetaverse.AssetType)assetType; + else if (Enum.IsDefined(typeof(OpenSimAssetType), assetType)) + return (OpenSimAssetType)assetType; + else + return OpenMetaverse.AssetType.Unknown; + } + private class TypeMapping { private sbyte assetType; @@ -56,12 +80,7 @@ namespace OpenSim.Framework public object AssetType { - get { - if (Enum.IsDefined(typeof(OpenMetaverse.AssetType), assetType)) - return (OpenMetaverse.AssetType)assetType; - else - return OpenMetaverse.AssetType.Unknown; - } + get { return AssetTypeFromCode(assetType); } } public InventoryType InventoryType @@ -102,6 +121,11 @@ namespace OpenSim.Framework : this((sbyte)assetType, inventoryType, contentType, null, extension) { } + + public TypeMapping(OpenSimAssetType assetType, InventoryType inventoryType, string contentType, string extension) + : this((sbyte)assetType, inventoryType, contentType, null, extension) + { + } } /// @@ -142,7 +166,9 @@ namespace OpenSim.Framework new TypeMapping(AssetType.CurrentOutfitFolder, InventoryType.Unknown, "application/vnd.ll.currentoutfitfolder", "currentoutfitfolder"), new TypeMapping(AssetType.OutfitFolder, InventoryType.Unknown, "application/vnd.ll.outfitfolder", "outfitfolder"), new TypeMapping(AssetType.MyOutfitsFolder, InventoryType.Unknown, "application/vnd.ll.myoutfitsfolder", "myoutfitsfolder"), - new TypeMapping(AssetType.Mesh, InventoryType.Mesh, "application/vnd.ll.mesh", "llm") + new TypeMapping(AssetType.Mesh, InventoryType.Mesh, "application/vnd.ll.mesh", "llm"), + + new TypeMapping(OpenSimAssetType.Material, InventoryType.Unknown, "application/llsd+xml", "material") }; private static Dictionary asset2Content; diff --git a/OpenSim/Framework/Serialization/ArchiveConstants.cs b/OpenSim/Framework/Serialization/ArchiveConstants.cs index 0c12787882..73ebfae234 100644 --- a/OpenSim/Framework/Serialization/ArchiveConstants.cs +++ b/OpenSim/Framework/Serialization/ArchiveConstants.cs @@ -29,6 +29,7 @@ using System; using System.Collections.Generic; using System.Text; using OpenMetaverse; +using OpenSimAssetType = OpenSim.Framework.SLUtil.OpenSimAssetType; namespace OpenSim.Framework.Serialization { @@ -128,6 +129,7 @@ namespace OpenSim.Framework.Serialization ASSET_TYPE_TO_EXTENSION[(sbyte)AssetType.Texture] = ASSET_EXTENSION_SEPARATOR + "texture.jp2"; ASSET_TYPE_TO_EXTENSION[(sbyte)AssetType.TextureTGA] = ASSET_EXTENSION_SEPARATOR + "texture.tga"; ASSET_TYPE_TO_EXTENSION[(sbyte)AssetType.TrashFolder] = ASSET_EXTENSION_SEPARATOR + "trashfolder.txt"; // Not sure if we'll ever see this + ASSET_TYPE_TO_EXTENSION[(sbyte)OpenSimAssetType.Material] = ASSET_EXTENSION_SEPARATOR + "material.xml"; // Not sure if we'll ever see this EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "animation.bvh"] = (sbyte)AssetType.Animation; EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "bodypart.txt"] = (sbyte)AssetType.Bodypart; @@ -152,6 +154,7 @@ namespace OpenSim.Framework.Serialization EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "texture.jp2"] = (sbyte)AssetType.Texture; EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "texture.tga"] = (sbyte)AssetType.TextureTGA; EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "trashfolder.txt"] = (sbyte)AssetType.TrashFolder; + EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "material.xml"] = (sbyte)OpenSimAssetType.Material; } public static string CreateOarLandDataPath(LandData ld) diff --git a/OpenSim/Region/CoreModules/Asset/FlotsamAssetCache.cs b/OpenSim/Region/CoreModules/Asset/FlotsamAssetCache.cs index 6a5f8f397e..b270de9c89 100644 --- a/OpenSim/Region/CoreModules/Asset/FlotsamAssetCache.cs +++ b/OpenSim/Region/CoreModules/Asset/FlotsamAssetCache.cs @@ -771,7 +771,7 @@ namespace OpenSim.Region.CoreModules.Asset UuidGatherer gatherer = new UuidGatherer(m_AssetService); HashSet uniqueUuids = new HashSet(); - Dictionary assets = new Dictionary(); + Dictionary assets = new Dictionary(); foreach (Scene s in m_Scenes) { @@ -794,7 +794,7 @@ namespace OpenSim.Region.CoreModules.Asset else if (storeUncached) { AssetBase cachedAsset = m_AssetService.Get(assetID.ToString()); - if (cachedAsset == null && assets[assetID] != AssetType.Unknown) + if (cachedAsset == null && assets[assetID] != (sbyte)AssetType.Unknown) m_log.DebugFormat( "[FLOTSAM ASSET CACHE]: Could not find asset {0}, type {1} referenced by object {2} at {3} in scene {4} when pre-caching all scene assets", assetID, assets[assetID], e.Name, e.AbsolutePosition, s.Name); diff --git a/OpenSim/Region/CoreModules/Avatar/Inventory/Archiver/InventoryArchiveWriteRequest.cs b/OpenSim/Region/CoreModules/Avatar/Inventory/Archiver/InventoryArchiveWriteRequest.cs index 4ec8ae7aa7..429271916f 100644 --- a/OpenSim/Region/CoreModules/Avatar/Inventory/Archiver/InventoryArchiveWriteRequest.cs +++ b/OpenSim/Region/CoreModules/Avatar/Inventory/Archiver/InventoryArchiveWriteRequest.cs @@ -78,7 +78,7 @@ namespace OpenSim.Region.CoreModules.Avatar.Inventory.Archiver /// /// Used to collect the uuids of the assets that we need to save into the archive /// - protected Dictionary m_assetUuids = new Dictionary(); + protected Dictionary m_assetUuids = new Dictionary(); /// /// Used to collect the uuids of the users that we need to save into the archive @@ -187,7 +187,7 @@ namespace OpenSim.Region.CoreModules.Avatar.Inventory.Archiver // Don't chase down link asset items as they actually point to their target item IDs rather than an asset if (SaveAssets && itemAssetType != AssetType.Link && itemAssetType != AssetType.LinkFolder) - m_assetGatherer.GatherAssetUuids(inventoryItem.AssetID, (AssetType)inventoryItem.AssetType, m_assetUuids); + m_assetGatherer.GatherAssetUuids(inventoryItem.AssetID, (sbyte)inventoryItem.AssetType, m_assetUuids); } /// diff --git a/OpenSim/Region/CoreModules/Framework/EntityTransfer/HGEntityTransferModule.cs b/OpenSim/Region/CoreModules/Framework/EntityTransfer/HGEntityTransferModule.cs index 04a0db6b47..09b19757ee 100644 --- a/OpenSim/Region/CoreModules/Framework/EntityTransfer/HGEntityTransferModule.cs +++ b/OpenSim/Region/CoreModules/Framework/EntityTransfer/HGEntityTransferModule.cs @@ -182,11 +182,11 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer { string url = aCircuit.ServiceURLs["AssetServerURI"].ToString(); m_log.DebugFormat("[HG ENTITY TRANSFER MODULE]: Incoming attachment {0} for HG user {1} with asset server {2}", so.Name, so.AttachedAvatar, url); - Dictionary ids = new Dictionary(); + Dictionary ids = new Dictionary(); HGUuidGatherer uuidGatherer = new HGUuidGatherer(Scene.AssetService, url); uuidGatherer.GatherAssetUuids(so, ids); - foreach (KeyValuePair kvp in ids) + foreach (KeyValuePair kvp in ids) uuidGatherer.FetchAsset(kvp.Key); } } diff --git a/OpenSim/Region/CoreModules/Framework/InventoryAccess/HGAssetMapper.cs b/OpenSim/Region/CoreModules/Framework/InventoryAccess/HGAssetMapper.cs index b7a4d1ac1c..d4fb1baf48 100644 --- a/OpenSim/Region/CoreModules/Framework/InventoryAccess/HGAssetMapper.cs +++ b/OpenSim/Region/CoreModules/Framework/InventoryAccess/HGAssetMapper.cs @@ -260,9 +260,9 @@ namespace OpenSim.Region.CoreModules.Framework.InventoryAccess // The act of gathering UUIDs downloads some assets from the remote server // but not all... - Dictionary ids = new Dictionary(); + Dictionary ids = new Dictionary(); HGUuidGatherer uuidGatherer = new HGUuidGatherer(m_scene.AssetService, userAssetURL); - uuidGatherer.GatherAssetUuids(assetID, (AssetType)meta.Type, ids); + uuidGatherer.GatherAssetUuids(assetID, meta.Type, ids); m_log.DebugFormat("[HG ASSET MAPPER]: Preparing to get {0} assets", ids.Count); bool success = true; foreach (UUID uuid in ids.Keys) @@ -286,9 +286,9 @@ namespace OpenSim.Region.CoreModules.Framework.InventoryAccess AssetBase asset = m_scene.AssetService.Get(assetID.ToString()); if (asset != null) { - Dictionary ids = new Dictionary(); + Dictionary ids = new Dictionary(); HGUuidGatherer uuidGatherer = new HGUuidGatherer(m_scene.AssetService, string.Empty); - uuidGatherer.GatherAssetUuids(asset.FullID, (AssetType)asset.Type, ids); + uuidGatherer.GatherAssetUuids(asset.FullID, asset.Type, ids); bool success = false; foreach (UUID uuid in ids.Keys) { diff --git a/OpenSim/Region/CoreModules/World/Archiver/ArchiveWriteRequest.cs b/OpenSim/Region/CoreModules/World/Archiver/ArchiveWriteRequest.cs index a990898570..7a844f49f4 100644 --- a/OpenSim/Region/CoreModules/World/Archiver/ArchiveWriteRequest.cs +++ b/OpenSim/Region/CoreModules/World/Archiver/ArchiveWriteRequest.cs @@ -178,7 +178,7 @@ namespace OpenSim.Region.CoreModules.World.Archiver // Archive the regions - Dictionary assetUuids = new Dictionary(); + Dictionary assetUuids = new Dictionary(); scenesGroup.ForEachScene(delegate(Scene scene) { @@ -216,7 +216,7 @@ namespace OpenSim.Region.CoreModules.World.Archiver } } - private void ArchiveOneRegion(Scene scene, string regionDir, Dictionary assetUuids) + private void ArchiveOneRegion(Scene scene, string regionDir, Dictionary assetUuids) { m_log.InfoFormat("[ARCHIVER]: Writing region {0}", scene.RegionInfo.RegionName); @@ -276,16 +276,16 @@ namespace OpenSim.Region.CoreModules.World.Archiver RegionSettings regionSettings = scene.RegionInfo.RegionSettings; if (regionSettings.TerrainTexture1 != RegionSettings.DEFAULT_TERRAIN_TEXTURE_1) - assetUuids[regionSettings.TerrainTexture1] = AssetType.Texture; + assetUuids[regionSettings.TerrainTexture1] = (sbyte)AssetType.Texture; if (regionSettings.TerrainTexture2 != RegionSettings.DEFAULT_TERRAIN_TEXTURE_2) - assetUuids[regionSettings.TerrainTexture2] = AssetType.Texture; + assetUuids[regionSettings.TerrainTexture2] = (sbyte)AssetType.Texture; if (regionSettings.TerrainTexture3 != RegionSettings.DEFAULT_TERRAIN_TEXTURE_3) - assetUuids[regionSettings.TerrainTexture3] = AssetType.Texture; + assetUuids[regionSettings.TerrainTexture3] = (sbyte)AssetType.Texture; if (regionSettings.TerrainTexture4 != RegionSettings.DEFAULT_TERRAIN_TEXTURE_4) - assetUuids[regionSettings.TerrainTexture4] = AssetType.Texture; + assetUuids[regionSettings.TerrainTexture4] = (sbyte)AssetType.Texture; Save(scene, sceneObjects, regionDir); } diff --git a/OpenSim/Region/CoreModules/World/Archiver/AssetsRequest.cs b/OpenSim/Region/CoreModules/World/Archiver/AssetsRequest.cs index 96000234ff..2d0da61145 100644 --- a/OpenSim/Region/CoreModules/World/Archiver/AssetsRequest.cs +++ b/OpenSim/Region/CoreModules/World/Archiver/AssetsRequest.cs @@ -81,7 +81,7 @@ namespace OpenSim.Region.CoreModules.World.Archiver /// /// uuids to request /// - protected IDictionary m_uuids; + protected IDictionary m_uuids; /// /// Callback used when all the assets requested have been received. @@ -115,7 +115,7 @@ namespace OpenSim.Region.CoreModules.World.Archiver protected Dictionary m_options; protected internal AssetsRequest( - AssetsArchiver assetsArchiver, IDictionary uuids, + AssetsArchiver assetsArchiver, IDictionary uuids, IAssetService assetService, IUserAccountService userService, UUID scope, Dictionary options, AssetsRequestCallback assetsRequestCallback) @@ -154,7 +154,7 @@ namespace OpenSim.Region.CoreModules.World.Archiver m_requestCallbackTimer.Enabled = true; - foreach (KeyValuePair kvp in m_uuids) + foreach (KeyValuePair kvp in m_uuids) { // m_log.DebugFormat("[ARCHIVER]: Requesting asset {0}", kvp.Key); @@ -235,9 +235,8 @@ namespace OpenSim.Region.CoreModules.World.Archiver // Check for broken asset types and fix them with the AssetType gleaned by UuidGatherer if (fetchedAsset != null && fetchedAsset.Type == (sbyte)AssetType.Unknown) { - AssetType type = (AssetType)assetType; - m_log.InfoFormat("[ARCHIVER]: Rewriting broken asset type for {0} to {1}", fetchedAsset.ID, type); - fetchedAsset.Type = (sbyte)type; + m_log.InfoFormat("[ARCHIVER]: Rewriting broken asset type for {0} to {1}", fetchedAsset.ID, SLUtil.AssetTypeFromCode((sbyte)assetType)); + fetchedAsset.Type = (sbyte)assetType; } AssetRequestCallback(fetchedAssetID, this, fetchedAsset); diff --git a/OpenSim/Region/Framework/Scenes/Tests/UuidGathererTests.cs b/OpenSim/Region/Framework/Scenes/Tests/UuidGathererTests.cs index dd27294372..1e59e3f764 100644 --- a/OpenSim/Region/Framework/Scenes/Tests/UuidGathererTests.cs +++ b/OpenSim/Region/Framework/Scenes/Tests/UuidGathererTests.cs @@ -62,8 +62,8 @@ namespace OpenSim.Region.Framework.Scenes.Tests = AssetHelpers.CreateAsset(corruptAssetUuid, AssetType.Notecard, "CORRUPT ASSET", UUID.Zero); m_assetService.Store(corruptAsset); - IDictionary foundAssetUuids = new Dictionary(); - m_uuidGatherer.GatherAssetUuids(corruptAssetUuid, AssetType.Object, foundAssetUuids); + IDictionary foundAssetUuids = new Dictionary(); + m_uuidGatherer.GatherAssetUuids(corruptAssetUuid, (sbyte)AssetType.Object, foundAssetUuids); // We count the uuid as gathered even if the asset itself is corrupt. Assert.That(foundAssetUuids.Count, Is.EqualTo(1)); @@ -78,9 +78,9 @@ namespace OpenSim.Region.Framework.Scenes.Tests TestHelpers.InMethod(); UUID missingAssetUuid = UUID.Parse("00000000-0000-0000-0000-000000000666"); - IDictionary foundAssetUuids = new Dictionary(); + IDictionary foundAssetUuids = new Dictionary(); - m_uuidGatherer.GatherAssetUuids(missingAssetUuid, AssetType.Object, foundAssetUuids); + m_uuidGatherer.GatherAssetUuids(missingAssetUuid, (sbyte)AssetType.Object, foundAssetUuids); // We count the uuid as gathered even if the asset itself is missing. Assert.That(foundAssetUuids.Count, Is.EqualTo(1)); @@ -103,8 +103,8 @@ namespace OpenSim.Region.Framework.Scenes.Tests AssetBase ncAsset = AssetHelpers.CreateNotecardAsset(ncAssetId, soAssetId.ToString()); m_assetService.Store(ncAsset); - IDictionary foundAssetUuids = new Dictionary(); - m_uuidGatherer.GatherAssetUuids(ncAssetId, AssetType.Notecard, foundAssetUuids); + IDictionary foundAssetUuids = new Dictionary(); + m_uuidGatherer.GatherAssetUuids(ncAssetId, (sbyte)AssetType.Notecard, foundAssetUuids); // We count the uuid as gathered even if the asset itself is corrupt. Assert.That(foundAssetUuids.Count, Is.EqualTo(2)); diff --git a/OpenSim/Region/Framework/Scenes/UuidGatherer.cs b/OpenSim/Region/Framework/Scenes/UuidGatherer.cs index 3e074b9cfc..42a19775ba 100644 --- a/OpenSim/Region/Framework/Scenes/UuidGatherer.cs +++ b/OpenSim/Region/Framework/Scenes/UuidGatherer.cs @@ -38,6 +38,7 @@ using OpenMetaverse.StructuredData; using OpenSim.Framework; using OpenSim.Region.Framework.Scenes.Serialization; using OpenSim.Services.Interfaces; +using OpenSimAssetType = OpenSim.Framework.SLUtil.OpenSimAssetType; namespace OpenSim.Region.Framework.Scenes { @@ -83,7 +84,7 @@ namespace OpenSim.Region.Framework.Scenes /// The uuid of the asset for which to gather referenced assets /// The type of the asset for the uuid given /// The assets gathered - public void GatherAssetUuids(UUID assetUuid, AssetType assetType, IDictionary assetUuids) + public void GatherAssetUuids(UUID assetUuid, sbyte assetType, IDictionary assetUuids) { // avoid infinite loops if (assetUuids.ContainsKey(assetUuid)) @@ -93,23 +94,27 @@ namespace OpenSim.Region.Framework.Scenes { assetUuids[assetUuid] = assetType; - if (AssetType.Bodypart == assetType || AssetType.Clothing == assetType) + if ((sbyte)AssetType.Bodypart == assetType || (sbyte)AssetType.Clothing == assetType) { GetWearableAssetUuids(assetUuid, assetUuids); } - else if (AssetType.Gesture == assetType) + else if ((sbyte)AssetType.Gesture == assetType) { GetGestureAssetUuids(assetUuid, assetUuids); } - else if (AssetType.Notecard == assetType) + else if ((sbyte)AssetType.Notecard == assetType) { GetTextEmbeddedAssetUuids(assetUuid, assetUuids); } - else if (AssetType.LSLText == assetType) + else if ((sbyte)AssetType.LSLText == assetType) { GetTextEmbeddedAssetUuids(assetUuid, assetUuids); } - else if (AssetType.Object == assetType) + else if ((sbyte)OpenSimAssetType.Material == assetType) + { + GetMaterialAssetUuids(assetUuid, assetUuids); + } + else if ((sbyte)AssetType.Object == assetType) { GetSceneObjectAssetUuids(assetUuid, assetUuids); } @@ -136,7 +141,7 @@ namespace OpenSim.Region.Framework.Scenes /// A dictionary which is populated with the asset UUIDs gathered and the type of that asset. /// For assets where the type is not clear (e.g. UUIDs extracted from LSL and notecards), the type is Unknown. /// - public void GatherAssetUuids(SceneObjectGroup sceneObject, IDictionary assetUuids) + public void GatherAssetUuids(SceneObjectGroup sceneObject, IDictionary assetUuids) { // m_log.DebugFormat( // "[ASSET GATHERER]: Getting assets for object {0}, {1}", sceneObject.Name, sceneObject.UUID); @@ -156,7 +161,7 @@ namespace OpenSim.Region.Framework.Scenes { // Get the prim's default texture. This will be used for faces which don't have their own texture if (textureEntry.DefaultTexture != null) - assetUuids[textureEntry.DefaultTexture.TextureID] = AssetType.Texture; + assetUuids[textureEntry.DefaultTexture.TextureID] = (sbyte)AssetType.Texture; if (textureEntry.FaceTextures != null) { @@ -164,20 +169,20 @@ namespace OpenSim.Region.Framework.Scenes foreach (Primitive.TextureEntryFace texture in textureEntry.FaceTextures) { if (texture != null) - assetUuids[texture.TextureID] = AssetType.Texture; + assetUuids[texture.TextureID] = (sbyte)AssetType.Texture; } } } // If the prim is a sculpt then preserve this information too if (part.Shape.SculptTexture != UUID.Zero) - assetUuids[part.Shape.SculptTexture] = AssetType.Texture; + assetUuids[part.Shape.SculptTexture] = (sbyte)AssetType.Texture; if (part.Shape.ProjectionTextureUUID != UUID.Zero) - assetUuids[part.Shape.ProjectionTextureUUID] = AssetType.Texture; + assetUuids[part.Shape.ProjectionTextureUUID] = (sbyte)AssetType.Texture; if (part.CollisionSound != UUID.Zero) - assetUuids[part.CollisionSound] = AssetType.Sound; + assetUuids[part.CollisionSound] = (sbyte)AssetType.Sound; if (part.ParticleSystem.Length > 0) { @@ -185,7 +190,7 @@ namespace OpenSim.Region.Framework.Scenes { Primitive.ParticleSystem ps = new Primitive.ParticleSystem(part.ParticleSystem, 0); if (ps.Texture != UUID.Zero) - assetUuids[ps.Texture] = AssetType.Texture; + assetUuids[ps.Texture] = (sbyte)AssetType.Texture; } catch (Exception e) { @@ -205,7 +210,7 @@ namespace OpenSim.Region.Framework.Scenes // tii.Name, tii.Type, part.Name, part.UUID); if (!assetUuids.ContainsKey(tii.AssetID)) - GatherAssetUuids(tii.AssetID, (AssetType)tii.Type, assetUuids); + GatherAssetUuids(tii.AssetID, (sbyte)tii.Type, assetUuids); } // FIXME: We need to make gathering modular but we cannot yet, since gatherers are not guaranteed @@ -213,8 +218,6 @@ namespace OpenSim.Region.Framework.Scenes // inventory transfer. There needs to be a way for a module to register a method without assuming a // Scene.EventManager is present. // part.ParentGroup.Scene.EventManager.TriggerGatherUuids(part, assetUuids); - - GatherMaterialsUuids(part, assetUuids); } catch (Exception e) { @@ -225,7 +228,7 @@ namespace OpenSim.Region.Framework.Scenes } } } - + // /// // /// The callback made when we request the asset for an object from the asset service. // /// @@ -238,73 +241,6 @@ namespace OpenSim.Region.Framework.Scenes // Monitor.Pulse(this); // } // } - - /// - /// Gather all of the texture asset UUIDs used to reference "Materials" such as normal and specular maps - /// - /// - /// - public void GatherMaterialsUuids(SceneObjectPart part, IDictionary assetUuids) - { - // scan thru the dynAttrs map of this part for any textures used as materials - OSD osdMaterials = null; - - lock (part.DynAttrs) - { - if (part.DynAttrs.ContainsStore("OpenSim", "Materials")) - { - OSDMap materialsStore = part.DynAttrs.GetStore("OpenSim", "Materials"); - - if (materialsStore == null) - return; - - materialsStore.TryGetValue("Materials", out osdMaterials); - } - - if (osdMaterials != null) - { - //m_log.Info("[UUID Gatherer]: found Materials: " + OSDParser.SerializeJsonString(osd)); - - if (osdMaterials is OSDArray) - { - OSDArray matsArr = osdMaterials as OSDArray; - foreach (OSDMap matMap in matsArr) - { - try - { - if (matMap.ContainsKey("Material")) - { - OSDMap mat = matMap["Material"] as OSDMap; - if (mat.ContainsKey("NormMap")) - { - UUID normalMapId = mat["NormMap"].AsUUID(); - if (normalMapId != UUID.Zero) - { - assetUuids[normalMapId] = AssetType.Texture; - //m_log.Info("[UUID Gatherer]: found normal map ID: " + normalMapId.ToString()); - } - } - if (mat.ContainsKey("SpecMap")) - { - UUID specularMapId = mat["SpecMap"].AsUUID(); - if (specularMapId != UUID.Zero) - { - assetUuids[specularMapId] = AssetType.Texture; - //m_log.Info("[UUID Gatherer]: found specular map ID: " + specularMapId.ToString()); - } - } - } - - } - catch (Exception e) - { - m_log.Warn("[UUID Gatherer]: exception getting materials: " + e.Message); - } - } - } - } - } - } /// /// Get an asset synchronously, potentially using an asynchronous callback. If the @@ -344,7 +280,7 @@ namespace OpenSim.Region.Framework.Scenes /// /// /// Dictionary in which to record the references - private void GetTextEmbeddedAssetUuids(UUID embeddingAssetId, IDictionary assetUuids) + private void GetTextEmbeddedAssetUuids(UUID embeddingAssetId, IDictionary assetUuids) { // m_log.DebugFormat("[ASSET GATHERER]: Getting assets for uuid references in asset {0}", embeddingAssetId); @@ -364,7 +300,7 @@ namespace OpenSim.Region.Framework.Scenes // Embedded asset references (if not false positives) could be for many types of asset, so we will // label these as unknown. - assetUuids[uuid] = AssetType.Unknown; + assetUuids[uuid] = (sbyte)AssetType.Unknown; } } } @@ -374,7 +310,7 @@ namespace OpenSim.Region.Framework.Scenes /// /// /// Dictionary in which to record the references - private void GetWearableAssetUuids(UUID wearableAssetUuid, IDictionary assetUuids) + private void GetWearableAssetUuids(UUID wearableAssetUuid, IDictionary assetUuids) { AssetBase assetBase = GetAsset(wearableAssetUuid); @@ -389,7 +325,7 @@ namespace OpenSim.Region.Framework.Scenes foreach (UUID uuid in wearableAsset.Textures.Values) { - assetUuids[uuid] = AssetType.Texture; + assetUuids[uuid] = (sbyte)AssetType.Texture; } } } @@ -401,7 +337,7 @@ namespace OpenSim.Region.Framework.Scenes /// /// /// - private void GetSceneObjectAssetUuids(UUID sceneObjectUuid, IDictionary assetUuids) + private void GetSceneObjectAssetUuids(UUID sceneObjectUuid, IDictionary assetUuids) { AssetBase objectAsset = GetAsset(sceneObjectUuid); @@ -430,7 +366,7 @@ namespace OpenSim.Region.Framework.Scenes /// /// /// - private void GetGestureAssetUuids(UUID gestureUuid, IDictionary assetUuids) + private void GetGestureAssetUuids(UUID gestureUuid, IDictionary assetUuids) { AssetBase assetBase = GetAsset(gestureUuid); if (null == assetBase) @@ -464,9 +400,29 @@ namespace OpenSim.Region.Framework.Scenes // If it can be parsed as a UUID, it is an asset ID UUID uuid; if (UUID.TryParse(id, out uuid)) - assetUuids[uuid] = AssetType.Animation; + assetUuids[uuid] = (sbyte)AssetType.Animation; } } + + /// + /// Get the asset uuid's referenced in a material. + /// + private void GetMaterialAssetUuids(UUID materialUuid, IDictionary assetUuids) + { + AssetBase assetBase = GetAsset(materialUuid); + if (null == assetBase) + return; + + OSDMap mat = (OSDMap)OSDParser.DeserializeLLSDXml(assetBase.Data); + + UUID normMap = mat["NormMap"].AsUUID(); + if (normMap != UUID.Zero) + assetUuids[normMap] = (sbyte)AssetType.Texture; + + UUID specMap = mat["SpecMap"].AsUUID(); + if (specMap != UUID.Zero) + assetUuids[specMap] = (sbyte)AssetType.Texture; + } } public class HGUuidGatherer : UuidGatherer diff --git a/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs index e70715485e..09041e8085 100644 --- a/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs +++ b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs @@ -42,77 +42,49 @@ using OpenSim.Framework.Servers; using OpenSim.Framework.Servers.HttpServer; using OpenSim.Region.Framework.Interfaces; using OpenSim.Region.Framework.Scenes; +using OpenSimAssetType = OpenSim.Framework.SLUtil.OpenSimAssetType; using Ionic.Zlib; // You will need to uncomment these lines if you are adding a region module to some other assembly which does not already // specify its assembly. Otherwise, the region modules in the assembly will not be picked up when OpenSimulator scans // the available DLLs -//[assembly: Addin("MaterialsDemoModule", "1.0")] +//[assembly: Addin("MaterialsModule", "1.0")] //[assembly: AddinDependency("OpenSim", "0.5")] -namespace OpenSim.Region.OptionalModules.MaterialsDemoModule +namespace OpenSim.Region.OptionalModules.Materials { - /// - /// - // # # ## ##### # # # # # #### - // # # # # # # ## # # ## # # # - // # # # # # # # # # # # # # # - // # ## # ###### ##### # # # # # # # # ### - // ## ## # # # # # ## # # ## # # - // # # # # # # # # # # # #### - // - // THIS MODULE IS FOR EXPERIMENTAL USE ONLY AND MAY CAUSE REGION OR ASSET CORRUPTION! - // - ////////////// WARNING ////////////////////////////////////////////////////////////////// - /// This is an *Experimental* module for developing support for materials-capable viewers - /// This module should NOT be used in a production environment! It may cause data corruption and - /// viewer crashes. It should be only used to evaluate implementations of materials. - /// - /// Materials are persisted via SceneObjectPart.dynattrs. This is a relatively new feature - /// of OpenSimulator and is not field proven at the time this module was written. Persistence - /// may fail or become corrupt and this could cause viewer crashes due to erroneous materials - /// data being sent to viewers. Materials descriptions might survive IAR, OAR, or other means - /// of archiving however the texture resources used by these materials probably will not as they - /// may not be adequately referenced to ensure proper archiving. - /// - /// - /// - /// To enable this module, add this string at the bottom of OpenSim.ini: - /// [MaterialsDemoModule] - /// - /// - /// - - [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "MaterialsDemoModule")] - public class MaterialsDemoModule : INonSharedRegionModule + [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "MaterialsModule")] + public class MaterialsModule : INonSharedRegionModule { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); - public string Name { get { return "MaterialsDemoModule"; } } + public string Name { get { return "MaterialsModule"; } } public Type ReplaceableInterface { get { return null; } } private Scene m_scene = null; private bool m_enabled = false; - public Dictionary m_knownMaterials = new Dictionary(); + public Dictionary m_regionMaterials = new Dictionary(); public void Initialise(IConfigSource source) { - m_enabled = (source.Configs["MaterialsDemoModule"] != null); + IConfig config = source.Configs["Materials"]; + if (config == null) + return; + + m_enabled = config.GetBoolean("enable_materials", true); if (!m_enabled) return; - m_log.DebugFormat("[MaterialsDemoModule]: Initialized"); + m_log.DebugFormat("[Materials]: Initialized"); } public void Close() { if (!m_enabled) return; - - //m_log.DebugFormat("[MaterialsDemoModule]: CLOSED MODULE"); } public void AddRegion(Scene scene) @@ -120,22 +92,19 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule if (!m_enabled) return; - //m_log.DebugFormat("[MaterialsDemoModule]: REGION {0} ADDED", scene.RegionInfo.RegionName); - m_scene = scene; m_scene.EventManager.OnRegisterCaps += OnRegisterCaps; m_scene.EventManager.OnObjectAddedToScene += EventManager_OnObjectAddedToScene; -// m_scene.EventManager.OnGatherUuids += GatherMaterialsUuids; } - void EventManager_OnObjectAddedToScene(SceneObjectGroup obj) + private void EventManager_OnObjectAddedToScene(SceneObjectGroup obj) { foreach (var part in obj.Parts) if (part != null) - GetStoredMaterialsForPart(part); + GetStoredMaterialsInPart(part); } - void OnRegisterCaps(OpenMetaverse.UUID agentID, OpenSim.Framework.Capabilities.Caps caps) + private void OnRegisterCaps(OpenMetaverse.UUID agentID, OpenSim.Framework.Capabilities.Caps caps) { string capsBase = "/CAPS/" + caps.CapsObjectPath; @@ -164,143 +133,65 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule m_scene.EventManager.OnRegisterCaps -= OnRegisterCaps; m_scene.EventManager.OnObjectAddedToScene -= EventManager_OnObjectAddedToScene; -// m_scene.EventManager.OnGatherUuids -= GatherMaterialsUuids; - - //m_log.DebugFormat("[MaterialsDemoModule]: REGION {0} REMOVED", scene.RegionInfo.RegionName); } public void RegionLoaded(Scene scene) { } - OSDMap GetMaterial(UUID id) - { - OSDMap map = null; - lock (m_knownMaterials) - { - if (m_knownMaterials.ContainsKey(id)) - { - map = new OSDMap(); - map["ID"] = OSD.FromBinary(id.GetBytes()); - map["Material"] = m_knownMaterials[id]; - } - } - return map; - } - - void GetStoredMaterialsForPart(SceneObjectPart part) + /// + /// Find the materials used in the SOP, and add them to 'm_regionMaterials'. + /// + private void GetStoredMaterialsInPart(SceneObjectPart part) { - OSD OSMaterials = null; - OSDArray matsArr = null; - - if (part.DynAttrs == null) - { - //m_log.Warn("[MaterialsDemoModule]: NULL DYNATTRS :( "); + if (part.Shape == null) return; - } - - lock (part.DynAttrs) - { - if (part.DynAttrs.ContainsStore("OpenSim", "Materials")) - { - OSDMap materialsStore = part.DynAttrs.GetStore("OpenSim", "Materials"); - - if (materialsStore == null) - return; - - materialsStore.TryGetValue("Materials", out OSMaterials); - } - - if (OSMaterials != null && OSMaterials is OSDArray) - matsArr = OSMaterials as OSDArray; - else - return; - } - - //m_log.Info("[MaterialsDemoModule]: OSMaterials: " + OSDParser.SerializeJsonString(OSMaterials)); - - if (matsArr == null) - { - //m_log.Info("[MaterialsDemoModule]: matsArr is null :( "); + var te = new Primitive.TextureEntry(part.Shape.TextureEntry, 0, part.Shape.TextureEntry.Length); + if (te == null) return; - } - foreach (OSD elemOsd in matsArr) + GetStoredMaterialInFace(part, te.DefaultTexture); + + foreach (Primitive.TextureEntryFace face in te.FaceTextures) { - if (elemOsd != null && elemOsd is OSDMap) - { - OSDMap matMap = elemOsd as OSDMap; - if (matMap.ContainsKey("ID") && matMap.ContainsKey("Material")) - { - try - { - lock (m_knownMaterials) - m_knownMaterials[matMap["ID"].AsUUID()] = (OSDMap)matMap["Material"]; - } - catch (Exception e) - { - m_log.Warn("[MaterialsDemoModule]: exception decoding persisted material ", e); - } - } - } + if (face != null) + GetStoredMaterialInFace(part, face); } } - void StoreMaterialsForPart(SceneObjectPart part) + /// + /// Find the materials used in one Face, and add them to 'm_regionMaterials'. + /// + private void GetStoredMaterialInFace(SceneObjectPart part, Primitive.TextureEntryFace face) { - try + UUID id = face.MaterialID; + if (id == UUID.Zero) + return; + + lock (m_regionMaterials) { - if (part == null || part.Shape == null) + if (m_regionMaterials.ContainsKey(id)) return; - - Dictionary mats = new Dictionary(); - - Primitive.TextureEntry te = part.Shape.Textures; - - if (te.DefaultTexture != null) + + byte[] data = m_scene.AssetService.GetData(id.ToString()); + if (data == null) { - lock (m_knownMaterials) - { - if (m_knownMaterials.ContainsKey(te.DefaultTexture.MaterialID)) - mats[te.DefaultTexture.MaterialID] = m_knownMaterials[te.DefaultTexture.MaterialID]; - } - } - - if (te.FaceTextures != null) - { - foreach (var face in te.FaceTextures) - { - if (face != null) - { - lock (m_knownMaterials) - { - if (m_knownMaterials.ContainsKey(face.MaterialID)) - mats[face.MaterialID] = m_knownMaterials[face.MaterialID]; - } - } - } - } - if (mats.Count == 0) + m_log.WarnFormat("[Materials]: Prim \"{0}\" ({1}) contains unknown material ID {2}", part.Name, part.UUID, id); return; - - OSDArray matsArr = new OSDArray(); - foreach (KeyValuePair kvp in mats) - { - OSDMap matOsd = new OSDMap(); - matOsd["ID"] = OSD.FromUUID(kvp.Key); - matOsd["Material"] = kvp.Value; - matsArr.Add(matOsd); } - OSDMap OSMaterials = new OSDMap(); - OSMaterials["Materials"] = matsArr; + OSDMap mat; + try + { + mat = (OSDMap)OSDParser.DeserializeLLSDXml(data); + } + catch (Exception e) + { + m_log.WarnFormat("[Materials]: cannot decode material asset {0}: {1}", id, e.Message); + return; + } - lock (part.DynAttrs) - part.DynAttrs.SetStore("OpenSim", "Materials", OSMaterials); - } - catch (Exception e) - { - m_log.Warn("[MaterialsDemoModule]: exception in StoreMaterialsForPart() ", e); + m_regionMaterials[id] = mat; } } @@ -308,8 +199,6 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule string param, IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) { - //m_log.Debug("[MaterialsDemoModule]: POST cap handler"); - OSDMap req = (OSDMap)OSDParser.DeserializeLLSDXml(request); OSDMap resp = new OSDMap(); @@ -333,34 +222,38 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule { foreach (OSD elem in (OSDArray)osd) { - try { UUID id = new UUID(elem.AsBinary(), 0); - lock (m_knownMaterials) + lock (m_regionMaterials) { - if (m_knownMaterials.ContainsKey(id)) + if (m_regionMaterials.ContainsKey(id)) { - //m_log.Info("[MaterialsDemoModule]: request for known material ID: " + id.ToString()); OSDMap matMap = new OSDMap(); matMap["ID"] = OSD.FromBinary(id.GetBytes()); - - matMap["Material"] = m_knownMaterials[id]; + matMap["Material"] = m_regionMaterials[id]; respArr.Add(matMap); } else - m_log.Info("[MaterialsDemoModule]: request for UNKNOWN material ID: " + id.ToString()); + { + m_log.Warn("[Materials]: request for unknown material ID: " + id.ToString()); + + // Theoretically we could try to load the material from the assets service, + // but that shouldn't be necessary because the viewer should only request + // materials that exist in a prim on the region, and all of these materials + // are already stored in m_regionMaterials. + } } } catch (Exception e) { - // report something here? + m_log.Error("Error getting materials in response to viewer request", e); continue; } } } - else if (osd is OSDMap) // reqest to assign a material + else if (osd is OSDMap) // request to assign a material { materialsFromViewer = osd as OSDMap; @@ -375,94 +268,118 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule { foreach (OSDMap matsMap in matsArr) { - //m_log.Debug("[MaterialsDemoModule]: processing matsMap: " + OSDParser.SerializeJsonString(matsMap)); - uint primLocalID = 0; - try { primLocalID = matsMap["ID"].AsUInteger(); } - catch (Exception e) { m_log.Warn("[MaterialsDemoModule]: cannot decode \"ID\" from matsMap: " + e.Message); } - //m_log.Debug("[MaterialsDemoModule]: primLocalID: " + primLocalID.ToString()); + try { + primLocalID = matsMap["ID"].AsUInteger(); + } + catch (Exception e) { + m_log.Warn("[Materials]: cannot decode \"ID\" from matsMap: " + e.Message); + continue; + } OSDMap mat = null; - try { mat = matsMap["Material"] as OSDMap; } - catch (Exception e) { m_log.Warn("[MaterialsDemoModule]: cannot decode \"Material\" from matsMap: " + e.Message); } - //m_log.Debug("[MaterialsDemoModule]: mat: " + OSDParser.SerializeJsonString(mat)); + try + { + mat = matsMap["Material"] as OSDMap; + } + catch (Exception e) + { + m_log.Warn("[Materials]: cannot decode \"Material\" from matsMap: " + e.Message); + continue; + } + + SceneObjectPart sop = m_scene.GetSceneObjectPart(primLocalID); + if (sop == null) + { + m_log.WarnFormat("[Materials]: SOP not found for localId: {0}", primLocalID.ToString()); + continue; + } + + Primitive.TextureEntry te = new Primitive.TextureEntry(sop.Shape.TextureEntry, 0, sop.Shape.TextureEntry.Length); + if (te == null) + { + m_log.WarnFormat("[Materials]: Error in TextureEntry for SOP {0} {1}", sop.Name, sop.UUID); + continue; + } + UUID id; if (mat == null) { + // This happens then the user removes a material from a prim id = UUID.Zero; } else { - id = HashOsd(mat); - lock (m_knownMaterials) - m_knownMaterials[id] = mat; + // Material UUID = hash of the material's data. + // This makes materials deduplicate across the entire grid (but isn't otherwise required). + byte[] data = System.Text.Encoding.ASCII.GetBytes(OSDParser.SerializeLLSDXmlString(mat)); + using (var md5 = MD5.Create()) + id = new UUID(md5.ComputeHash(data), 0); + + lock (m_regionMaterials) + { + if (!m_regionMaterials.ContainsKey(id)) + { + m_regionMaterials[id] = mat; + + // This asset might exist already, but it's ok to try to store it again + string name = "Material " + ChooseMaterialName(mat, sop); + name = name.Substring(0, Math.Min(64, name.Length)).Trim(); + AssetBase asset = new AssetBase(id, name, (sbyte)OpenSimAssetType.Material, sop.OwnerID.ToString()); + asset.Data = data; + m_scene.AssetService.Store(asset); + } + } } - var sop = m_scene.GetSceneObjectPart(primLocalID); - if (sop == null) - m_log.Debug("[MaterialsDemoModule]: null SOP for localId: " + primLocalID.ToString()); - else - { - var te = new Primitive.TextureEntry(sop.Shape.TextureEntry, 0, sop.Shape.TextureEntry.Length); - if (te == null) + int face = -1; + + if (matsMap.ContainsKey("Face")) + { + face = matsMap["Face"].AsInteger(); + if (te.FaceTextures == null) // && face == 0) { - m_log.Debug("[MaterialsDemoModule]: null TextureEntry for localId: " + primLocalID.ToString()); + if (te.DefaultTexture == null) + m_log.WarnFormat("[Materials]: te.DefaultTexture is null in {0} {1}", sop.Name, sop.UUID); + else + te.DefaultTexture.MaterialID = id; } else { - int face = -1; - - if (matsMap.ContainsKey("Face")) + if (te.FaceTextures.Length >= face - 1) { - face = matsMap["Face"].AsInteger(); - if (te.FaceTextures == null) // && face == 0) - { - if (te.DefaultTexture == null) - m_log.Debug("[MaterialsDemoModule]: te.DefaultTexture is null"); - else - te.DefaultTexture.MaterialID = id; - } - else - { - if (te.FaceTextures.Length >= face - 1) - { - if (te.FaceTextures[face] == null) - te.DefaultTexture.MaterialID = id; - else - te.FaceTextures[face].MaterialID = id; - } - } - } - else - { - if (te.DefaultTexture != null) + if (te.FaceTextures[face] == null) te.DefaultTexture.MaterialID = id; - } - - //m_log.DebugFormat("[MaterialsDemoModule]: in \"{0}\", setting material ID for face {1} to {2}", sop.Name, face, id); - - //we cant use sop.UpdateTextureEntry(te); because it filters so do it manually - - if (sop.ParentGroup != null) - { - sop.Shape.TextureEntry = te.GetBytes(); - sop.TriggerScriptChangedEvent(Changed.TEXTURE); - sop.UpdateFlag = UpdateRequired.FULL; - sop.ParentGroup.HasGroupChanged = true; - - sop.ScheduleFullUpdate(); - - StoreMaterialsForPart(sop); + else + te.FaceTextures[face].MaterialID = id; } } } + else + { + if (te.DefaultTexture != null) + te.DefaultTexture.MaterialID = id; + } + + //m_log.DebugFormat("[Materials]: in \"{0}\" {1}, setting material ID for face {2} to {3}", sop.Name, sop.UUID, face, id); + + // We can't use sop.UpdateTextureEntry(te) because it filters, so do it manually + sop.Shape.TextureEntry = te.GetBytes(); + + if (sop.ParentGroup != null) + { + sop.TriggerScriptChangedEvent(Changed.TEXTURE); + sop.UpdateFlag = UpdateRequired.FULL; + sop.ParentGroup.HasGroupChanged = true; + sop.ScheduleFullUpdate(); + } } } catch (Exception e) { - m_log.Warn("[MaterialsDemoModule]: exception processing received material ", e); + m_log.Warn("[Materials]: exception processing received material ", e); } } } @@ -472,36 +389,63 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule } catch (Exception e) { - m_log.Warn("[MaterialsDemoModule]: exception decoding zipped CAP payload ", e); + m_log.Warn("[Materials]: exception decoding zipped CAP payload ", e); //return ""; } - //m_log.Debug("[MaterialsDemoModule]: knownMaterials.Count: " + m_knownMaterials.Count.ToString()); } resp["Zipped"] = ZCompressOSD(respArr, false); string response = OSDParser.SerializeLLSDXmlString(resp); - //m_log.Debug("[MaterialsDemoModule]: cap request: " + request); - //m_log.Debug("[MaterialsDemoModule]: cap request (zipped portion): " + ZippedOsdBytesToString(req["Zipped"].AsBinary())); - //m_log.Debug("[MaterialsDemoModule]: cap response: " + response); + //m_log.Debug("[Materials]: cap request: " + request); + //m_log.Debug("[Materials]: cap request (zipped portion): " + ZippedOsdBytesToString(req["Zipped"].AsBinary())); + //m_log.Debug("[Materials]: cap response: " + response); return response; } + /// + /// Use heuristics to choose a good name for the material. + /// + private string ChooseMaterialName(OSDMap mat, SceneObjectPart sop) + { + UUID normMap = mat["NormMap"].AsUUID(); + if (normMap != UUID.Zero) + { + AssetBase asset = m_scene.AssetService.GetCached(normMap.ToString()); + if ((asset != null) && (asset.Name.Length > 0) && !asset.Name.Equals("From IAR")) + return asset.Name; + } + + UUID specMap = mat["SpecMap"].AsUUID(); + if (specMap != UUID.Zero) + { + AssetBase asset = m_scene.AssetService.GetCached(specMap.ToString()); + if ((asset != null) && (asset.Name.Length > 0) && !asset.Name.Equals("From IAR")) + return asset.Name; + } + + if (sop.Name != "Primitive") + return sop.Name; + + if ((sop.ParentGroup != null) && (sop.ParentGroup.Name != "Primitive")) + return sop.ParentGroup.Name; + + return ""; + } + public string RenderMaterialsGetCap(string request, string path, string param, IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) { - //m_log.Debug("[MaterialsDemoModule]: GET cap handler"); - OSDMap resp = new OSDMap(); int matsCount = 0; OSDArray allOsd = new OSDArray(); - lock (m_knownMaterials) + lock (m_regionMaterials) { - foreach (KeyValuePair kvp in m_knownMaterials) + foreach (KeyValuePair kvp in m_regionMaterials) { OSDMap matMap = new OSDMap(); @@ -513,12 +457,11 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule } resp["Zipped"] = ZCompressOSD(allOsd, false); - //m_log.Debug("[MaterialsDemoModule]: matsCount: " + matsCount.ToString()); return OSDParser.SerializeLLSDXmlString(resp); } - static string ZippedOsdBytesToString(byte[] bytes) + private static string ZippedOsdBytesToString(byte[] bytes) { try { @@ -537,26 +480,27 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule /// private static UUID HashOsd(OSD osd) { + byte[] data = OSDParser.SerializeLLSDBinary(osd, false); using (var md5 = MD5.Create()) - using (MemoryStream ms = new MemoryStream(OSDParser.SerializeLLSDBinary(osd, false))) - return new UUID(md5.ComputeHash(ms), 0); + return new UUID(md5.ComputeHash(data), 0); } public static OSD ZCompressOSD(OSD inOsd, bool useHeader) { OSD osd = null; + byte[] data = OSDParser.SerializeLLSDBinary(inOsd, useHeader); + using (MemoryStream msSinkCompressed = new MemoryStream()) { using (Ionic.Zlib.ZlibStream zOut = new Ionic.Zlib.ZlibStream(msSinkCompressed, Ionic.Zlib.CompressionMode.Compress, CompressionLevel.BestCompression, true)) { - CopyStream(new MemoryStream(OSDParser.SerializeLLSDBinary(inOsd, useHeader)), zOut); - zOut.Close(); + zOut.Write(data, 0, data.Length); } msSinkCompressed.Seek(0L, SeekOrigin.Begin); - osd = OSD.FromBinary( msSinkCompressed.ToArray()); + osd = OSD.FromBinary(msSinkCompressed.ToArray()); } return osd; @@ -571,94 +515,14 @@ namespace OpenSim.Region.OptionalModules.MaterialsDemoModule { using (Ionic.Zlib.ZlibStream zOut = new Ionic.Zlib.ZlibStream(msSinkUnCompressed, CompressionMode.Decompress, true)) { - CopyStream(new MemoryStream(input), zOut); - zOut.Close(); + zOut.Write(input, 0, input.Length); } + msSinkUnCompressed.Seek(0L, SeekOrigin.Begin); osd = OSDParser.DeserializeLLSDBinary(msSinkUnCompressed.ToArray()); } return osd; } - - static void CopyStream(System.IO.Stream input, System.IO.Stream output) - { - byte[] buffer = new byte[2048]; - int len; - while ((len = input.Read(buffer, 0, 2048)) > 0) - { - output.Write(buffer, 0, len); - } - - output.Flush(); - } - - // FIXME: This code is currently still in UuidGatherer since we cannot use Scene.EventManager as some - // calls to the gatherer are done for objects with no scene. -// /// -// /// Gather all of the texture asset UUIDs used to reference "Materials" such as normal and specular maps -// /// -// /// -// /// -// private void GatherMaterialsUuids(SceneObjectPart part, IDictionary assetUuids) -// { -// // scan thru the dynAttrs map of this part for any textures used as materials -// OSD osdMaterials = null; -// -// lock (part.DynAttrs) -// { -// if (part.DynAttrs.ContainsStore("OpenSim", "Materials")) -// { -// OSDMap materialsStore = part.DynAttrs.GetStore("OpenSim", "Materials"); -// if (materialsStore == null) -// return; -// -// materialsStore.TryGetValue("Materials", out osdMaterials); -// } -// -// if (osdMaterials != null) -// { -// //m_log.Info("[UUID Gatherer]: found Materials: " + OSDParser.SerializeJsonString(osd)); -// -// if (osdMaterials is OSDArray) -// { -// OSDArray matsArr = osdMaterials as OSDArray; -// foreach (OSDMap matMap in matsArr) -// { -// try -// { -// if (matMap.ContainsKey("Material")) -// { -// OSDMap mat = matMap["Material"] as OSDMap; -// if (mat.ContainsKey("NormMap")) -// { -// UUID normalMapId = mat["NormMap"].AsUUID(); -// if (normalMapId != UUID.Zero) -// { -// assetUuids[normalMapId] = AssetType.Texture; -// //m_log.Info("[UUID Gatherer]: found normal map ID: " + normalMapId.ToString()); -// } -// } -// if (mat.ContainsKey("SpecMap")) -// { -// UUID specularMapId = mat["SpecMap"].AsUUID(); -// if (specularMapId != UUID.Zero) -// { -// assetUuids[specularMapId] = AssetType.Texture; -// //m_log.Info("[UUID Gatherer]: found specular map ID: " + specularMapId.ToString()); -// } -// } -// } -// -// } -// catch (Exception e) -// { -// m_log.Warn("[MaterialsDemoModule]: exception getting materials: " + e.Message); -// } -// } -// } -// } -// } -// } } } diff --git a/bin/OpenSim.ini.example b/bin/OpenSim.ini.example index 28c16cf5dc..28369a3ae1 100644 --- a/bin/OpenSim.ini.example +++ b/bin/OpenSim.ini.example @@ -691,6 +691,12 @@ ; enable_windlight = false +[Materials] + ;# {enable_materials} {} {Enable Materials support?} {true false} true + ;; This enables the use of Materials. + ; enable_materials = true + + [DataSnapshot] ;# {index_sims} {} {Enable data snapshotting (search)?} {true false} false ;; The following set of configs pertains to search. From 68d83425c6b39614210b28e97d5006a882ea3097 Mon Sep 17 00:00:00 2001 From: Oren Hurvitz Date: Thu, 12 Dec 2013 15:14:24 +0200 Subject: [PATCH 04/15] When asked to change the Material for one face, change only that face; not the default material --- .../Materials/MaterialsModule.cs | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs index 09041e8085..97795941aa 100644 --- a/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs +++ b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs @@ -339,27 +339,14 @@ namespace OpenSim.Region.OptionalModules.Materials if (matsMap.ContainsKey("Face")) { face = matsMap["Face"].AsInteger(); - if (te.FaceTextures == null) // && face == 0) - { - if (te.DefaultTexture == null) - m_log.WarnFormat("[Materials]: te.DefaultTexture is null in {0} {1}", sop.Name, sop.UUID); - else - te.DefaultTexture.MaterialID = id; - } - else - { - if (te.FaceTextures.Length >= face - 1) - { - if (te.FaceTextures[face] == null) - te.DefaultTexture.MaterialID = id; - else - te.FaceTextures[face].MaterialID = id; - } - } + Primitive.TextureEntryFace faceEntry = te.CreateFace((uint)face); + faceEntry.MaterialID = id; } else { - if (te.DefaultTexture != null) + if (te.DefaultTexture == null) + m_log.WarnFormat("[Materials]: TextureEntry.DefaultTexture is null in {0} {1}", sop.Name, sop.UUID); + else te.DefaultTexture.MaterialID = id; } From d1f16c4b4b3f5c0938f3f0572c70e92cb90b6a0b Mon Sep 17 00:00:00 2001 From: Oren Hurvitz Date: Sun, 5 Jan 2014 14:03:10 +0200 Subject: [PATCH 05/15] Check agent permissions before modifying an object's materials. Also, when creating a Material asset, set the current agent as the Creator. --- .../Materials/MaterialsModule.cs | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs index 97795941aa..4b635d8c72 100644 --- a/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs +++ b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs @@ -109,7 +109,10 @@ namespace OpenSim.Region.OptionalModules.Materials string capsBase = "/CAPS/" + caps.CapsObjectPath; IRequestHandler renderMaterialsPostHandler - = new RestStreamHandler("POST", capsBase + "/", RenderMaterialsPostCap, "RenderMaterials", null); + = new RestStreamHandler("POST", capsBase + "/", + (request, path, param, httpRequest, httpResponse) + => RenderMaterialsPostCap(request, agentID), + "RenderMaterials", null); caps.RegisterHandler("RenderMaterials", renderMaterialsPostHandler); // OpenSimulator CAPs infrastructure seems to be somewhat hostile towards any CAP that requires both GET @@ -117,12 +120,18 @@ namespace OpenSim.Region.OptionalModules.Materials // handler normally and then add a GET handler via MainServer IRequestHandler renderMaterialsGetHandler - = new RestStreamHandler("GET", capsBase + "/", RenderMaterialsGetCap, "RenderMaterials", null); + = new RestStreamHandler("GET", capsBase + "/", + (request, path, param, httpRequest, httpResponse) + => RenderMaterialsGetCap(request), + "RenderMaterials", null); MainServer.Instance.AddStreamHandler(renderMaterialsGetHandler); // materials viewer seems to use either POST or PUT, so assign POST handler for PUT as well IRequestHandler renderMaterialsPutHandler - = new RestStreamHandler("PUT", capsBase + "/", RenderMaterialsPostCap, "RenderMaterials", null); + = new RestStreamHandler("PUT", capsBase + "/", + (request, path, param, httpRequest, httpResponse) + => RenderMaterialsPostCap(request, agentID), + "RenderMaterials", null); MainServer.Instance.AddStreamHandler(renderMaterialsPutHandler); } @@ -195,9 +204,7 @@ namespace OpenSim.Region.OptionalModules.Materials } } - public string RenderMaterialsPostCap(string request, string path, - string param, IOSHttpRequest httpRequest, - IOSHttpResponse httpResponse) + public string RenderMaterialsPostCap(string request, UUID agentID) { OSDMap req = (OSDMap)OSDParser.DeserializeLLSDXml(request); OSDMap resp = new OSDMap(); @@ -295,6 +302,12 @@ namespace OpenSim.Region.OptionalModules.Materials continue; } + if (!m_scene.Permissions.CanEditObject(sop.UUID, agentID)) + { + m_log.WarnFormat("User {0} can't edit object {1} {2}", agentID, sop.Name, sop.UUID); + continue; + } + Primitive.TextureEntry te = new Primitive.TextureEntry(sop.Shape.TextureEntry, 0, sop.Shape.TextureEntry.Length); if (te == null) { @@ -326,7 +339,7 @@ namespace OpenSim.Region.OptionalModules.Materials // This asset might exist already, but it's ok to try to store it again string name = "Material " + ChooseMaterialName(mat, sop); name = name.Substring(0, Math.Min(64, name.Length)).Trim(); - AssetBase asset = new AssetBase(id, name, (sbyte)OpenSimAssetType.Material, sop.OwnerID.ToString()); + AssetBase asset = new AssetBase(id, name, (sbyte)OpenSimAssetType.Material, agentID.ToString()); asset.Data = data; m_scene.AssetService.Store(asset); } @@ -422,9 +435,7 @@ namespace OpenSim.Region.OptionalModules.Materials } - public string RenderMaterialsGetCap(string request, string path, - string param, IOSHttpRequest httpRequest, - IOSHttpResponse httpResponse) + public string RenderMaterialsGetCap(string request) { OSDMap resp = new OSDMap(); int matsCount = 0; From 28723beb0ccec654ac24ee1632b137b424fd3360 Mon Sep 17 00:00:00 2001 From: dahlia Date: Mon, 20 Jan 2014 02:57:08 -0800 Subject: [PATCH 06/15] Add code to convert legacy materials stored in DynAttrs to new asset format and store them as assets --- .../Materials/MaterialsModule.cs | 122 +++++++++++++++--- 1 file changed, 102 insertions(+), 20 deletions(-) diff --git a/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs index 4b635d8c72..1b5a7a37d1 100644 --- a/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs +++ b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs @@ -148,6 +148,78 @@ namespace OpenSim.Region.OptionalModules.Materials { } + /// + /// Searches the part for any legacy materials stored in DynAttrs and converts them to assets, replacing + /// the MaterialIDs in the TextureEntries for the part. + /// Deletes the legacy materials from the part as they are no longer needed. + /// + /// + private void ConvertLegacyMaterialsInPart(SceneObjectPart part) + { + if (part.DynAttrs == null) + return; + + var te = new Primitive.TextureEntry(part.Shape.TextureEntry, 0, part.Shape.TextureEntry.Length); + if (te == null) + return; + + OSD OSMaterials = null; + OSDArray matsArr = null; + + lock (part.DynAttrs) + { + if (part.DynAttrs.ContainsStore("OpenSim", "Materials")) + { + OSDMap materialsStore = part.DynAttrs.GetStore("OpenSim", "Materials"); + + if (materialsStore == null) + return; + + materialsStore.TryGetValue("Materials", out OSMaterials); + } + + if (OSMaterials != null && OSMaterials is OSDArray) + matsArr = OSMaterials as OSDArray; + else + return; + } + + if (matsArr == null) + return; + + foreach (OSD elemOsd in matsArr) + { + if (elemOsd != null && elemOsd is OSDMap) + { + OSDMap matMap = elemOsd as OSDMap; + if (matMap.ContainsKey("ID") && matMap.ContainsKey("Material")) + { + UUID id = matMap["ID"].AsUUID(); + OSDMap material = (OSDMap)matMap["Material"]; + bool used = false; + + foreach (var face in te.FaceTextures) + if (face.MaterialID == id) + used = true; + + if (used) + { // store legacy material in new asset format, and update the part texture entry with the new hashed UUID + + var newId = StoreMaterialAsAsset(part.CreatorID, material, part); + foreach (var face in te.FaceTextures) + if (face.MaterialID == id) + face.MaterialID = newId; + } + } + } + } + + part.Shape.TextureEntry = te.GetBytes(); + + lock (part.DynAttrs) + part.DynAttrs.RemoveStore("OpenSim", "Materials"); + } + /// /// Find the materials used in the SOP, and add them to 'm_regionMaterials'. /// @@ -155,6 +227,9 @@ namespace OpenSim.Region.OptionalModules.Materials { if (part.Shape == null) return; + + ConvertLegacyMaterialsInPart(part); + var te = new Primitive.TextureEntry(part.Shape.TextureEntry, 0, part.Shape.TextureEntry.Length); if (te == null) return; @@ -324,26 +399,7 @@ namespace OpenSim.Region.OptionalModules.Materials } else { - // Material UUID = hash of the material's data. - // This makes materials deduplicate across the entire grid (but isn't otherwise required). - byte[] data = System.Text.Encoding.ASCII.GetBytes(OSDParser.SerializeLLSDXmlString(mat)); - using (var md5 = MD5.Create()) - id = new UUID(md5.ComputeHash(data), 0); - - lock (m_regionMaterials) - { - if (!m_regionMaterials.ContainsKey(id)) - { - m_regionMaterials[id] = mat; - - // This asset might exist already, but it's ok to try to store it again - string name = "Material " + ChooseMaterialName(mat, sop); - name = name.Substring(0, Math.Min(64, name.Length)).Trim(); - AssetBase asset = new AssetBase(id, name, (sbyte)OpenSimAssetType.Material, agentID.ToString()); - asset.Data = data; - m_scene.AssetService.Store(asset); - } - } + id = StoreMaterialAsAsset(agentID, mat, sop); } @@ -404,6 +460,32 @@ namespace OpenSim.Region.OptionalModules.Materials return response; } + private UUID StoreMaterialAsAsset(UUID agentID, OSDMap mat, SceneObjectPart sop) + { + UUID id; + // Material UUID = hash of the material's data. + // This makes materials deduplicate across the entire grid (but isn't otherwise required). + byte[] data = System.Text.Encoding.ASCII.GetBytes(OSDParser.SerializeLLSDXmlString(mat)); + using (var md5 = MD5.Create()) + id = new UUID(md5.ComputeHash(data), 0); + + lock (m_regionMaterials) + { + if (!m_regionMaterials.ContainsKey(id)) + { + m_regionMaterials[id] = mat; + + // This asset might exist already, but it's ok to try to store it again + string name = "Material " + ChooseMaterialName(mat, sop); + name = name.Substring(0, Math.Min(64, name.Length)).Trim(); + AssetBase asset = new AssetBase(id, name, (sbyte)OpenSimAssetType.Material, agentID.ToString()); + asset.Data = data; + m_scene.AssetService.Store(asset); + } + } + return id; + } + /// /// Use heuristics to choose a good name for the material. /// From 95c926b2cd8585dd5b84ad7827d21b6122ea1001 Mon Sep 17 00:00:00 2001 From: dahlia Date: Mon, 20 Jan 2014 03:02:30 -0800 Subject: [PATCH 07/15] delay texture entry parsing until absolutely necessary while converting legacy materials --- .../Region/OptionalModules/Materials/MaterialsModule.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs index 1b5a7a37d1..d8ec9795dd 100644 --- a/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs +++ b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs @@ -159,10 +159,6 @@ namespace OpenSim.Region.OptionalModules.Materials if (part.DynAttrs == null) return; - var te = new Primitive.TextureEntry(part.Shape.TextureEntry, 0, part.Shape.TextureEntry.Length); - if (te == null) - return; - OSD OSMaterials = null; OSDArray matsArr = null; @@ -187,6 +183,10 @@ namespace OpenSim.Region.OptionalModules.Materials if (matsArr == null) return; + var te = new Primitive.TextureEntry(part.Shape.TextureEntry, 0, part.Shape.TextureEntry.Length); + if (te == null) + return; + foreach (OSD elemOsd in matsArr) { if (elemOsd != null && elemOsd is OSDMap) From 36d8a24a867fbbc95214653fec463aced8ba7c5f Mon Sep 17 00:00:00 2001 From: dahlia Date: Mon, 20 Jan 2014 03:11:01 -0800 Subject: [PATCH 08/15] force SOG update when converting legacy materials to ensure changes are persisted --- OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs index d8ec9795dd..ce2a56abda 100644 --- a/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs +++ b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs @@ -215,6 +215,8 @@ namespace OpenSim.Region.OptionalModules.Materials } part.Shape.TextureEntry = te.GetBytes(); + part.ParentGroup.HasGroupChanged = true; + part.ScheduleFullUpdate(); lock (part.DynAttrs) part.DynAttrs.RemoveStore("OpenSim", "Materials"); From 4800303abdaff19471988097c89193ff6cc4b24e Mon Sep 17 00:00:00 2001 From: dahlia Date: Mon, 20 Jan 2014 03:18:36 -0800 Subject: [PATCH 09/15] update OpenMetaverse.StructuredData to git master (bf4e9f654ff99c85e20b53e56faac38e307dd8c2) which fixes JSON OSD serialization to a standards compliant means of encoding floating point NaN and Infinity --- bin/OpenMetaverse.StructuredData.XML | 234 +++++++++++++-------------- bin/OpenMetaverse.StructuredData.dll | Bin 106496 -> 106496 bytes 2 files changed, 117 insertions(+), 117 deletions(-) diff --git a/bin/OpenMetaverse.StructuredData.XML b/bin/OpenMetaverse.StructuredData.XML index 789ad5b811..3999d9977d 100644 --- a/bin/OpenMetaverse.StructuredData.XML +++ b/bin/OpenMetaverse.StructuredData.XML @@ -4,123 +4,6 @@ OpenMetaverse.StructuredData - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Uses reflection to create an SDMap from all of the SD - serializable types in an object - - Class or struct containing serializable types - An SDMap holding the serialized values from the - container object - - - - Uses reflection to deserialize member variables in an object from - an SDMap - - Reference to an object to fill with deserialized - values - Serialized values to put in the target - object - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -345,5 +228,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Uses reflection to create an SDMap from all of the SD + serializable types in an object + + Class or struct containing serializable types + An SDMap holding the serialized values from the + container object + + + + Uses reflection to deserialize member variables in an object from + an SDMap + + Reference to an object to fill with deserialized + values + Serialized values to put in the target + object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/OpenMetaverse.StructuredData.dll b/bin/OpenMetaverse.StructuredData.dll index d980531e18d08cdd5c971bd110479c3e4705cd94..ed613e710f0803df6ea03f2679ed6258d1caeb16 100755 GIT binary patch literal 106496 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 Oz>EZDB=G;H1pWt+xKmI7 From 8e72b53edc435c2c2fbec0b8c91304e7f7a6a4f2 Mon Sep 17 00:00:00 2001 From: "Justin Clark-Casey (justincc)" Date: Mon, 20 Jan 2014 19:16:19 +0000 Subject: [PATCH 10/15] Stop exceptions being generated on agent connection if a telehub object has been deleted or has no spawn points. --- OpenSim/Framework/RegionSettings.cs | 21 ++-- .../World/Estate/EstateManagementCommands.cs | 2 +- .../World/Estate/EstateManagementModule.cs | 8 +- OpenSim/Region/Framework/Scenes/Scene.cs | 50 +++++--- .../Scenes/Tests/SceneTelehubTests.cs | 119 ++++++++++++++++++ OpenSim/Tests/Common/Mock/TestClient.cs | 5 - OpenSim/Tests/Common/TestHelpers.cs | 21 +++- 7 files changed, 186 insertions(+), 40 deletions(-) create mode 100644 OpenSim/Region/Framework/Scenes/Tests/SceneTelehubTests.cs diff --git a/OpenSim/Framework/RegionSettings.cs b/OpenSim/Framework/RegionSettings.cs index db8c53ef95..a895c40cc1 100644 --- a/OpenSim/Framework/RegionSettings.cs +++ b/OpenSim/Framework/RegionSettings.cs @@ -482,21 +482,14 @@ namespace OpenSim.Framework set { m_LoadedCreationID = value; } } - // Connected Telehub object - private UUID m_TelehubObject = UUID.Zero; - public UUID TelehubObject - { - get - { - return m_TelehubObject; - } - set - { - m_TelehubObject = value; - } - } + ///

+ /// Connected Telehub object + /// + public UUID TelehubObject { get; set; } - // Our Connected Telehub's SpawnPoints + /// + /// Our connected Telehub's SpawnPoints + /// public List l_SpawnPoints = new List(); // Add a SpawnPoint diff --git a/OpenSim/Region/CoreModules/World/Estate/EstateManagementCommands.cs b/OpenSim/Region/CoreModules/World/Estate/EstateManagementCommands.cs index 173b603fe5..1659493981 100644 --- a/OpenSim/Region/CoreModules/World/Estate/EstateManagementCommands.cs +++ b/OpenSim/Region/CoreModules/World/Estate/EstateManagementCommands.cs @@ -60,7 +60,7 @@ namespace OpenSim.Region.CoreModules.World.Estate public void Initialise() { - m_log.DebugFormat("[ESTATE MODULE]: Setting up estate commands for region {0}", m_module.Scene.RegionInfo.RegionName); +// m_log.DebugFormat("[ESTATE MODULE]: Setting up estate commands for region {0}", m_module.Scene.RegionInfo.RegionName); m_module.Scene.AddCommand("Regions", m_module, "set terrain texture", "set terrain texture [] []", diff --git a/OpenSim/Region/CoreModules/World/Estate/EstateManagementModule.cs b/OpenSim/Region/CoreModules/World/Estate/EstateManagementModule.cs index 17387da751..3bd7b4a218 100644 --- a/OpenSim/Region/CoreModules/World/Estate/EstateManagementModule.cs +++ b/OpenSim/Region/CoreModules/World/Estate/EstateManagementModule.cs @@ -702,7 +702,7 @@ namespace OpenSim.Region.CoreModules.World.Estate } } - public void handleOnEstateManageTelehub(IClientAPI client, UUID invoice, UUID senderID, string cmd, uint param1) + public void HandleOnEstateManageTelehub(IClientAPI client, UUID invoice, UUID senderID, string cmd, uint param1) { SceneObjectPart part; @@ -742,7 +742,9 @@ namespace OpenSim.Region.CoreModules.World.Estate default: break; } - SendTelehubInfo(client); + + if (client != null) + SendTelehubInfo(client); } private void SendSimulatorBlueBoxMessage( @@ -1214,7 +1216,7 @@ namespace OpenSim.Region.CoreModules.World.Estate client.OnEstateRestartSimRequest += handleEstateRestartSimRequest; client.OnEstateChangeCovenantRequest += handleChangeEstateCovenantRequest; client.OnEstateChangeInfo += handleEstateChangeInfo; - client.OnEstateManageTelehub += handleOnEstateManageTelehub; + client.OnEstateManageTelehub += HandleOnEstateManageTelehub; client.OnUpdateEstateAccessDeltaRequest += handleEstateAccessDeltaRequest; client.OnSimulatorBlueBoxMessageRequest += SendSimulatorBlueBoxMessage; client.OnEstateBlueBoxMessageRequest += SendEstateBlueBoxMessage; diff --git a/OpenSim/Region/Framework/Scenes/Scene.cs b/OpenSim/Region/Framework/Scenes/Scene.cs index 567ce2ad03..59c5b0922e 100644 --- a/OpenSim/Region/Framework/Scenes/Scene.cs +++ b/OpenSim/Region/Framework/Scenes/Scene.cs @@ -3946,32 +3946,52 @@ namespace OpenSim.Region.Framework.Scenes } } +// m_log.DebugFormat( +// "[SCENE]: Found telehub object {0} for new user connection {1} to {2}", +// RegionInfo.RegionSettings.TelehubObject, acd.Name, Name); + // Honor Estate teleport routing via Telehubs excluding ViaHome and GodLike TeleportFlags if (RegionInfo.RegionSettings.TelehubObject != UUID.Zero && RegionInfo.EstateSettings.AllowDirectTeleport == false && !viahome && !godlike) { SceneObjectGroup telehub = GetSceneObjectGroup(RegionInfo.RegionSettings.TelehubObject); - // Can have multiple SpawnPoints - List spawnpoints = RegionInfo.RegionSettings.SpawnPoints(); - if (spawnpoints.Count > 1) + + if (telehub != null) { - // We have multiple SpawnPoints, Route the agent to a random or sequential one - if (SpawnPointRouting == "random") - acd.startpos = spawnpoints[Util.RandomClass.Next(spawnpoints.Count) - 1].GetLocation( - telehub.AbsolutePosition, - telehub.GroupRotation - ); + // Can have multiple SpawnPoints + List spawnpoints = RegionInfo.RegionSettings.SpawnPoints(); + if (spawnpoints.Count > 1) + { + // We have multiple SpawnPoints, Route the agent to a random or sequential one + if (SpawnPointRouting == "random") + acd.startpos = spawnpoints[Util.RandomClass.Next(spawnpoints.Count) - 1].GetLocation( + telehub.AbsolutePosition, + telehub.GroupRotation + ); + else + acd.startpos = spawnpoints[SpawnPoint()].GetLocation( + telehub.AbsolutePosition, + telehub.GroupRotation + ); + } + else if (spawnpoints.Count == 1) + { + // We have a single SpawnPoint and will route the agent to it + acd.startpos = spawnpoints[0].GetLocation(telehub.AbsolutePosition, telehub.GroupRotation); + } else - acd.startpos = spawnpoints[SpawnPoint()].GetLocation( - telehub.AbsolutePosition, - telehub.GroupRotation - ); + { + m_log.DebugFormat( + "[SCENE]: No spawnpoints defined for telehub {0} for {1} in {2}. Continuing.", + RegionInfo.RegionSettings.TelehubObject, acd.Name, Name); + } } else { - // We have a single SpawnPoint and will route the agent to it - acd.startpos = spawnpoints[0].GetLocation(telehub.AbsolutePosition, telehub.GroupRotation); + m_log.DebugFormat( + "[SCENE]: No telehub {0} found to direct {1} in {2}. Continuing.", + RegionInfo.RegionSettings.TelehubObject, acd.Name, Name); } return true; diff --git a/OpenSim/Region/Framework/Scenes/Tests/SceneTelehubTests.cs b/OpenSim/Region/Framework/Scenes/Tests/SceneTelehubTests.cs new file mode 100644 index 0000000000..9a97acc176 --- /dev/null +++ b/OpenSim/Region/Framework/Scenes/Tests/SceneTelehubTests.cs @@ -0,0 +1,119 @@ +/* + * 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 OpenSimulator 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 Nini.Config; +using NUnit.Framework; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Region.CoreModules.World.Estate; +using OpenSim.Region.Framework.Scenes; +using OpenSim.Region.Framework.Interfaces; +using OpenSim.Services.Interfaces; +using OpenSim.Tests.Common; +using OpenSim.Tests.Common.Mock; + +namespace OpenSim.Region.Framework.Scenes.Tests +{ + /// + /// Scene telehub tests + /// + /// + /// TODO: Tests which run through normal functionality. Currently, the only test is one that checks behaviour + /// in the case of an error condition + /// + [TestFixture] + public class SceneTelehubTests : OpenSimTestCase + { + /// + /// Test for desired behaviour when a telehub has no spawn points + /// + [Test] + public void TestNoTelehubSpawnPoints() + { + TestHelpers.InMethod(); +// TestHelpers.EnableLogging(); + + EstateManagementModule emm = new EstateManagementModule(); + + SceneHelpers sh = new SceneHelpers(); + Scene scene = sh.SetupScene(); + SceneHelpers.SetupSceneModules(scene, emm); + + UUID telehubSceneObjectOwner = TestHelpers.ParseTail(0x1); + + SceneObjectGroup telehubSo = SceneHelpers.AddSceneObject(scene, "telehubObject", telehubSceneObjectOwner); + + emm.HandleOnEstateManageTelehub(null, UUID.Zero, UUID.Zero, "connect", telehubSo.LocalId); + scene.RegionInfo.EstateSettings.AllowDirectTeleport = false; + + // Must still be possible to successfully log in + UUID loggingInUserId = TestHelpers.ParseTail(0x2); + + UserAccount ua + = UserAccountHelpers.CreateUserWithInventory(scene, "Test", "User", loggingInUserId, "password"); + + SceneHelpers.AddScenePresence(scene, ua); + + Assert.That(scene.GetScenePresence(loggingInUserId), Is.Not.Null); + } + + /// + /// Test for desired behaviour when the scene object nominated as a telehub object does not exist. + /// + [Test] + public void TestNoTelehubSceneObject() + { + TestHelpers.InMethod(); +// TestHelpers.EnableLogging(); + + EstateManagementModule emm = new EstateManagementModule(); + + SceneHelpers sh = new SceneHelpers(); + Scene scene = sh.SetupScene(); + SceneHelpers.SetupSceneModules(scene, emm); + + UUID telehubSceneObjectOwner = TestHelpers.ParseTail(0x1); + + SceneObjectGroup telehubSo = SceneHelpers.AddSceneObject(scene, "telehubObject", telehubSceneObjectOwner); + SceneObjectGroup spawnPointSo = SceneHelpers.AddSceneObject(scene, "spawnpointObject", telehubSceneObjectOwner); + + emm.HandleOnEstateManageTelehub(null, UUID.Zero, UUID.Zero, "connect", telehubSo.LocalId); + emm.HandleOnEstateManageTelehub(null, UUID.Zero, UUID.Zero, "spawnpoint add", spawnPointSo.LocalId); + scene.RegionInfo.EstateSettings.AllowDirectTeleport = false; + + scene.DeleteSceneObject(telehubSo, false); + + // Must still be possible to successfully log in + UUID loggingInUserId = TestHelpers.ParseTail(0x2); + + UserAccount ua + = UserAccountHelpers.CreateUserWithInventory(scene, "Test", "User", loggingInUserId, "password"); + + SceneHelpers.AddScenePresence(scene, ua); + + Assert.That(scene.GetScenePresence(loggingInUserId), Is.Not.Null); + } + } +} \ No newline at end of file diff --git a/OpenSim/Tests/Common/Mock/TestClient.cs b/OpenSim/Tests/Common/Mock/TestClient.cs index 937010285f..a4247e303a 100644 --- a/OpenSim/Tests/Common/Mock/TestClient.cs +++ b/OpenSim/Tests/Common/Mock/TestClient.cs @@ -788,11 +788,6 @@ namespace OpenSim.Tests.Common.Mock { OnRegionHandShakeReply(this); } - - if (OnCompleteMovementToRegion != null) - { - OnCompleteMovementToRegion(this, true); - } } public void SendAssetUploadCompleteMessage(sbyte AssetType, bool Success, UUID AssetFullID) diff --git a/OpenSim/Tests/Common/TestHelpers.cs b/OpenSim/Tests/Common/TestHelpers.cs index a684d72e7f..6bf23f8e67 100644 --- a/OpenSim/Tests/Common/TestHelpers.cs +++ b/OpenSim/Tests/Common/TestHelpers.cs @@ -117,8 +117,6 @@ namespace OpenSim.Tests.Common /// Parse a UUID stem into a full UUID. ///
/// - /// Yes, this is completely inconsistent with ParseTail but this is probably a better way to do it, - /// UUIDs are conceptually not hexadecmial numbers. /// The fragment will come at the start of the UUID. The rest will be 0s /// /// @@ -143,5 +141,24 @@ namespace OpenSim.Tests.Common { return new UUID(string.Format("00000000-0000-0000-0000-{0:X12}", tail)); } + + /// + /// Parse a UUID tail section into a full UUID. + /// + /// + /// The fragment will come at the end of the UUID. The rest will be 0s + /// + /// + /// + /// A UUID fragment that will be parsed into a full UUID. Therefore, it can only contain + /// cahracters which are valid in a UUID, except for "-" which is currently only allowed if a full UUID is + /// given as the 'fragment'. + /// + public static UUID ParseTail(string stem) + { + string rawUuid = stem.PadLeft(32, '0'); + + return UUID.Parse(rawUuid); + } } } From 2e78e89c36e661f72773e54f97bec3f04af67b79 Mon Sep 17 00:00:00 2001 From: Mic Bowman Date: Mon, 20 Jan 2014 11:33:49 -0800 Subject: [PATCH 11/15] Clean up orphaned json stores. This can happen when an object is removed, when a script is removed, or when a script is reset. Also added a stats command to track the number of json stores used by a region. Will probably add some more commands later. --- .../Framework/Interfaces/IJsonStoreModule.cs | 7 + .../Scripting/JsonStore/JsonStoreCommands.cs | 195 ++++++++++++++++++ .../Scripting/JsonStore/JsonStoreModule.cs | 41 +++- .../JsonStore/JsonStoreScriptModule.cs | 41 +++- 4 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 OpenSim/Region/OptionalModules/Scripting/JsonStore/JsonStoreCommands.cs diff --git a/OpenSim/Region/Framework/Interfaces/IJsonStoreModule.cs b/OpenSim/Region/Framework/Interfaces/IJsonStoreModule.cs index b67312e0b4..1a897219df 100644 --- a/OpenSim/Region/Framework/Interfaces/IJsonStoreModule.cs +++ b/OpenSim/Region/Framework/Interfaces/IJsonStoreModule.cs @@ -51,10 +51,17 @@ namespace OpenSim.Region.Framework.Interfaces UUID = 5 } + public struct JsonStoreStats + { + public int StoreCount; + } + public delegate void TakeValueCallback(string s); public interface IJsonStoreModule { + JsonStoreStats GetStoreStats(); + bool AttachObjectStore(UUID objectID); bool CreateStore(string value, ref UUID result); bool DestroyStore(UUID storeID); diff --git a/OpenSim/Region/OptionalModules/Scripting/JsonStore/JsonStoreCommands.cs b/OpenSim/Region/OptionalModules/Scripting/JsonStore/JsonStoreCommands.cs new file mode 100644 index 0000000000..d4b19ddd91 --- /dev/null +++ b/OpenSim/Region/OptionalModules/Scripting/JsonStore/JsonStoreCommands.cs @@ -0,0 +1,195 @@ +/* + * Copyright (c) Contributors + * 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 Mono.Addins; + +using System; +using System.Reflection; +using System.Threading; +using System.Text; +using System.Net; +using System.Net.Sockets; +using log4net; +using Nini.Config; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; +using OpenSim.Region.Framework.Interfaces; +using OpenSim.Region.Framework.Scenes; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace OpenSim.Region.OptionalModules.Scripting.JsonStore +{ + [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "JsonStoreCommandsModule")] + + public class JsonStoreCommandsModule : INonSharedRegionModule + { + private static readonly ILog m_log = + LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private IConfig m_config = null; + private bool m_enabled = false; + + private Scene m_scene = null; + //private IJsonStoreModule m_store; + private JsonStoreModule m_store; + +#region Region Module interface + + // ----------------------------------------------------------------- + /// + /// Name of this shared module is it's class name + /// + // ----------------------------------------------------------------- + public string Name + { + get { return this.GetType().Name; } + } + + // ----------------------------------------------------------------- + /// + /// Initialise this shared module + /// + /// this region is getting initialised + /// nini config, we are not using this + // ----------------------------------------------------------------- + public void Initialise(IConfigSource config) + { + try + { + if ((m_config = config.Configs["JsonStore"]) == null) + { + // There is no configuration, the module is disabled + // m_log.InfoFormat("[JsonStore] no configuration info"); + return; + } + + m_enabled = m_config.GetBoolean("Enabled", m_enabled); + } + catch (Exception e) + { + m_log.Error("[JsonStore]: initialization error: {0}", e); + return; + } + + if (m_enabled) + m_log.DebugFormat("[JsonStore]: module is enabled"); + } + + // ----------------------------------------------------------------- + /// + /// everything is loaded, perform post load configuration + /// + // ----------------------------------------------------------------- + public void PostInitialise() + { + } + + // ----------------------------------------------------------------- + /// + /// Nothing to do on close + /// + // ----------------------------------------------------------------- + public void Close() + { + } + + // ----------------------------------------------------------------- + /// + /// + // ----------------------------------------------------------------- + public void AddRegion(Scene scene) + { + if (m_enabled) + { + m_scene = scene; + + } + } + + // ----------------------------------------------------------------- + /// + /// + // ----------------------------------------------------------------- + public void RemoveRegion(Scene scene) + { + // need to remove all references to the scene in the subscription + // list to enable full garbage collection of the scene object + } + + // ----------------------------------------------------------------- + /// + /// Called when all modules have been added for a region. This is + /// where we hook up events + /// + // ----------------------------------------------------------------- + public void RegionLoaded(Scene scene) + { + if (m_enabled) + { + m_scene = scene; + + m_store = (JsonStoreModule) m_scene.RequestModuleInterface(); + if (m_store == null) + { + m_log.ErrorFormat("[JsonStoreCommands]: JsonModule interface not defined"); + m_enabled = false; + return; + } + + scene.AddCommand("JsonStore", this, "jsonstore stats", "jsonstore stats", + "Display statistics about the state of the JsonStore module", "", + CmdStats); + } + } + + /// ----------------------------------------------------------------- + /// + /// + // ----------------------------------------------------------------- + public Type ReplaceableInterface + { + get { return null; } + } + +#endregion + +#region Commands + + private void CmdStats(string module, string[] cmd) + { + if (MainConsole.Instance.ConsoleScene != m_scene && MainConsole.Instance.ConsoleScene != null) + return; + + JsonStoreStats stats = m_store.GetStoreStats(); + MainConsole.Instance.OutputFormat("{0}\t{1}",m_scene.RegionInfo.RegionName,stats.StoreCount); + } + +#endregion + + } +} diff --git a/OpenSim/Region/OptionalModules/Scripting/JsonStore/JsonStoreModule.cs b/OpenSim/Region/OptionalModules/Scripting/JsonStore/JsonStoreModule.cs index 5fbfcc583a..b502a55508 100644 --- a/OpenSim/Region/OptionalModules/Scripting/JsonStore/JsonStoreModule.cs +++ b/OpenSim/Region/OptionalModules/Scripting/JsonStore/JsonStoreModule.cs @@ -42,7 +42,6 @@ using OpenSim.Region.Framework.Scenes; using System.Collections.Generic; using System.Text.RegularExpressions; - namespace OpenSim.Region.OptionalModules.Scripting.JsonStore { [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "JsonStoreModule")] @@ -60,6 +59,7 @@ namespace OpenSim.Region.OptionalModules.Scripting.JsonStore private Scene m_scene = null; private Dictionary m_JsonValueStore; + private UUID m_sharedStore; #region Region Module interface @@ -140,6 +140,8 @@ namespace OpenSim.Region.OptionalModules.Scripting.JsonStore m_sharedStore = UUID.Zero; m_JsonValueStore = new Dictionary(); m_JsonValueStore.Add(m_sharedStore,new JsonStore("")); + + scene.EventManager.OnObjectBeingRemovedFromScene += EventManagerOnObjectBeingRemovedFromScene; } } @@ -149,6 +151,8 @@ namespace OpenSim.Region.OptionalModules.Scripting.JsonStore // ----------------------------------------------------------------- public void RemoveRegion(Scene scene) { + scene.EventManager.OnObjectBeingRemovedFromScene -= EventManagerOnObjectBeingRemovedFromScene; + // need to remove all references to the scene in the subscription // list to enable full garbage collection of the scene object } @@ -161,7 +165,9 @@ namespace OpenSim.Region.OptionalModules.Scripting.JsonStore // ----------------------------------------------------------------- public void RegionLoaded(Scene scene) { - if (m_enabled) {} + if (m_enabled) + { + } } /// ----------------------------------------------------------------- @@ -175,8 +181,39 @@ namespace OpenSim.Region.OptionalModules.Scripting.JsonStore #endregion +#region SceneEvents + // ----------------------------------------------------------------- + /// + /// + /// + // ----------------------------------------------------------------- + public void EventManagerOnObjectBeingRemovedFromScene(SceneObjectGroup obj) + { + obj.ForEachPart(delegate(SceneObjectPart sop) { DestroyStore(sop.UUID); } ); + } + +#endregion + #region ScriptInvocationInteface + + // ----------------------------------------------------------------- + /// + /// + /// + // ----------------------------------------------------------------- + public JsonStoreStats GetStoreStats() + { + JsonStoreStats stats; + + lock (m_JsonValueStore) + { + stats.StoreCount = m_JsonValueStore.Count; + } + + return stats; + } + // ----------------------------------------------------------------- /// /// diff --git a/OpenSim/Region/OptionalModules/Scripting/JsonStore/JsonStoreScriptModule.cs b/OpenSim/Region/OptionalModules/Scripting/JsonStore/JsonStoreScriptModule.cs index 1bb5aee796..9fbfb66b8e 100644 --- a/OpenSim/Region/OptionalModules/Scripting/JsonStore/JsonStoreScriptModule.cs +++ b/OpenSim/Region/OptionalModules/Scripting/JsonStore/JsonStoreScriptModule.cs @@ -59,7 +59,9 @@ namespace OpenSim.Region.OptionalModules.Scripting.JsonStore private IScriptModuleComms m_comms; private IJsonStoreModule m_store; - + + private Dictionary> m_scriptStores = new Dictionary>(); + #region Region Module interface // ----------------------------------------------------------------- @@ -126,6 +128,8 @@ namespace OpenSim.Region.OptionalModules.Scripting.JsonStore // ----------------------------------------------------------------- public void AddRegion(Scene scene) { + scene.EventManager.OnScriptReset += HandleScriptReset; + scene.EventManager.OnRemoveScript += HandleScriptReset; } // ----------------------------------------------------------------- @@ -134,10 +138,32 @@ namespace OpenSim.Region.OptionalModules.Scripting.JsonStore // ----------------------------------------------------------------- public void RemoveRegion(Scene scene) { + scene.EventManager.OnScriptReset -= HandleScriptReset; + scene.EventManager.OnRemoveScript -= HandleScriptReset; + // need to remove all references to the scene in the subscription // list to enable full garbage collection of the scene object } + // ----------------------------------------------------------------- + /// + /// + // ----------------------------------------------------------------- + private void HandleScriptReset(uint localID, UUID itemID) + { + HashSet stores; + + lock (m_scriptStores) + { + if (! m_scriptStores.TryGetValue(itemID, out stores)) + return; + m_scriptStores.Remove(itemID); + } + + foreach (UUID id in stores) + m_store.DestroyStore(id); + } + // ----------------------------------------------------------------- /// /// Called when all modules have been added for a region. This is @@ -250,6 +276,13 @@ namespace OpenSim.Region.OptionalModules.Scripting.JsonStore if (! m_store.CreateStore(value, ref uuid)) GenerateRuntimeError("Failed to create Json store"); + lock (m_scriptStores) + { + if (! m_scriptStores.ContainsKey(scriptID)) + m_scriptStores[scriptID] = new HashSet(); + + m_scriptStores[scriptID].Add(uuid); + } return uuid; } @@ -261,6 +294,12 @@ namespace OpenSim.Region.OptionalModules.Scripting.JsonStore [ScriptInvocation] public int JsonDestroyStore(UUID hostID, UUID scriptID, UUID storeID) { + lock(m_scriptStores) + { + if (m_scriptStores.ContainsKey(scriptID)) + m_scriptStores[scriptID].Remove(storeID); + } + return m_store.DestroyStore(storeID) ? 1 : 0; } From 1cae3664a52fe48965954afc19804b11720c4add Mon Sep 17 00:00:00 2001 From: dahlia Date: Mon, 20 Jan 2014 11:53:33 -0800 Subject: [PATCH 12/15] add null texture entry face check before converting legacy materials --- OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs index ce2a56abda..c4bc8a025e 100644 --- a/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs +++ b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs @@ -199,7 +199,7 @@ namespace OpenSim.Region.OptionalModules.Materials bool used = false; foreach (var face in te.FaceTextures) - if (face.MaterialID == id) + if (face != null && face.MaterialID == id) used = true; if (used) @@ -207,7 +207,7 @@ namespace OpenSim.Region.OptionalModules.Materials var newId = StoreMaterialAsAsset(part.CreatorID, material, part); foreach (var face in te.FaceTextures) - if (face.MaterialID == id) + if (face != null && face.MaterialID == id) face.MaterialID = newId; } } From af58631f00b95081dc99f4f75e8ec6b031b8cf2a Mon Sep 17 00:00:00 2001 From: dahlia Date: Mon, 20 Jan 2014 13:57:14 -0800 Subject: [PATCH 13/15] rather than converting existing materials to assets, just retrieve them and make them available for viewing. Any new materials added to the scene will become assets. --- .../Materials/MaterialsModule.cs | 44 +++++-------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs index c4bc8a025e..afb788b8ae 100644 --- a/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs +++ b/OpenSim/Region/OptionalModules/Materials/MaterialsModule.cs @@ -149,12 +149,10 @@ namespace OpenSim.Region.OptionalModules.Materials } /// - /// Searches the part for any legacy materials stored in DynAttrs and converts them to assets, replacing - /// the MaterialIDs in the TextureEntries for the part. - /// Deletes the legacy materials from the part as they are no longer needed. + /// Finds any legacy materials stored in DynAttrs that may exist for this part and add them to 'm_regionMaterials'. /// /// - private void ConvertLegacyMaterialsInPart(SceneObjectPart part) + private void GetLegacyStoredMaterialsInPart(SceneObjectPart part) { if (part.DynAttrs == null) return; @@ -183,10 +181,6 @@ namespace OpenSim.Region.OptionalModules.Materials if (matsArr == null) return; - var te = new Primitive.TextureEntry(part.Shape.TextureEntry, 0, part.Shape.TextureEntry.Length); - if (te == null) - return; - foreach (OSD elemOsd in matsArr) { if (elemOsd != null && elemOsd is OSDMap) @@ -194,32 +188,18 @@ namespace OpenSim.Region.OptionalModules.Materials OSDMap matMap = elemOsd as OSDMap; if (matMap.ContainsKey("ID") && matMap.ContainsKey("Material")) { - UUID id = matMap["ID"].AsUUID(); - OSDMap material = (OSDMap)matMap["Material"]; - bool used = false; - - foreach (var face in te.FaceTextures) - if (face != null && face.MaterialID == id) - used = true; - - if (used) - { // store legacy material in new asset format, and update the part texture entry with the new hashed UUID - - var newId = StoreMaterialAsAsset(part.CreatorID, material, part); - foreach (var face in te.FaceTextures) - if (face != null && face.MaterialID == id) - face.MaterialID = newId; + try + { + lock (m_regionMaterials) + m_regionMaterials[matMap["ID"].AsUUID()] = (OSDMap)matMap["Material"]; + } + catch (Exception e) + { + m_log.Warn("[Materials]: exception decoding persisted legacy material: " + e.ToString()); } } } } - - part.Shape.TextureEntry = te.GetBytes(); - part.ParentGroup.HasGroupChanged = true; - part.ScheduleFullUpdate(); - - lock (part.DynAttrs) - part.DynAttrs.RemoveStore("OpenSim", "Materials"); } /// @@ -230,12 +210,12 @@ namespace OpenSim.Region.OptionalModules.Materials if (part.Shape == null) return; - ConvertLegacyMaterialsInPart(part); - var te = new Primitive.TextureEntry(part.Shape.TextureEntry, 0, part.Shape.TextureEntry.Length); if (te == null) return; + GetLegacyStoredMaterialsInPart(part); + GetStoredMaterialInFace(part, te.DefaultTexture); foreach (Primitive.TextureEntryFace face in te.FaceTextures) From 7bd42fc42f0d945fe96b058d06f14c091d96b2d2 Mon Sep 17 00:00:00 2001 From: dahlia Date: Mon, 20 Jan 2014 15:01:18 -0800 Subject: [PATCH 14/15] Add back code to UuidGatherer to retrieve UUIDs for materials stored in DynAttrs. This is unfortunately still necessary until a better solution for handling existing legacy materials can be implemented --- .../Region/Framework/Scenes/UuidGatherer.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/OpenSim/Region/Framework/Scenes/UuidGatherer.cs b/OpenSim/Region/Framework/Scenes/UuidGatherer.cs index 42a19775ba..75a51b5e68 100644 --- a/OpenSim/Region/Framework/Scenes/UuidGatherer.cs +++ b/OpenSim/Region/Framework/Scenes/UuidGatherer.cs @@ -218,6 +218,10 @@ namespace OpenSim.Region.Framework.Scenes // inventory transfer. There needs to be a way for a module to register a method without assuming a // Scene.EventManager is present. // part.ParentGroup.Scene.EventManager.TriggerGatherUuids(part, assetUuids); + + + // still needed to retrieve textures used as materials for any parts containing legacy materials stored in DynAttrs + GatherMaterialsUuids(part, assetUuids); } catch (Exception e) { @@ -241,6 +245,75 @@ namespace OpenSim.Region.Framework.Scenes // Monitor.Pulse(this); // } // } + + /// + /// Gather all of the texture asset UUIDs used to reference "Materials" such as normal and specular maps + /// stored in legacy format in part.DynAttrs + /// + /// + /// + //public void GatherMaterialsUuids(SceneObjectPart part, IDictionary assetUuids) + public void GatherMaterialsUuids(SceneObjectPart part, IDictionary assetUuids) + { + // scan thru the dynAttrs map of this part for any textures used as materials + OSD osdMaterials = null; + + lock (part.DynAttrs) + { + if (part.DynAttrs.ContainsStore("OpenSim", "Materials")) + { + OSDMap materialsStore = part.DynAttrs.GetStore("OpenSim", "Materials"); + + if (materialsStore == null) + return; + + materialsStore.TryGetValue("Materials", out osdMaterials); + } + + if (osdMaterials != null) + { + //m_log.Info("[UUID Gatherer]: found Materials: " + OSDParser.SerializeJsonString(osd)); + + if (osdMaterials is OSDArray) + { + OSDArray matsArr = osdMaterials as OSDArray; + foreach (OSDMap matMap in matsArr) + { + try + { + if (matMap.ContainsKey("Material")) + { + OSDMap mat = matMap["Material"] as OSDMap; + if (mat.ContainsKey("NormMap")) + { + UUID normalMapId = mat["NormMap"].AsUUID(); + if (normalMapId != UUID.Zero) + { + assetUuids[normalMapId] = (sbyte)AssetType.Texture; + //m_log.Info("[UUID Gatherer]: found normal map ID: " + normalMapId.ToString()); + } + } + if (mat.ContainsKey("SpecMap")) + { + UUID specularMapId = mat["SpecMap"].AsUUID(); + if (specularMapId != UUID.Zero) + { + assetUuids[specularMapId] = (sbyte)AssetType.Texture; + //m_log.Info("[UUID Gatherer]: found specular map ID: " + specularMapId.ToString()); + } + } + } + + } + catch (Exception e) + { + m_log.Warn("[UUID Gatherer]: exception getting materials: " + e.Message); + } + } + } + } + } + } /// /// Get an asset synchronously, potentially using an asynchronous callback. If the From 83626e60e69ac0534faffa40f9e79a5d3ae0d332 Mon Sep 17 00:00:00 2001 From: Mic Bowman Date: Mon, 20 Jan 2014 18:59:43 -0800 Subject: [PATCH 15/15] Adds a configuration option to cannibalize bandwidth from the udp texture throttle and move it to the task throttle. Since most viewers are using http textures, the udp texture throttle is holding onto bw that could be used for more responsive prims updates. See the documentation for CannibalizeTextureRate in OpenSimDefaults.ini. Option is disabled by default. --- .../Region/ClientStack/Linden/UDP/LLUDPClient.cs | 14 ++++++++++++++ .../Region/ClientStack/Linden/UDP/ThrottleRates.cs | 6 ++++++ bin/OpenSimDefaults.ini | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/OpenSim/Region/ClientStack/Linden/UDP/LLUDPClient.cs b/OpenSim/Region/ClientStack/Linden/UDP/LLUDPClient.cs index 202cc625a2..51433cbbc3 100644 --- a/OpenSim/Region/ClientStack/Linden/UDP/LLUDPClient.cs +++ b/OpenSim/Region/ClientStack/Linden/UDP/LLUDPClient.cs @@ -162,6 +162,12 @@ namespace OpenSim.Region.ClientStack.LindenUDP private int m_defaultRTO = 1000; // 1sec is the recommendation in the RFC private int m_maxRTO = 60000; + /// + /// This is the percentage of the udp texture queue to add to the task queue since + /// textures are now generally handled through http. + /// + private double m_cannibalrate = 0.0; + private ClientInfo m_info = new ClientInfo(); /// @@ -201,6 +207,8 @@ namespace OpenSim.Region.ClientStack.LindenUDP // Create an array of token buckets for this clients different throttle categories m_throttleCategories = new TokenBucket[THROTTLE_CATEGORY_COUNT]; + m_cannibalrate = rates.CannibalizeTextureRate; + for (int i = 0; i < THROTTLE_CATEGORY_COUNT; i++) { ThrottleOutPacketType type = (ThrottleOutPacketType)i; @@ -349,6 +357,12 @@ namespace OpenSim.Region.ClientStack.LindenUDP texture = Math.Max(texture, LLUDPServer.MTU); asset = Math.Max(asset, LLUDPServer.MTU); + // Since most textures are now delivered through http, make it possible + // to cannibalize some of the bw from the texture throttle to use for + // the task queue (e.g. object updates) + task = task + (int)(m_cannibalrate * texture); + texture = (int)((1 - m_cannibalrate) * texture); + //int total = resend + land + wind + cloud + task + texture + asset; //m_log.DebugFormat("[LLUDPCLIENT]: {0} is setting throttles. Resend={1}, Land={2}, Wind={3}, Cloud={4}, Task={5}, Texture={6}, Asset={7}, Total={8}", // AgentID, resend, land, wind, cloud, task, texture, asset, total); diff --git a/OpenSim/Region/ClientStack/Linden/UDP/ThrottleRates.cs b/OpenSim/Region/ClientStack/Linden/UDP/ThrottleRates.cs index c9aac0ba09..e5bae6ee6c 100644 --- a/OpenSim/Region/ClientStack/Linden/UDP/ThrottleRates.cs +++ b/OpenSim/Region/ClientStack/Linden/UDP/ThrottleRates.cs @@ -59,6 +59,9 @@ namespace OpenSim.Region.ClientStack.LindenUDP /// Flag used to enable adaptive throttles public bool AdaptiveThrottlesEnabled; + /// Amount of the texture throttle to steal for the task throttle + public double CannibalizeTextureRate; + /// /// Default constructor /// @@ -80,6 +83,9 @@ namespace OpenSim.Region.ClientStack.LindenUDP Total = throttleConfig.GetInt("client_throttle_max_bps", 0); AdaptiveThrottlesEnabled = throttleConfig.GetBoolean("enable_adaptive_throttles", false); + + CannibalizeTextureRate = (double)throttleConfig.GetFloat("CannibalizeTextureRate", 0.0f); + CannibalizeTextureRate = Util.Clamp(CannibalizeTextureRate,0.0, 0.9); } catch (Exception) { } } diff --git a/bin/OpenSimDefaults.ini b/bin/OpenSimDefaults.ini index ae7b7947d8..0da99ba5d0 100644 --- a/bin/OpenSimDefaults.ini +++ b/bin/OpenSimDefaults.ini @@ -539,6 +539,16 @@ ; ;TextureSendLimit = 20 + ; CannibalizeTextureRate allows bandwidth to be moved from the + ; UDP texture throttle to the task throttle. Since most viewers + ; use HTTP textures, this provides a means of using what is largely + ; unused bandwidth in the total throttle. The value is the proportion + ; of the texture rate to move to the task queue. It must be between + ; 0.0 (none of the bandwidth is cannibalized) and 0.9 (90% of the + ; bandwidth is grabbed) + ; + ; CannibalizeTextureRate = 0.5 + ; Quash and remove any light properties from attachments not on the ; hands. This allows flashlights and lanterns to function, but kills ; silly vanity "Facelights" dead. Sorry, head mounted miner's lamps