aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/Region/ClientStack/Linden/Caps/BunchOfCaps/MeshCost.cs
diff options
context:
space:
mode:
Diffstat (limited to 'OpenSim/Region/ClientStack/Linden/Caps/BunchOfCaps/MeshCost.cs')
-rw-r--r--OpenSim/Region/ClientStack/Linden/Caps/BunchOfCaps/MeshCost.cs727
1 files changed, 727 insertions, 0 deletions
diff --git a/OpenSim/Region/ClientStack/Linden/Caps/BunchOfCaps/MeshCost.cs b/OpenSim/Region/ClientStack/Linden/Caps/BunchOfCaps/MeshCost.cs
new file mode 100644
index 0000000..f6a950f
--- /dev/null
+++ b/OpenSim/Region/ClientStack/Linden/Caps/BunchOfCaps/MeshCost.cs
@@ -0,0 +1,727 @@
1/*
2 * Copyright (c) Contributors, http://opensimulator.org/
3 * See CONTRIBUTORS.TXT for a full list of copyright holders.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 * * Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * * Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 * * Neither the name of the OpenSimulator Project nor the
13 * names of its contributors may be used to endorse or promote products
14 * derived from this software without specific prior written permission.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
17 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
20 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27
28
29using System;
30using System.IO;
31using System.Collections;
32using System.Collections.Generic;
33using System.Text;
34
35using OpenMetaverse;
36using OpenMetaverse.StructuredData;
37
38using OpenSim.Framework;
39using OpenSim.Region.Framework;
40using OpenSim.Region.Framework.Scenes;
41using OpenSim.Framework.Capabilities;
42
43using ComponentAce.Compression.Libs.zlib;
44
45using OSDArray = OpenMetaverse.StructuredData.OSDArray;
46using OSDMap = OpenMetaverse.StructuredData.OSDMap;
47
48namespace OpenSim.Region.ClientStack.Linden
49{
50 public struct ModelPrimLimits
51 {
52
53 }
54
55 public class ModelCost
56 {
57
58 // upload fee defaults
59 // fees are normalized to 1.0
60 // this parameters scale them to basic cost ( so 1.0 translates to 10 )
61
62 public float ModelMeshCostFactor = 0.0f; // scale total cost relative to basic (excluding textures)
63 public float ModelTextureCostFactor = 1.0f; // scale textures fee to basic.
64 public float ModelMinCostFactor = 0.0f; // 0.5f; // minimum total model free excluding textures
65
66 // itens costs in normalized values
67 // ie will be multiplied by basicCost and factors above
68 public float primCreationCost = 0.002f; // extra cost for each prim creation overhead
69 // weigthed size to normalized cost
70 public float bytecost = 1e-5f;
71
72 // mesh upload fees based on compressed data sizes
73 // several data sections are counted more that once
74 // to promote user optimization
75 // following parameters control how many extra times they are added
76 // to global size.
77 // LOD meshs
78 const float medSizeWth = 1f; // 2x
79 const float lowSizeWth = 1.5f; // 2.5x
80 const float lowestSizeWth = 2f; // 3x
81 // favor potencially physical optimized meshs versus automatic decomposition
82 const float physMeshSizeWth = 6f; // counts 7x
83 const float physHullSizeWth = 8f; // counts 9x
84
85 // stream cost area factors
86 // more or less like SL
87 const float highLodFactor = 17.36f;
88 const float midLodFactor = 277.78f;
89 const float lowLodFactor = 1111.11f;
90
91 // physics cost is below, identical to SL, assuming shape type convex
92 // server cost is below identical to SL assuming non scripted non physical object
93
94 // internal
95 const int bytesPerCoord = 6; // 3 coords, 2 bytes per each
96
97 // control prims dimensions
98 public float PrimScaleMin = 0.001f;
99 public float NonPhysicalPrimScaleMax = 256f;
100 public float PhysicalPrimScaleMax = 10f;
101 public int ObjectLinkedPartsMax = 512;
102
103 // storage for a single mesh asset cost parameters
104 private class ameshCostParam
105 {
106 // LOD sizes for size dependent streaming cost
107 public int highLODSize;
108 public int medLODSize;
109 public int lowLODSize;
110 public int lowestLODSize;
111 // normalized fee based on compressed data sizes
112 public float costFee;
113 // physics cost
114 public float physicsCost;
115 }
116
117 // calculates a mesh model costs
118 // returns false on error, with a reason on parameter error
119 // resources input LLSD request
120 // basicCost input region assets upload cost
121 // totalcost returns model total upload fee
122 // meshcostdata returns detailed costs for viewer
123 // avatarSkeleton if mesh includes a avatar skeleton
124 // useAvatarCollider if we should use physics mesh for avatar
125 public bool MeshModelCost(LLSDAssetResource resources, int basicCost, out int totalcost,
126 LLSDAssetUploadResponseData meshcostdata, out string error, ref string warning)
127 {
128 totalcost = 0;
129 error = string.Empty;
130
131 bool avatarSkeleton = false;
132
133 if (resources == null ||
134 resources.instance_list == null ||
135 resources.instance_list.Array.Count == 0)
136 {
137 error = "missing model information.";
138 return false;
139 }
140
141 int numberInstances = resources.instance_list.Array.Count;
142
143 if( numberInstances > ObjectLinkedPartsMax )
144 {
145 error = "Model whould have more than " + ObjectLinkedPartsMax.ToString() + " linked prims";
146 return false;
147 }
148
149 meshcostdata.model_streaming_cost = 0.0;
150 meshcostdata.simulation_cost = 0.0;
151 meshcostdata.physics_cost = 0.0;
152 meshcostdata.resource_cost = 0.0;
153
154 meshcostdata.upload_price_breakdown.mesh_instance = 0;
155 meshcostdata.upload_price_breakdown.mesh_physics = 0;
156 meshcostdata.upload_price_breakdown.mesh_streaming = 0;
157 meshcostdata.upload_price_breakdown.model = 0;
158
159 int itmp;
160
161 // textures cost
162 if (resources.texture_list != null && resources.texture_list.Array.Count > 0)
163 {
164 float textures_cost = (float)(resources.texture_list.Array.Count * basicCost);
165 textures_cost *= ModelTextureCostFactor;
166
167 itmp = (int)(textures_cost + 0.5f); // round
168 meshcostdata.upload_price_breakdown.texture = itmp;
169 totalcost += itmp;
170 }
171
172 // meshs assets cost
173 float meshsfee = 0;
174 int numberMeshs = 0;
175 bool haveMeshs = false;
176
177 bool curskeleton;
178 bool curAvatarPhys;
179
180 List<ameshCostParam> meshsCosts = new List<ameshCostParam>();
181
182 if (resources.mesh_list != null && resources.mesh_list.Array.Count > 0)
183 {
184 numberMeshs = resources.mesh_list.Array.Count;
185
186 for (int i = 0; i < numberMeshs; i++)
187 {
188 ameshCostParam curCost = new ameshCostParam();
189 byte[] data = (byte[])resources.mesh_list.Array[i];
190
191 if (!MeshCost(data, curCost,out curskeleton, out curAvatarPhys, out error))
192 {
193 return false;
194 }
195
196 if (curskeleton)
197 {
198 if (avatarSkeleton)
199 {
200 error = "model can only contain a avatar skeleton";
201 return false;
202 }
203 avatarSkeleton = true;
204 }
205 meshsCosts.Add(curCost);
206 meshsfee += curCost.costFee;
207 }
208 haveMeshs = true;
209 }
210
211 // instances (prims) cost
212
213
214 int mesh;
215 int skipedSmall = 0;
216 for (int i = 0; i < numberInstances; i++)
217 {
218 Hashtable inst = (Hashtable)resources.instance_list.Array[i];
219
220 ArrayList ascale = (ArrayList)inst["scale"];
221 Vector3 scale;
222 double tmp;
223 tmp = (double)ascale[0];
224 scale.X = (float)tmp;
225 tmp = (double)ascale[1];
226 scale.Y = (float)tmp;
227 tmp = (double)ascale[2];
228 scale.Z = (float)tmp;
229
230 if (scale.X < PrimScaleMin || scale.Y < PrimScaleMin || scale.Z < PrimScaleMin)
231 {
232 skipedSmall++;
233 continue;
234 }
235
236 if (scale.X > NonPhysicalPrimScaleMax || scale.Y > NonPhysicalPrimScaleMax || scale.Z > NonPhysicalPrimScaleMax)
237 {
238 error = "Model contains parts with sides larger than " + NonPhysicalPrimScaleMax.ToString() + "m. Please ajust scale";
239 return false;
240 }
241
242 if (haveMeshs && inst.ContainsKey("mesh"))
243 {
244 mesh = (int)inst["mesh"];
245
246 if (mesh >= numberMeshs)
247 {
248 error = "Incoerent model information.";
249 return false;
250 }
251
252 // streamming cost
253
254 float sqdiam = scale.LengthSquared();
255
256 ameshCostParam curCost = meshsCosts[mesh];
257 float mesh_streaming = streamingCost(curCost, sqdiam);
258
259 meshcostdata.model_streaming_cost += mesh_streaming;
260 meshcostdata.physics_cost += curCost.physicsCost;
261 }
262 else // instance as no mesh ??
263 {
264 // to do later if needed
265 meshcostdata.model_streaming_cost += 0.5f;
266 meshcostdata.physics_cost += 1.0f;
267 }
268
269 // assume unscripted and static prim server cost
270 meshcostdata.simulation_cost += 0.5f;
271 // charge for prims creation
272 meshsfee += primCreationCost;
273 }
274
275 if (skipedSmall > 0)
276 {
277 if (skipedSmall > numberInstances / 2)
278 {
279 error = "Model contains too many prims smaller than " + PrimScaleMin.ToString() +
280 "m minimum allowed size. Please check scalling";
281 return false;
282 }
283 else
284 warning += skipedSmall.ToString() + " of the requested " +numberInstances.ToString() +
285 " model prims will not upload because they are smaller than " + PrimScaleMin.ToString() +
286 "m minimum allowed size. Please check scalling ";
287 }
288
289 if (meshcostdata.physics_cost <= meshcostdata.model_streaming_cost)
290 meshcostdata.resource_cost = meshcostdata.model_streaming_cost;
291 else
292 meshcostdata.resource_cost = meshcostdata.physics_cost;
293
294 if (meshcostdata.resource_cost < meshcostdata.simulation_cost)
295 meshcostdata.resource_cost = meshcostdata.simulation_cost;
296
297 // scale cost
298 // at this point a cost of 1.0 whould mean basic cost
299 meshsfee *= ModelMeshCostFactor;
300
301 if (meshsfee < ModelMinCostFactor)
302 meshsfee = ModelMinCostFactor;
303
304 // actually scale it to basic cost
305 meshsfee *= (float)basicCost;
306
307 meshsfee += 0.5f; // rounding
308
309 totalcost += (int)meshsfee;
310
311 // breakdown prices
312 // don't seem to be in use so removed code for now
313
314 return true;
315 }
316
317 // single mesh asset cost
318 private bool MeshCost(byte[] data, ameshCostParam cost,out bool skeleton, out bool avatarPhys, out string error)
319 {
320 cost.highLODSize = 0;
321 cost.medLODSize = 0;
322 cost.lowLODSize = 0;
323 cost.lowestLODSize = 0;
324 cost.physicsCost = 0.0f;
325 cost.costFee = 0.0f;
326
327 error = string.Empty;
328
329 skeleton = false;
330 avatarPhys = false;
331
332 if (data == null || data.Length == 0)
333 {
334 error = "Missing model information.";
335 return false;
336 }
337
338 OSD meshOsd = null;
339 int start = 0;
340
341 error = "Invalid model data";
342
343 using (MemoryStream ms = new MemoryStream(data))
344 {
345 try
346 {
347 OSD osd = OSDParser.DeserializeLLSDBinary(ms);
348 if (osd is OSDMap)
349 meshOsd = (OSDMap)osd;
350 else
351 return false;
352 }
353 catch (Exception e)
354 {
355 return false;
356 }
357 start = (int)ms.Position;
358 }
359
360 OSDMap map = (OSDMap)meshOsd;
361 OSDMap tmpmap;
362
363 int highlod_size = 0;
364 int medlod_size = 0;
365 int lowlod_size = 0;
366 int lowestlod_size = 0;
367 int skin_size = 0;
368
369 int hulls_size = 0;
370 int phys_nhulls;
371 int phys_hullsvertices = 0;
372
373 int physmesh_size = 0;
374 int phys_ntriangles = 0;
375
376 int submesh_offset = -1;
377
378 if (map.ContainsKey("skeleton"))
379 {
380 tmpmap = (OSDMap)map["skeleton"];
381 if (tmpmap.ContainsKey("offset") && tmpmap.ContainsKey("size"))
382 {
383 int sksize = tmpmap["size"].AsInteger();
384 if(sksize > 0)
385 skeleton = true;
386 }
387 }
388
389 if (map.ContainsKey("physics_convex"))
390 {
391 tmpmap = (OSDMap)map["physics_convex"];
392 if (tmpmap.ContainsKey("offset"))
393 submesh_offset = tmpmap["offset"].AsInteger() + start;
394 if (tmpmap.ContainsKey("size"))
395 hulls_size = tmpmap["size"].AsInteger();
396 }
397
398 if (submesh_offset < 0 || hulls_size == 0)
399 {
400 error = "Missing physics_convex block";
401 return false;
402 }
403
404 if (!hulls(data, submesh_offset, hulls_size, out phys_hullsvertices, out phys_nhulls))
405 {
406 error = "Bad physics_convex block";
407 return false;
408 }
409
410 submesh_offset = -1;
411
412 // only look for LOD meshs sizes
413
414 if (map.ContainsKey("high_lod"))
415 {
416 tmpmap = (OSDMap)map["high_lod"];
417 // see at least if there is a offset for this one
418 if (tmpmap.ContainsKey("offset"))
419 submesh_offset = tmpmap["offset"].AsInteger() + start;
420 if (tmpmap.ContainsKey("size"))
421 highlod_size = tmpmap["size"].AsInteger();
422 }
423
424 if (submesh_offset < 0 || highlod_size <= 0)
425 {
426 error = "Missing high_lod block";
427 return false;
428 }
429
430 bool haveprev = true;
431
432 if (map.ContainsKey("medium_lod"))
433 {
434 tmpmap = (OSDMap)map["medium_lod"];
435 if (tmpmap.ContainsKey("size"))
436 medlod_size = tmpmap["size"].AsInteger();
437 else
438 haveprev = false;
439 }
440
441 if (haveprev && map.ContainsKey("low_lod"))
442 {
443 tmpmap = (OSDMap)map["low_lod"];
444 if (tmpmap.ContainsKey("size"))
445 lowlod_size = tmpmap["size"].AsInteger();
446 else
447 haveprev = false;
448 }
449
450 if (haveprev && map.ContainsKey("lowest_lod"))
451 {
452 tmpmap = (OSDMap)map["lowest_lod"];
453 if (tmpmap.ContainsKey("size"))
454 lowestlod_size = tmpmap["size"].AsInteger();
455 }
456
457 if (map.ContainsKey("skin"))
458 {
459 tmpmap = (OSDMap)map["skin"];
460 if (tmpmap.ContainsKey("size"))
461 skin_size = tmpmap["size"].AsInteger();
462 }
463
464 cost.highLODSize = highlod_size;
465 cost.medLODSize = medlod_size;
466 cost.lowLODSize = lowlod_size;
467 cost.lowestLODSize = lowestlod_size;
468
469 submesh_offset = -1;
470
471 tmpmap = null;
472 if(map.ContainsKey("physics_mesh"))
473 tmpmap = (OSDMap)map["physics_mesh"];
474 else if (map.ContainsKey("physics_shape")) // old naming
475 tmpmap = (OSDMap)map["physics_shape"];
476
477 if(tmpmap != null)
478 {
479 if (tmpmap.ContainsKey("offset"))
480 submesh_offset = tmpmap["offset"].AsInteger() + start;
481 if (tmpmap.ContainsKey("size"))
482 physmesh_size = tmpmap["size"].AsInteger();
483
484 if (submesh_offset >= 0 || physmesh_size > 0)
485 {
486
487 if (!submesh(data, submesh_offset, physmesh_size, out phys_ntriangles))
488 {
489 error = "Model data parsing error";
490 return false;
491 }
492 }
493 }
494
495 // upload is done in convex shape type so only one hull
496 phys_hullsvertices++;
497 cost.physicsCost = 0.04f * phys_hullsvertices;
498
499 float sfee;
500
501 sfee = data.Length; // start with total compressed data size
502
503 // penalize lod meshs that should be more builder optimized
504 sfee += medSizeWth * medlod_size;
505 sfee += lowSizeWth * lowlod_size;
506 sfee += lowestSizeWth * lowlod_size;
507
508 // physics
509 // favor potencial optimized meshs versus automatic decomposition
510 if (physmesh_size != 0)
511 sfee += physMeshSizeWth * (physmesh_size + hulls_size / 4); // reduce cost of mandatory convex hull
512 else
513 sfee += physHullSizeWth * hulls_size;
514
515 // bytes to money
516 sfee *= bytecost;
517
518 cost.costFee = sfee;
519 return true;
520 }
521
522 // parses a LOD or physics mesh component
523 private bool submesh(byte[] data, int offset, int size, out int ntriangles)
524 {
525 ntriangles = 0;
526
527 OSD decodedMeshOsd = new OSD();
528 byte[] meshBytes = new byte[size];
529 System.Buffer.BlockCopy(data, offset, meshBytes, 0, size);
530 try
531 {
532 using (MemoryStream inMs = new MemoryStream(meshBytes))
533 {
534 using (MemoryStream outMs = new MemoryStream())
535 {
536 using (ZOutputStream zOut = new ZOutputStream(outMs))
537 {
538 byte[] readBuffer = new byte[4096];
539 int readLen = 0;
540 while ((readLen = inMs.Read(readBuffer, 0, readBuffer.Length)) > 0)
541 {
542 zOut.Write(readBuffer, 0, readLen);
543 }
544 zOut.Flush();
545 outMs.Seek(0, SeekOrigin.Begin);
546
547 byte[] decompressedBuf = outMs.GetBuffer();
548 decodedMeshOsd = OSDParser.DeserializeLLSDBinary(decompressedBuf);
549 }
550 }
551 }
552 }
553 catch (Exception e)
554 {
555 return false;
556 }
557
558 OSDArray decodedMeshOsdArray = null;
559 if ((!decodedMeshOsd is OSDArray))
560 return false;
561
562 byte[] dummy;
563
564 decodedMeshOsdArray = (OSDArray)decodedMeshOsd;
565 foreach (OSD subMeshOsd in decodedMeshOsdArray)
566 {
567 if (subMeshOsd is OSDMap)
568 {
569 OSDMap subtmpmap = (OSDMap)subMeshOsd;
570 if (subtmpmap.ContainsKey("NoGeometry") && ((OSDBoolean)subtmpmap["NoGeometry"]))
571 continue;
572
573 if (!subtmpmap.ContainsKey("Position"))
574 return false;
575
576 if (subtmpmap.ContainsKey("TriangleList"))
577 {
578 dummy = subtmpmap["TriangleList"].AsBinary();
579 ntriangles += dummy.Length / bytesPerCoord;
580 }
581 else
582 return false;
583 }
584 }
585
586 return true;
587 }
588
589 // parses convex hulls component
590 private bool hulls(byte[] data, int offset, int size, out int nvertices, out int nhulls)
591 {
592 nvertices = 0;
593 nhulls = 1;
594
595 OSD decodedMeshOsd = new OSD();
596 byte[] meshBytes = new byte[size];
597 System.Buffer.BlockCopy(data, offset, meshBytes, 0, size);
598 try
599 {
600 using (MemoryStream inMs = new MemoryStream(meshBytes))
601 {
602 using (MemoryStream outMs = new MemoryStream())
603 {
604 using (ZOutputStream zOut = new ZOutputStream(outMs))
605 {
606 byte[] readBuffer = new byte[4096];
607 int readLen = 0;
608 while ((readLen = inMs.Read(readBuffer, 0, readBuffer.Length)) > 0)
609 {
610 zOut.Write(readBuffer, 0, readLen);
611 }
612 zOut.Flush();
613 outMs.Seek(0, SeekOrigin.Begin);
614
615 byte[] decompressedBuf = outMs.GetBuffer();
616 decodedMeshOsd = OSDParser.DeserializeLLSDBinary(decompressedBuf);
617 }
618 }
619 }
620 }
621 catch (Exception e)
622 {
623 return false;
624 }
625
626 OSDMap cmap = (OSDMap)decodedMeshOsd;
627 if (cmap == null)
628 return false;
629
630 byte[] dummy;
631
632 // must have one of this
633 if (cmap.ContainsKey("BoundingVerts"))
634 {
635 dummy = cmap["BoundingVerts"].AsBinary();
636 nvertices = dummy.Length / bytesPerCoord;
637 }
638 else
639 return false;
640
641/* upload is done with convex shape type
642 if (cmap.ContainsKey("HullList"))
643 {
644 dummy = cmap["HullList"].AsBinary();
645 nhulls += dummy.Length;
646 }
647
648
649 if (cmap.ContainsKey("Positions"))
650 {
651 dummy = cmap["Positions"].AsBinary();
652 nvertices = dummy.Length / bytesPerCoord;
653 }
654 */
655
656 return true;
657 }
658
659 // returns streaming cost from on mesh LODs sizes in curCost and square of prim size length
660 private float streamingCost(ameshCostParam curCost, float sqdiam)
661 {
662 // compute efective areas
663 float ma = 262144f;
664
665 float mh = sqdiam * highLodFactor;
666 if (mh > ma)
667 mh = ma;
668 float mm = sqdiam * midLodFactor;
669 if (mm > ma)
670 mm = ma;
671
672 float ml = sqdiam * lowLodFactor;
673 if (ml > ma)
674 ml = ma;
675
676 float mlst = ma;
677
678 mlst -= ml;
679 ml -= mm;
680 mm -= mh;
681
682 if (mlst < 1.0f)
683 mlst = 1.0f;
684 if (ml < 1.0f)
685 ml = 1.0f;
686 if (mm < 1.0f)
687 mm = 1.0f;
688 if (mh < 1.0f)
689 mh = 1.0f;
690
691 ma = mlst + ml + mm + mh;
692
693 // get LODs compressed sizes
694 // giving 384 bytes bonus
695 int lst = curCost.lowestLODSize - 384;
696 int l = curCost.lowLODSize - 384;
697 int m = curCost.medLODSize - 384;
698 int h = curCost.highLODSize - 384;
699
700 // use previus higher LOD size on missing ones
701 if (m <= 0)
702 m = h;
703 if (l <= 0)
704 l = m;
705 if (lst <= 0)
706 lst = l;
707
708 // force minumum sizes
709 if (lst < 16)
710 lst = 16;
711 if (l < 16)
712 l = 16;
713 if (m < 16)
714 m = 16;
715 if (h < 16)
716 h = 16;
717
718 // compute cost weighted by relative effective areas
719 float cost = (float)lst * mlst + (float)l * ml + (float)m * mm + (float)h * mh;
720 cost /= ma;
721
722 cost *= 0.004f; // overall tunning parameter
723
724 return cost;
725 }
726 }
727}