[1114] | 1 | using System;
|
---|
| 2 | using System.Collections.Generic;
|
---|
| 3 | using System.IO;
|
---|
| 4 | using System.Xml;
|
---|
| 5 | using Oni.Metadata;
|
---|
| 6 | using Oni.Xml;
|
---|
| 7 |
|
---|
| 8 | namespace Oni.Totoro
|
---|
| 9 | {
|
---|
| 10 | internal class AnimationXmlReader
|
---|
| 11 | {
|
---|
| 12 | private const string ns = "";
|
---|
| 13 | private static readonly char[] emptyChars = new char[0];
|
---|
| 14 | private XmlReader xml;
|
---|
| 15 | private string basePath;
|
---|
| 16 | private Animation animation;
|
---|
| 17 | private AnimationDaeReader daeReader;
|
---|
| 18 |
|
---|
| 19 | private AnimationXmlReader()
|
---|
| 20 | {
|
---|
| 21 | }
|
---|
| 22 |
|
---|
| 23 | public static Animation Read(XmlReader xml, string baseDir)
|
---|
| 24 | {
|
---|
| 25 | var reader = new AnimationXmlReader
|
---|
| 26 | {
|
---|
| 27 | xml = xml,
|
---|
| 28 | basePath = baseDir,
|
---|
| 29 | animation = new Animation()
|
---|
| 30 | };
|
---|
| 31 |
|
---|
| 32 | var animation = reader.Read();
|
---|
| 33 | animation.ValidateFrames();
|
---|
| 34 | return animation;
|
---|
| 35 | }
|
---|
| 36 |
|
---|
| 37 | private Animation Read()
|
---|
| 38 | {
|
---|
| 39 | animation.Name = xml.GetAttribute("Name");
|
---|
| 40 | xml.ReadStartElement("Animation", ns);
|
---|
| 41 |
|
---|
| 42 | if (xml.IsStartElement("DaeImport") || xml.IsStartElement("Import"))
|
---|
| 43 | ImportDaeAnimation();
|
---|
| 44 |
|
---|
| 45 | xml.ReadStartElement("Lookup");
|
---|
| 46 | animation.Type = MetaEnum.Parse<AnimationType>(xml.ReadElementContentAsString("Type", ns));
|
---|
| 47 | animation.AimingType = MetaEnum.Parse<AnimationType>(xml.ReadElementContentAsString("AimingType", ns));
|
---|
| 48 | animation.FromState = MetaEnum.Parse<AnimationState>(xml.ReadElementContentAsString("FromState", ns));
|
---|
| 49 | animation.ToState = MetaEnum.Parse<AnimationState>(xml.ReadElementContentAsString("ToState", ns));
|
---|
| 50 | animation.Varient = MetaEnum.Parse<AnimationVarient>(xml.ReadElementContentAsString("Varient", ns));
|
---|
| 51 | animation.FirstLevelAvailable = xml.ReadElementContentAsInt("FirstLevel", ns);
|
---|
| 52 | ReadRawArray("Shortcuts", animation.Shortcuts, Read);
|
---|
| 53 | xml.ReadEndElement();
|
---|
| 54 |
|
---|
| 55 | animation.Flags = MetaEnum.Parse<AnimationFlags>(xml.ReadElementContentAsString("Flags", ns));
|
---|
| 56 | xml.ReadStartElement("Atomic", ns);
|
---|
| 57 | animation.AtomicStart = xml.ReadElementContentAsInt("Start", ns);
|
---|
| 58 | animation.AtomicEnd = xml.ReadElementContentAsInt("End", ns);
|
---|
| 59 | xml.ReadEndElement();
|
---|
| 60 | xml.ReadStartElement("Invulnerable", ns);
|
---|
| 61 | animation.InvulnerableStart = xml.ReadElementContentAsInt("Start", ns);
|
---|
| 62 | animation.InvulnerableEnd = xml.ReadElementContentAsInt("End", ns);
|
---|
| 63 | xml.ReadEndElement();
|
---|
| 64 | xml.ReadStartElement("Overlay", ns);
|
---|
| 65 | animation.OverlayUsedBones = MetaEnum.Parse<BoneMask>(xml.ReadElementContentAsString("UsedBones", ns));
|
---|
| 66 | animation.OverlayReplacedBones = MetaEnum.Parse<BoneMask>(xml.ReadElementContentAsString("ReplacedBones", ns));
|
---|
| 67 | xml.ReadEndElement();
|
---|
| 68 |
|
---|
| 69 | xml.ReadStartElement("DirectAnimations", ns);
|
---|
| 70 | animation.DirectAnimations[0] = xml.ReadElementContentAsString("Link", ns);
|
---|
| 71 | animation.DirectAnimations[1] = xml.ReadElementContentAsString("Link", ns);
|
---|
| 72 | xml.ReadEndElement();
|
---|
| 73 | xml.ReadStartElement("Pause");
|
---|
| 74 | animation.HardPause = xml.ReadElementContentAsInt("Hard", ns);
|
---|
| 75 | animation.SoftPause = xml.ReadElementContentAsInt("Soft", ns);
|
---|
| 76 | xml.ReadEndElement();
|
---|
| 77 | xml.ReadStartElement("Interpolation", ns);
|
---|
| 78 | animation.InterpolationEnd = xml.ReadElementContentAsInt("End", ns);
|
---|
| 79 | animation.InterpolationMax = xml.ReadElementContentAsInt("Max", ns);
|
---|
| 80 | xml.ReadEndElement();
|
---|
| 81 |
|
---|
| 82 | animation.FinalRotation = MathHelper.ToRadians(xml.ReadElementContentAsFloat("FinalRotation", ns));
|
---|
| 83 | animation.Direction = MetaEnum.Parse<Direction>(xml.ReadElementContentAsString("Direction", ns));
|
---|
| 84 | animation.Vocalization = xml.ReadElementContentAsInt("Vocalization", ns);
|
---|
| 85 | animation.ActionFrame = xml.ReadElementContentAsInt("ActionFrame", ns);
|
---|
| 86 | animation.Impact = xml.ReadElementContentAsString("Impact", ns);
|
---|
| 87 |
|
---|
| 88 | ReadRawArray("Particle", animation.Particles, Read);
|
---|
| 89 | ReadRawArray("MotionBlur", animation.MotionBlur, Read);
|
---|
| 90 | ReadRawArray("Footsteps", animation.Footsteps, Read);
|
---|
| 91 | ReadRawArray("Sounds", animation.Sounds, Read);
|
---|
| 92 |
|
---|
| 93 | if (daeReader == null)
|
---|
| 94 | {
|
---|
| 95 | ReadHeights();
|
---|
| 96 | ReadVelocities();
|
---|
| 97 | ReadRotations();
|
---|
| 98 | ReadPositions();
|
---|
| 99 | }
|
---|
| 100 |
|
---|
| 101 | ReadThrowInfo();
|
---|
| 102 | ReadRawArray("SelfDamage", animation.SelfDamage, Read);
|
---|
| 103 |
|
---|
| 104 | if (xml.IsStartElement("Attacks"))
|
---|
| 105 | {
|
---|
| 106 | ReadRawArray("Attacks", animation.Attacks, Read);
|
---|
| 107 | ReadAttackRing();
|
---|
| 108 | }
|
---|
| 109 |
|
---|
| 110 | xml.ReadEndElement();
|
---|
| 111 |
|
---|
| 112 | if (daeReader != null)
|
---|
| 113 | {
|
---|
| 114 | daeReader.Read(animation);
|
---|
| 115 | }
|
---|
| 116 |
|
---|
| 117 | return animation;
|
---|
| 118 | }
|
---|
| 119 |
|
---|
| 120 | private void ReadVelocities()
|
---|
| 121 | {
|
---|
| 122 | if (!xml.IsStartElement("Velocities"))
|
---|
| 123 | return;
|
---|
| 124 |
|
---|
| 125 | if (xml.SkipEmpty())
|
---|
| 126 | return;
|
---|
| 127 |
|
---|
| 128 | xml.ReadStartElement();
|
---|
| 129 |
|
---|
| 130 | while (xml.IsStartElement())
|
---|
| 131 | animation.Velocities.Add(xml.ReadElementContentAsVector2());
|
---|
| 132 |
|
---|
| 133 | xml.ReadEndElement();
|
---|
| 134 | }
|
---|
| 135 |
|
---|
| 136 | private void ReadPositions()
|
---|
| 137 | {
|
---|
| 138 | var xz = new Vector2();
|
---|
| 139 |
|
---|
| 140 | if (xml.IsStartElement("PositionOffset"))
|
---|
| 141 | {
|
---|
| 142 | xml.ReadStartElement();
|
---|
| 143 | xz.X = xml.ReadElementContentAsFloat("X", ns);
|
---|
| 144 | xz.Y = xml.ReadElementContentAsFloat("Z", ns);
|
---|
| 145 | xml.ReadEndElement();
|
---|
| 146 | }
|
---|
| 147 |
|
---|
| 148 | ReadRawArray("Positions", animation.Positions, ReadPosition);
|
---|
| 149 |
|
---|
| 150 | for (int i = 0; i < animation.Positions.Count; i++)
|
---|
| 151 | {
|
---|
| 152 | var position = animation.Positions[i];
|
---|
| 153 | position.X = xz.X;
|
---|
| 154 | position.Z = xz.Y;
|
---|
| 155 | xz += animation.Velocities[i];
|
---|
| 156 | }
|
---|
| 157 | }
|
---|
| 158 |
|
---|
| 159 | private void ReadRotations()
|
---|
| 160 | {
|
---|
| 161 | var rotations = animation.Rotations;
|
---|
| 162 |
|
---|
| 163 | xml.ReadStartElement("Rotations");
|
---|
| 164 |
|
---|
| 165 | while (xml.IsStartElement())
|
---|
| 166 | {
|
---|
| 167 | xml.ReadStartElement("Bone");
|
---|
| 168 |
|
---|
| 169 | var keys = new List<KeyFrame>();
|
---|
| 170 |
|
---|
| 171 | int count = 0;
|
---|
| 172 |
|
---|
| 173 | while (xml.IsStartElement())
|
---|
| 174 | {
|
---|
| 175 | string name = xml.LocalName;
|
---|
| 176 | string[] tokens = xml.ReadElementContentAsString().Split(emptyChars, StringSplitOptions.RemoveEmptyEntries);
|
---|
| 177 |
|
---|
| 178 | var key = new KeyFrame();
|
---|
| 179 | key.Duration = XmlConvert.ToByte(tokens[0]);
|
---|
| 180 |
|
---|
| 181 | switch (name)
|
---|
| 182 | {
|
---|
| 183 | case "EKey":
|
---|
| 184 | animation.FrameSize = 6;
|
---|
| 185 | key.Rotation.X = XmlConvert.ToSingle(tokens[1]);
|
---|
| 186 | key.Rotation.Y = XmlConvert.ToSingle(tokens[2]);
|
---|
| 187 | key.Rotation.Z = XmlConvert.ToSingle(tokens[3]);
|
---|
| 188 | break;
|
---|
| 189 |
|
---|
| 190 | case "QKey":
|
---|
| 191 | animation.FrameSize = 16;
|
---|
| 192 | key.Rotation.X = XmlConvert.ToSingle(tokens[1]);
|
---|
| 193 | key.Rotation.Y = XmlConvert.ToSingle(tokens[2]);
|
---|
| 194 | key.Rotation.Z = XmlConvert.ToSingle(tokens[3]);
|
---|
| 195 | key.Rotation.W = -XmlConvert.ToSingle(tokens[4]);
|
---|
| 196 | break;
|
---|
| 197 |
|
---|
| 198 | default:
|
---|
| 199 | throw new InvalidDataException(string.Format("Unknonw animation key type '{0}'", name));
|
---|
| 200 | }
|
---|
| 201 |
|
---|
| 202 | count += key.Duration;
|
---|
| 203 | keys.Add(key);
|
---|
| 204 | }
|
---|
| 205 |
|
---|
| 206 | if (count != animation.Velocities.Count)
|
---|
| 207 | throw new InvalidDataException("bad number of frames");
|
---|
| 208 |
|
---|
| 209 | rotations.Add(keys);
|
---|
| 210 | xml.ReadEndElement();
|
---|
| 211 | }
|
---|
| 212 |
|
---|
| 213 | xml.ReadEndElement();
|
---|
| 214 | }
|
---|
| 215 |
|
---|
| 216 | private void ReadHeights()
|
---|
| 217 | {
|
---|
| 218 | if (!xml.IsStartElement("Heights"))
|
---|
| 219 | return;
|
---|
| 220 |
|
---|
| 221 | if (xml.SkipEmpty())
|
---|
| 222 | return;
|
---|
| 223 |
|
---|
| 224 | xml.ReadStartElement();
|
---|
| 225 |
|
---|
| 226 | while (xml.IsStartElement())
|
---|
| 227 | animation.Heights.Add(xml.ReadElementContentAsFloat("Height", ns));
|
---|
| 228 |
|
---|
| 229 | xml.ReadEndElement();
|
---|
| 230 | }
|
---|
| 231 |
|
---|
| 232 | private void ReadThrowInfo()
|
---|
| 233 | {
|
---|
| 234 | if (!xml.IsStartElement("ThrowSource"))
|
---|
| 235 | return;
|
---|
| 236 |
|
---|
| 237 | if (xml.SkipEmpty())
|
---|
| 238 | return;
|
---|
| 239 |
|
---|
| 240 | animation.ThrowSource = new ThrowInfo();
|
---|
| 241 | xml.ReadStartElement("ThrowSource");
|
---|
| 242 | xml.ReadStartElement("TargetAdjustment");
|
---|
| 243 | animation.ThrowSource.Position = xml.ReadElementContentAsVector3("Position");
|
---|
| 244 | animation.ThrowSource.Angle = xml.ReadElementContentAsFloat("Angle", ns);
|
---|
| 245 | xml.ReadEndElement();
|
---|
| 246 | animation.ThrowSource.Distance = xml.ReadElementContentAsFloat("Distance", ns);
|
---|
| 247 | animation.ThrowSource.Type = MetaEnum.Parse<AnimationType>(xml.ReadElementContentAsString("TargetType", ns));
|
---|
| 248 | xml.ReadEndElement();
|
---|
| 249 | }
|
---|
| 250 |
|
---|
| 251 | private void ReadAttackRing()
|
---|
| 252 | {
|
---|
| 253 | if (!xml.IsStartElement("AttackRing") && !xml.IsStartElement("HorizontalExtents"))
|
---|
| 254 | return;
|
---|
| 255 |
|
---|
| 256 | if (animation.Attacks.Count == 0)
|
---|
| 257 | {
|
---|
| 258 | Console.Error.WriteLine("Warning: AttackRing found but no attacks are present, ignoring");
|
---|
| 259 | xml.Skip();
|
---|
| 260 | return;
|
---|
| 261 | }
|
---|
| 262 |
|
---|
| 263 | xml.ReadStartElement();
|
---|
| 264 |
|
---|
| 265 | for (int i = 0; i < 36; i++)
|
---|
| 266 | animation.AttackRing[i] = xml.ReadElementContentAsFloat();
|
---|
| 267 |
|
---|
| 268 | xml.ReadEndElement();
|
---|
| 269 | }
|
---|
| 270 |
|
---|
| 271 | private void ReadPosition(Position position)
|
---|
| 272 | {
|
---|
| 273 | xml.ReadStartElement("Position");
|
---|
| 274 | position.Height = xml.ReadElementContentAsFloat("Height", ns);
|
---|
| 275 | position.YOffset = xml.ReadElementContentAsFloat("YOffset", ns);
|
---|
| 276 | xml.ReadEndElement();
|
---|
| 277 | }
|
---|
| 278 |
|
---|
| 279 | private void Read(Particle particle)
|
---|
| 280 | {
|
---|
| 281 | xml.ReadStartElement("Particle");
|
---|
| 282 | particle.Start = xml.ReadElementContentAsInt("Start", ns);
|
---|
| 283 | particle.End = xml.ReadElementContentAsInt("End", ns);
|
---|
| 284 | particle.Bone = MetaEnum.Parse<Bone>(xml.ReadElementContentAsString("Bone", ns));
|
---|
| 285 | particle.Name = xml.ReadElementContentAsString("Name", ns);
|
---|
| 286 | xml.ReadEndElement();
|
---|
| 287 | }
|
---|
| 288 |
|
---|
| 289 | private void Read(Sound sound)
|
---|
| 290 | {
|
---|
| 291 | xml.ReadStartElement("Sound");
|
---|
| 292 | sound.Name = xml.ReadElementContentAsString("Name", ns);
|
---|
| 293 | sound.Start = xml.ReadElementContentAsInt("Start", ns);
|
---|
| 294 | xml.ReadEndElement();
|
---|
| 295 | }
|
---|
| 296 |
|
---|
| 297 | private void Read(Shortcut shortcut)
|
---|
| 298 | {
|
---|
| 299 | xml.ReadStartElement("Shortcut");
|
---|
| 300 | shortcut.FromState = MetaEnum.Parse<AnimationState>(xml.ReadElementContentAsString("FromState", ns));
|
---|
| 301 | shortcut.Length = xml.ReadElementContentAsInt("Length", ns);
|
---|
| 302 | shortcut.ReplaceAtomic = (xml.ReadElementContentAsString("ReplaceAtomic", ns) == "yes");
|
---|
| 303 | xml.ReadEndElement();
|
---|
| 304 | }
|
---|
| 305 |
|
---|
| 306 | private void Read(Footstep footstep)
|
---|
| 307 | {
|
---|
| 308 | xml.ReadStartElement("Footstep");
|
---|
| 309 |
|
---|
| 310 | string frame = xml.GetAttribute("Frame");
|
---|
| 311 |
|
---|
| 312 | if (frame != null)
|
---|
| 313 | {
|
---|
| 314 | footstep.Frame = XmlConvert.ToInt32(frame);
|
---|
| 315 | footstep.Type = MetaEnum.Parse<FootstepType>(xml.GetAttribute("Type"));
|
---|
| 316 | }
|
---|
| 317 | else
|
---|
| 318 | {
|
---|
| 319 | footstep.Frame = xml.ReadElementContentAsInt("Frame", ns);
|
---|
| 320 | footstep.Type = MetaEnum.Parse<FootstepType>(xml.ReadElementContentAsString("Type", ns));
|
---|
| 321 | }
|
---|
| 322 |
|
---|
| 323 | xml.ReadEndElement();
|
---|
| 324 | }
|
---|
| 325 |
|
---|
| 326 | private void Read(Damage damage)
|
---|
| 327 | {
|
---|
| 328 | xml.ReadStartElement("Damage");
|
---|
| 329 | damage.Points = xml.ReadElementContentAsInt("Points", ns);
|
---|
| 330 | damage.Frame = xml.ReadElementContentAsInt("Frame", ns);
|
---|
| 331 | xml.ReadEndElement();
|
---|
| 332 | }
|
---|
| 333 |
|
---|
| 334 | private void Read(MotionBlur d)
|
---|
| 335 | {
|
---|
| 336 | xml.ReadStartElement("MotionBlur");
|
---|
| 337 | d.Bones = MetaEnum.Parse<BoneMask>(xml.ReadElementContentAsString("Bones", ns));
|
---|
| 338 | d.Start = xml.ReadElementContentAsInt("Start", ns);
|
---|
| 339 | d.End = xml.ReadElementContentAsInt("End", ns);
|
---|
| 340 | d.Lifetime = xml.ReadElementContentAsInt("Lifetime", ns);
|
---|
| 341 | d.Alpha = xml.ReadElementContentAsInt("Alpha", ns);
|
---|
| 342 | d.Interval = xml.ReadElementContentAsInt("Interval", ns);
|
---|
| 343 | xml.ReadEndElement();
|
---|
| 344 | }
|
---|
| 345 |
|
---|
| 346 | private void Read(Attack attack)
|
---|
| 347 | {
|
---|
| 348 | xml.ReadStartElement("Attack");
|
---|
| 349 | attack.Start = xml.ReadElementContentAsInt("Start", ns);
|
---|
| 350 | attack.End = xml.ReadElementContentAsInt("End", ns);
|
---|
| 351 | attack.Bones = MetaEnum.Parse<BoneMask>(xml.ReadElementContentAsString("Bones", ns));
|
---|
| 352 | attack.Flags = MetaEnum.Parse<AttackFlags>(xml.ReadElementContentAsString("Flags", ns));
|
---|
| 353 | attack.Knockback = xml.ReadElementContentAsFloat("Knockback", ns);
|
---|
| 354 | attack.HitPoints = xml.ReadElementContentAsInt("HitPoints", ns);
|
---|
| 355 | attack.HitType = MetaEnum.Parse<AnimationType>(xml.ReadElementContentAsString("HitType", ns));
|
---|
| 356 | attack.HitLength = xml.ReadElementContentAsInt("HitLength", ns);
|
---|
| 357 | attack.StunLength = xml.ReadElementContentAsInt("StunLength", ns);
|
---|
| 358 | attack.StaggerLength = xml.ReadElementContentAsInt("StaggerLength", ns);
|
---|
| 359 |
|
---|
| 360 | if (xml.IsStartElement("Extents"))
|
---|
| 361 | {
|
---|
| 362 | ReadRawArray("Extents", attack.Extents, Read);
|
---|
| 363 |
|
---|
| 364 | if (attack.Extents.Count != attack.End - attack.Start + 1)
|
---|
| 365 | Console.Error.WriteLine("Error: Attack starting at frame {0} has an incorrect number of extents ({1})", attack.Start, attack.Extents.Count);
|
---|
| 366 | }
|
---|
| 367 |
|
---|
| 368 | xml.ReadEndElement();
|
---|
| 369 | }
|
---|
| 370 |
|
---|
| 371 | private void Read(AttackExtent extent)
|
---|
| 372 | {
|
---|
| 373 | xml.ReadStartElement("Extent");
|
---|
| 374 | extent.Angle = xml.ReadElementContentAsFloat("Angle", ns);
|
---|
| 375 | extent.Length = xml.ReadElementContentAsFloat("Length", ns);
|
---|
| 376 | extent.MinY = xml.ReadElementContentAsFloat("MinY", ns);
|
---|
| 377 | extent.MaxY = xml.ReadElementContentAsFloat("MaxY", ns);
|
---|
| 378 | xml.ReadEndElement();
|
---|
| 379 | }
|
---|
| 380 |
|
---|
| 381 | private void ReadRawArray<T>(string name, List<T> list, Action<T> elementReader)
|
---|
| 382 | where T : new()
|
---|
| 383 | {
|
---|
| 384 | if (xml.SkipEmpty())
|
---|
| 385 | return;
|
---|
| 386 |
|
---|
| 387 | xml.ReadStartElement();
|
---|
| 388 |
|
---|
| 389 | while (xml.IsStartElement())
|
---|
| 390 | {
|
---|
| 391 | T t = new T();
|
---|
| 392 | elementReader(t);
|
---|
| 393 | list.Add(t);
|
---|
| 394 | }
|
---|
| 395 |
|
---|
| 396 | xml.ReadEndElement();
|
---|
| 397 | }
|
---|
| 398 |
|
---|
| 399 | private void ImportDaeAnimation()
|
---|
| 400 | {
|
---|
| 401 | string filePath = xml.GetAttribute("Path");
|
---|
| 402 | bool empty = xml.SkipEmpty();
|
---|
| 403 |
|
---|
| 404 | if (!empty)
|
---|
| 405 | {
|
---|
| 406 | xml.ReadStartElement();
|
---|
| 407 |
|
---|
| 408 | if (filePath == null)
|
---|
| 409 | filePath = xml.ReadElementContentAsString("Path", ns);
|
---|
| 410 | }
|
---|
| 411 |
|
---|
| 412 | filePath = Path.Combine(basePath, filePath);
|
---|
| 413 |
|
---|
| 414 | if (!File.Exists(filePath))
|
---|
| 415 | {
|
---|
| 416 | Console.Error.WriteLine("Could not find animation import source file '{0}'", filePath);
|
---|
| 417 | return;
|
---|
| 418 | }
|
---|
| 419 |
|
---|
| 420 | Console.WriteLine("Importing {0}", filePath);
|
---|
| 421 |
|
---|
| 422 | daeReader = new AnimationDaeReader();
|
---|
| 423 | daeReader.Scene = Dae.Reader.ReadFile(filePath);
|
---|
| 424 |
|
---|
| 425 | if (!empty)
|
---|
| 426 | {
|
---|
| 427 | if (xml.IsStartElement("Start"))
|
---|
| 428 | daeReader.StartFrame = xml.ReadElementContentAsInt("Start", ns);
|
---|
| 429 |
|
---|
| 430 | if (xml.IsStartElement("End"))
|
---|
| 431 | daeReader.EndFrame = xml.ReadElementContentAsInt("End", ns);
|
---|
| 432 |
|
---|
| 433 | xml.ReadEndElement();
|
---|
| 434 | }
|
---|
| 435 | }
|
---|
| 436 | }
|
---|
| 437 | }
|
---|