aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorJustin Clark-Casey (justincc)2014-10-28 23:00:49 +0000
committerJustin Clark-Casey (justincc)2014-11-25 23:23:09 +0000
commit3a1ce2715a522dcb1971944af17ad10d2263c7ab (patch)
tree910296da502f599c8d019c88fdcea7128eff25be
parentAdd "wearables show" console command. (diff)
downloadopensim-SC_OLD-3a1ce2715a522dcb1971944af17ad10d2263c7ab.zip
opensim-SC_OLD-3a1ce2715a522dcb1971944af17ad10d2263c7ab.tar.gz
opensim-SC_OLD-3a1ce2715a522dcb1971944af17ad10d2263c7ab.tar.bz2
opensim-SC_OLD-3a1ce2715a522dcb1971944af17ad10d2263c7ab.tar.xz
Add "wearables check" console command
This checks that all the wearable assets and any assets for a given logged in avatar exist in the asset service
-rw-r--r--OpenSim/Region/Framework/Scenes/UuidGatherer.cs222
-rw-r--r--OpenSim/Region/OptionalModules/Avatar/Appearance/AppearanceInfoModule.cs96
2 files changed, 245 insertions, 73 deletions
diff --git a/OpenSim/Region/Framework/Scenes/UuidGatherer.cs b/OpenSim/Region/Framework/Scenes/UuidGatherer.cs
index d07cc6a..20ff5b5 100644
--- a/OpenSim/Region/Framework/Scenes/UuidGatherer.cs
+++ b/OpenSim/Region/Framework/Scenes/UuidGatherer.cs
@@ -72,6 +72,67 @@ namespace OpenSim.Region.Framework.Scenes
72 { 72 {
73 m_assetService = assetService; 73 m_assetService = assetService;
74 } 74 }
75
76 /// <summary>
77 /// Gather all the asset uuids associated with the asset referenced by a given uuid
78 /// </summary>
79 /// <remarks>
80 /// This includes both those directly associated with
81 /// it (e.g. face textures) and recursively, those of items within it's inventory (e.g. objects contained
82 /// within this object).
83 /// This method assumes that the asset type associated with this asset in persistent storage is correct (which
84 /// should always be the case). So with this method we always need to retrieve asset data even if the asset
85 /// is of a type which is known not to reference any other assets
86 /// </remarks>
87 /// <param name="assetUuid">The uuid of the asset for which to gather referenced assets</param>
88 /// <param name="assetUuids">The assets gathered</param>
89 public void GatherAssetUuids(UUID assetUuid, IDictionary<UUID, sbyte> assetUuids)
90 {
91 // avoid infinite loops
92 if (assetUuids.ContainsKey(assetUuid))
93 return;
94
95 try
96 {
97 AssetBase assetBase = GetAsset(assetUuid);
98
99 if (null != assetBase)
100 {
101 sbyte assetType = assetBase.Type;
102 assetUuids[assetUuid] = assetType;
103
104 if ((sbyte)AssetType.Bodypart == assetType || (sbyte)AssetType.Clothing == assetType)
105 {
106 GetWearableAssetUuids(assetBase, assetUuids);
107 }
108 else if ((sbyte)AssetType.Gesture == assetType)
109 {
110 GetGestureAssetUuids(assetBase, assetUuids);
111 }
112 else if ((sbyte)AssetType.Notecard == assetType)
113 {
114 GetTextEmbeddedAssetUuids(assetBase, assetUuids);
115 }
116 else if ((sbyte)AssetType.LSLText == assetType)
117 {
118 GetTextEmbeddedAssetUuids(assetBase, assetUuids);
119 }
120 else if ((sbyte)OpenSimAssetType.Material == assetType)
121 {
122 GetMaterialAssetUuids(assetBase, assetUuids);
123 }
124 else if ((sbyte)AssetType.Object == assetType)
125 {
126 GetSceneObjectAssetUuids(assetBase, assetUuids);
127 }
128 }
129 }
130 catch (Exception)
131 {
132 m_log.ErrorFormat("[UUID GATHERER]: Failed to gather uuids for asset id {0}", assetUuid);
133 throw;
134 }
135 }
75 136
76 /// <summary> 137 /// <summary>
77 /// Gather all the asset uuids associated with the asset referenced by a given uuid 138 /// Gather all the asset uuids associated with the asset referenced by a given uuid
@@ -246,19 +307,6 @@ namespace OpenSim.Region.Framework.Scenes
246 } 307 }
247 } 308 }
248 309
249// /// <summary>
250// /// The callback made when we request the asset for an object from the asset service.
251// /// </summary>
252// private void AssetReceived(string id, Object sender, AssetBase asset)
253// {
254// lock (this)
255// {
256// m_requestedObjectAsset = asset;
257// m_waitingForObjectAsset = false;
258// Monitor.Pulse(this);
259// }
260// }
261
262 /// <summary> 310 /// <summary>
263 /// Gather all of the texture asset UUIDs used to reference "Materials" such as normal and specular maps 311 /// Gather all of the texture asset UUIDs used to reference "Materials" such as normal and specular maps
264 /// stored in legacy format in part.DynAttrs 312 /// stored in legacy format in part.DynAttrs
@@ -362,32 +410,42 @@ namespace OpenSim.Region.Framework.Scenes
362 } 410 }
363 411
364 /// <summary> 412 /// <summary>
365 /// Record the asset uuids embedded within the given script. 413 /// Record the asset uuids embedded within the given text (e.g. a script).
366 /// </summary> 414 /// </summary>
367 /// <param name="scriptUuid"></param> 415 /// <param name="textAssetUuid"></param>
368 /// <param name="assetUuids">Dictionary in which to record the references</param> 416 /// <param name="assetUuids">Dictionary in which to record the references</param>
369 private void GetTextEmbeddedAssetUuids(UUID embeddingAssetId, IDictionary<UUID, sbyte> assetUuids) 417 private void GetTextEmbeddedAssetUuids(UUID textAssetUuid, IDictionary<UUID, sbyte> assetUuids)
370 { 418 {
371// m_log.DebugFormat("[ASSET GATHERER]: Getting assets for uuid references in asset {0}", embeddingAssetId); 419// m_log.DebugFormat("[ASSET GATHERER]: Getting assets for uuid references in asset {0}", embeddingAssetId);
372 420
373 AssetBase embeddingAsset = GetAsset(embeddingAssetId); 421 AssetBase textAsset = GetAsset(textAssetUuid);
374 422
375 if (null != embeddingAsset) 423 if (null != textAsset)
376 { 424 GetTextEmbeddedAssetUuids(textAsset, assetUuids);
377 string script = Utils.BytesToString(embeddingAsset.Data); 425 }
378// m_log.DebugFormat("[ARCHIVER]: Script {0}", script);
379 MatchCollection uuidMatches = Util.PermissiveUUIDPattern.Matches(script);
380// m_log.DebugFormat("[ARCHIVER]: Found {0} matches in text", uuidMatches.Count);
381 426
382 foreach (Match uuidMatch in uuidMatches) 427 /// <summary>
383 { 428 /// Record the asset uuids embedded within the given text (e.g. a script).
384 UUID uuid = new UUID(uuidMatch.Value); 429 /// </summary>
385// m_log.DebugFormat("[ARCHIVER]: Recording {0} in text", uuid); 430 /// <param name="textAsset"></param>
431 /// <param name="assetUuids">Dictionary in which to record the references</param>
432 private void GetTextEmbeddedAssetUuids(AssetBase textAsset, IDictionary<UUID, sbyte> assetUuids)
433 {
434 // m_log.DebugFormat("[ASSET GATHERER]: Getting assets for uuid references in asset {0}", embeddingAssetId);
386 435
387 // Embedded asset references (if not false positives) could be for many types of asset, so we will 436 string script = Utils.BytesToString(textAsset.Data);
388 // label these as unknown. 437 // m_log.DebugFormat("[ARCHIVER]: Script {0}", script);
389 assetUuids[uuid] = (sbyte)AssetType.Unknown; 438 MatchCollection uuidMatches = Util.PermissiveUUIDPattern.Matches(script);
390 } 439 // m_log.DebugFormat("[ARCHIVER]: Found {0} matches in text", uuidMatches.Count);
440
441 foreach (Match uuidMatch in uuidMatches)
442 {
443 UUID uuid = new UUID(uuidMatch.Value);
444 // m_log.DebugFormat("[ARCHIVER]: Recording {0} in text", uuid);
445
446 // Embedded asset references (if not false positives) could be for many types of asset, so we will
447 // label these as unknown.
448 assetUuids[uuid] = (sbyte)AssetType.Unknown;
391 } 449 }
392 } 450 }
393 451
@@ -401,18 +459,26 @@ namespace OpenSim.Region.Framework.Scenes
401 AssetBase assetBase = GetAsset(wearableAssetUuid); 459 AssetBase assetBase = GetAsset(wearableAssetUuid);
402 460
403 if (null != assetBase) 461 if (null != assetBase)
462 GetWearableAssetUuids(assetBase, assetUuids);
463 }
464
465 /// <summary>
466 /// Record the uuids referenced by the given wearable asset
467 /// </summary>
468 /// <param name="assetBase"></param>
469 /// <param name="assetUuids">Dictionary in which to record the references</param>
470 private void GetWearableAssetUuids(AssetBase assetBase, IDictionary<UUID, sbyte> assetUuids)
471 {
472 //m_log.Debug(new System.Text.ASCIIEncoding().GetString(bodypartAsset.Data));
473 AssetWearable wearableAsset = new AssetBodypart(assetBase.FullID, assetBase.Data);
474 wearableAsset.Decode();
475
476 //m_log.DebugFormat(
477 // "[ARCHIVER]: Wearable asset {0} references {1} assets", wearableAssetUuid, wearableAsset.Textures.Count);
478
479 foreach (UUID uuid in wearableAsset.Textures.Values)
404 { 480 {
405 //m_log.Debug(new System.Text.ASCIIEncoding().GetString(bodypartAsset.Data)); 481 assetUuids[uuid] = (sbyte)AssetType.Texture;
406 AssetWearable wearableAsset = new AssetBodypart(wearableAssetUuid, assetBase.Data);
407 wearableAsset.Decode();
408
409 //m_log.DebugFormat(
410 // "[ARCHIVER]: Wearable asset {0} references {1} assets", wearableAssetUuid, wearableAsset.Textures.Count);
411
412 foreach (UUID uuid in wearableAsset.Textures.Values)
413 {
414 assetUuids[uuid] = (sbyte)AssetType.Texture;
415 }
416 } 482 }
417 } 483 }
418 484
@@ -425,25 +491,35 @@ namespace OpenSim.Region.Framework.Scenes
425 /// <param name="assetUuids"></param> 491 /// <param name="assetUuids"></param>
426 private void GetSceneObjectAssetUuids(UUID sceneObjectUuid, IDictionary<UUID, sbyte> assetUuids) 492 private void GetSceneObjectAssetUuids(UUID sceneObjectUuid, IDictionary<UUID, sbyte> assetUuids)
427 { 493 {
428 AssetBase objectAsset = GetAsset(sceneObjectUuid); 494 AssetBase sceneObjectAsset = GetAsset(sceneObjectUuid);
429 495
430 if (null != objectAsset) 496 if (null != sceneObjectAsset)
497 GetSceneObjectAssetUuids(sceneObjectAsset, assetUuids);
498 }
499
500 /// <summary>
501 /// Get all the asset uuids associated with a given object. This includes both those directly associated with
502 /// it (e.g. face textures) and recursively, those of items within it's inventory (e.g. objects contained
503 /// within this object).
504 /// </summary>
505 /// <param name="sceneObjectAsset"></param>
506 /// <param name="assetUuids"></param>
507 private void GetSceneObjectAssetUuids(AssetBase sceneObjectAsset, IDictionary<UUID, sbyte> assetUuids)
508 {
509 string xml = Utils.BytesToString(sceneObjectAsset.Data);
510
511 CoalescedSceneObjects coa;
512 if (CoalescedSceneObjectsSerializer.TryFromXml(xml, out coa))
431 { 513 {
432 string xml = Utils.BytesToString(objectAsset.Data); 514 foreach (SceneObjectGroup sog in coa.Objects)
433 515 GatherAssetUuids(sog, assetUuids);
434 CoalescedSceneObjects coa; 516 }
435 if (CoalescedSceneObjectsSerializer.TryFromXml(xml, out coa)) 517 else
436 { 518 {
437 foreach (SceneObjectGroup sog in coa.Objects) 519 SceneObjectGroup sog = SceneObjectSerializer.FromOriginalXmlFormat(xml);
438 GatherAssetUuids(sog, assetUuids); 520
439 } 521 if (null != sog)
440 else 522 GatherAssetUuids(sog, assetUuids);
441 {
442 SceneObjectGroup sog = SceneObjectSerializer.FromOriginalXmlFormat(xml);
443
444 if (null != sog)
445 GatherAssetUuids(sog, assetUuids);
446 }
447 } 523 }
448 } 524 }
449 525
@@ -454,12 +530,22 @@ namespace OpenSim.Region.Framework.Scenes
454 /// <param name="assetUuids"></param> 530 /// <param name="assetUuids"></param>
455 private void GetGestureAssetUuids(UUID gestureUuid, IDictionary<UUID, sbyte> assetUuids) 531 private void GetGestureAssetUuids(UUID gestureUuid, IDictionary<UUID, sbyte> assetUuids)
456 { 532 {
457 AssetBase assetBase = GetAsset(gestureUuid); 533 AssetBase gestureAsset = GetAsset(gestureUuid);
458 if (null == assetBase) 534 if (null == gestureAsset)
459 return; 535 return;
460 536
461 using (MemoryStream ms = new MemoryStream(assetBase.Data)) 537 GetGestureAssetUuids(gestureAsset, assetUuids);
462 using (StreamReader sr = new StreamReader(ms)) 538 }
539
540 /// <summary>
541 /// Get the asset uuid associated with a gesture
542 /// </summary>
543 /// <param name="gestureAsset"></param>
544 /// <param name="assetUuids"></param>
545 private void GetGestureAssetUuids(AssetBase gestureAsset, IDictionary<UUID, sbyte> assetUuids)
546 {
547 using (MemoryStream ms = new MemoryStream(gestureAsset.Data))
548 using (StreamReader sr = new StreamReader(ms))
463 { 549 {
464 sr.ReadLine(); // Unknown (Version?) 550 sr.ReadLine(); // Unknown (Version?)
465 sr.ReadLine(); // Unknown 551 sr.ReadLine(); // Unknown
@@ -500,7 +586,15 @@ namespace OpenSim.Region.Framework.Scenes
500 if (null == assetBase) 586 if (null == assetBase)
501 return; 587 return;
502 588
503 OSDMap mat = (OSDMap)OSDParser.DeserializeLLSDXml(assetBase.Data); 589 GetMaterialAssetUuids(assetBase, assetUuids);
590 }
591
592 /// <summary>
593 /// Get the asset uuid's referenced in a material.
594 /// </summary>
595 private void GetMaterialAssetUuids(AssetBase materialAsset, IDictionary<UUID, sbyte> assetUuids)
596 {
597 OSDMap mat = (OSDMap)OSDParser.DeserializeLLSDXml(materialAsset.Data);
504 598
505 UUID normMap = mat["NormMap"].AsUUID(); 599 UUID normMap = mat["NormMap"].AsUUID();
506 if (normMap != UUID.Zero) 600 if (normMap != UUID.Zero)
diff --git a/OpenSim/Region/OptionalModules/Avatar/Appearance/AppearanceInfoModule.cs b/OpenSim/Region/OptionalModules/Avatar/Appearance/AppearanceInfoModule.cs
index 51dfd47..f67f613 100644
--- a/OpenSim/Region/OptionalModules/Avatar/Appearance/AppearanceInfoModule.cs
+++ b/OpenSim/Region/OptionalModules/Avatar/Appearance/AppearanceInfoModule.cs
@@ -51,7 +51,8 @@ namespace OpenSim.Region.OptionalModules.Avatar.Appearance
51 { 51 {
52// private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); 52// private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
53 53
54 private Dictionary<UUID, Scene> m_scenes = new Dictionary<UUID, Scene>(); 54 private List<Scene> m_scenes = new List<Scene>();
55
55// private IAvatarFactoryModule m_avatarFactory; 56// private IAvatarFactoryModule m_avatarFactory;
56 57
57 public string Name { get { return "Appearance Information Module"; } } 58 public string Name { get { return "Appearance Information Module"; } }
@@ -83,7 +84,7 @@ namespace OpenSim.Region.OptionalModules.Avatar.Appearance
83// m_log.DebugFormat("[APPEARANCE INFO MODULE]: REGION {0} REMOVED", scene.RegionInfo.RegionName); 84// m_log.DebugFormat("[APPEARANCE INFO MODULE]: REGION {0} REMOVED", scene.RegionInfo.RegionName);
84 85
85 lock (m_scenes) 86 lock (m_scenes)
86 m_scenes.Remove(scene.RegionInfo.RegionID); 87 m_scenes.Remove(scene);
87 } 88 }
88 89
89 public void RegionLoaded(Scene scene) 90 public void RegionLoaded(Scene scene)
@@ -91,7 +92,7 @@ namespace OpenSim.Region.OptionalModules.Avatar.Appearance
91// m_log.DebugFormat("[APPEARANCE INFO MODULE]: REGION {0} LOADED", scene.RegionInfo.RegionName); 92// m_log.DebugFormat("[APPEARANCE INFO MODULE]: REGION {0} LOADED", scene.RegionInfo.RegionName);
92 93
93 lock (m_scenes) 94 lock (m_scenes)
94 m_scenes[scene.RegionInfo.RegionID] = scene; 95 m_scenes.Add(scene);
95 96
96 scene.AddCommand( 97 scene.AddCommand(
97 "Users", this, "show appearance", 98 "Users", this, "show appearance",
@@ -140,6 +141,13 @@ namespace OpenSim.Region.OptionalModules.Avatar.Appearance
140 "If no avatar name is given then a general summary for all avatars in the scene is shown.\n" 141 "If no avatar name is given then a general summary for all avatars in the scene is shown.\n"
141 + "If an avatar name is given then specific information about current wearables is shown.", 142 + "If an avatar name is given then specific information about current wearables is shown.",
142 HandleShowWearablesCommand); 143 HandleShowWearablesCommand);
144
145 scene.AddCommand(
146 "Users", this, "wearables check",
147 "wearables check <first-name> <last-name>",
148 "Check that the wearables of a given avatar in the scene are valid.",
149 "This currently checks that the wearable assets themselves and any assets referenced by them exist.",
150 HandleCheckWearablesCommand);
143 } 151 }
144 152
145 private void HandleSendAppearanceCommand(string module, string[] cmd) 153 private void HandleSendAppearanceCommand(string module, string[] cmd)
@@ -163,7 +171,7 @@ namespace OpenSim.Region.OptionalModules.Avatar.Appearance
163 171
164 lock (m_scenes) 172 lock (m_scenes)
165 { 173 {
166 foreach (Scene scene in m_scenes.Values) 174 foreach (Scene scene in m_scenes)
167 { 175 {
168 if (targetNameSupplied) 176 if (targetNameSupplied)
169 { 177 {
@@ -215,7 +223,7 @@ namespace OpenSim.Region.OptionalModules.Avatar.Appearance
215 223
216 lock (m_scenes) 224 lock (m_scenes)
217 { 225 {
218 foreach (Scene scene in m_scenes.Values) 226 foreach (Scene scene in m_scenes)
219 { 227 {
220 if (targetNameSupplied) 228 if (targetNameSupplied)
221 { 229 {
@@ -251,7 +259,7 @@ namespace OpenSim.Region.OptionalModules.Avatar.Appearance
251 259
252 lock (m_scenes) 260 lock (m_scenes)
253 { 261 {
254 foreach (Scene scene in m_scenes.Values) 262 foreach (Scene scene in m_scenes)
255 { 263 {
256 ScenePresence sp = scene.GetScenePresence(firstname, lastname); 264 ScenePresence sp = scene.GetScenePresence(firstname, lastname);
257 if (sp != null && !sp.IsChildAgent) 265 if (sp != null && !sp.IsChildAgent)
@@ -285,7 +293,7 @@ namespace OpenSim.Region.OptionalModules.Avatar.Appearance
285 293
286 lock (m_scenes) 294 lock (m_scenes)
287 { 295 {
288 foreach (Scene scene in m_scenes.Values) 296 foreach (Scene scene in m_scenes)
289 { 297 {
290 scene.ForEachRootScenePresence( 298 scene.ForEachRootScenePresence(
291 sp => 299 sp =>
@@ -338,7 +346,7 @@ namespace OpenSim.Region.OptionalModules.Avatar.Appearance
338 { 346 {
339 lock (m_scenes) 347 lock (m_scenes)
340 { 348 {
341 foreach (Scene scene in m_scenes.Values) 349 foreach (Scene scene in m_scenes)
342 { 350 {
343 ScenePresence sp = scene.GetScenePresence(optionalTargetFirstName, optionalTargetLastName); 351 ScenePresence sp = scene.GetScenePresence(optionalTargetFirstName, optionalTargetLastName);
344 if (sp != null && !sp.IsChildAgent) 352 if (sp != null && !sp.IsChildAgent)
@@ -354,7 +362,7 @@ namespace OpenSim.Region.OptionalModules.Avatar.Appearance
354 362
355 lock (m_scenes) 363 lock (m_scenes)
356 { 364 {
357 foreach (Scene scene in m_scenes.Values) 365 foreach (Scene scene in m_scenes)
358 { 366 {
359 scene.ForEachRootScenePresence( 367 scene.ForEachRootScenePresence(
360 sp => 368 sp =>
@@ -376,6 +384,76 @@ namespace OpenSim.Region.OptionalModules.Avatar.Appearance
376 MainConsole.Instance.Output(sb.ToString()); 384 MainConsole.Instance.Output(sb.ToString());
377 } 385 }
378 386
387 private void HandleCheckWearablesCommand(string module, string[] cmd)
388 {
389 if (cmd.Length != 4)
390 {
391 MainConsole.Instance.OutputFormat("Usage: wearables check <first-name> <last-name>");
392 return;
393 }
394
395 string firstname = cmd[2];
396 string lastname = cmd[3];
397
398 StringBuilder sb = new StringBuilder();
399 UuidGatherer uuidGatherer = new UuidGatherer(m_scenes[0].AssetService);
400
401 lock (m_scenes)
402 {
403 foreach (Scene scene in m_scenes)
404 {
405 ScenePresence sp = scene.GetScenePresence(firstname, lastname);
406 if (sp != null && !sp.IsChildAgent)
407 {
408 sb.AppendFormat("Wearables checks for {0}\n\n", sp.Name);
409
410 for (int i = (int)WearableType.Shape; i < (int)WearableType.Physics; i++)
411 {
412 AvatarWearable aw = sp.Appearance.Wearables[i];
413
414 if (aw.Count > 0)
415 {
416 sb.Append(Enum.GetName(typeof(WearableType), i));
417 sb.Append("\n");
418
419 for (int j = 0; j < aw.Count; j++)
420 {
421 WearableItem wi = aw[j];
422
423 ConsoleDisplayList cdl = new ConsoleDisplayList();
424 cdl.Indent = 2;
425 cdl.AddRow("Item UUID", wi.ItemID);
426 cdl.AddRow("Assets", "");
427 sb.Append(cdl.ToString());
428
429 Dictionary<UUID, sbyte> assetUuids = new Dictionary<UUID, sbyte>();
430 uuidGatherer.GatherAssetUuids(wi.AssetID, assetUuids);
431 string[] assetStrings
432 = Array.ConvertAll<UUID, string>(assetUuids.Keys.ToArray(), u => u.ToString());
433
434 bool[] existChecks = scene.AssetService.AssetsExist(assetStrings);
435
436 ConsoleDisplayTable cdt = new ConsoleDisplayTable();
437 cdt.Indent = 4;
438 cdt.AddColumn("Type", 10);
439 cdt.AddColumn("UUID", ConsoleDisplayUtil.UuidSize);
440 cdt.AddColumn("Found", 5);
441
442 for (int k = 0; k < existChecks.Length; k++)
443 cdt.AddRow((AssetType)assetUuids[new UUID(assetStrings[k])], assetStrings[k], existChecks[k] ? "yes" : "no");
444
445 sb.Append(cdt.ToString());
446 sb.Append("\n");
447 }
448 }
449 }
450 }
451 }
452 }
453
454 MainConsole.Instance.Output(sb.ToString());
455 }
456
379 private void AppendWearablesDetailReport(ScenePresence sp, StringBuilder sb) 457 private void AppendWearablesDetailReport(ScenePresence sp, StringBuilder sb)
380 { 458 {
381 sb.AppendFormat("\nWearables for {0}\n", sp.Name); 459 sb.AppendFormat("\nWearables for {0}\n", sp.Name);