aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/Region/Framework/Scenes/KeyframeMotion.cs
diff options
context:
space:
mode:
Diffstat (limited to 'OpenSim/Region/Framework/Scenes/KeyframeMotion.cs')
-rw-r--r--OpenSim/Region/Framework/Scenes/KeyframeMotion.cs829
1 files changed, 829 insertions, 0 deletions
diff --git a/OpenSim/Region/Framework/Scenes/KeyframeMotion.cs b/OpenSim/Region/Framework/Scenes/KeyframeMotion.cs
new file mode 100644
index 0000000..646403f
--- /dev/null
+++ b/OpenSim/Region/Framework/Scenes/KeyframeMotion.cs
@@ -0,0 +1,829 @@
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
28using System;
29using System.Timers;
30using System.Collections;
31using System.Collections.Generic;
32using System.IO;
33using System.Diagnostics;
34using System.Reflection;
35using System.Threading;
36using OpenMetaverse;
37using OpenSim.Framework;
38using OpenSim.Region.Framework.Interfaces;
39using OpenSim.Region.Physics.Manager;
40using OpenSim.Region.Framework.Scenes.Serialization;
41using System.Runtime.Serialization.Formatters.Binary;
42using System.Runtime.Serialization;
43using Timer = System.Timers.Timer;
44using log4net;
45
46namespace OpenSim.Region.Framework.Scenes
47{
48 public class KeyframeTimer
49 {
50 private static Dictionary<Scene, KeyframeTimer> m_timers =
51 new Dictionary<Scene, KeyframeTimer>();
52
53 private Timer m_timer;
54 private Dictionary<KeyframeMotion, object> m_motions = new Dictionary<KeyframeMotion, object>();
55 private object m_lockObject = new object();
56 private object m_timerLock = new object();
57 private const double m_tickDuration = 50.0;
58
59 public double TickDuration
60 {
61 get { return m_tickDuration; }
62 }
63
64 public KeyframeTimer(Scene scene)
65 {
66 m_timer = new Timer();
67 m_timer.Interval = TickDuration;
68 m_timer.AutoReset = true;
69 m_timer.Elapsed += OnTimer;
70 }
71
72 public void Start()
73 {
74 lock (m_timer)
75 {
76 if (!m_timer.Enabled)
77 m_timer.Start();
78 }
79 }
80
81 private void OnTimer(object sender, ElapsedEventArgs ea)
82 {
83 if (!Monitor.TryEnter(m_timerLock))
84 return;
85
86 try
87 {
88 List<KeyframeMotion> motions;
89
90 lock (m_lockObject)
91 {
92 motions = new List<KeyframeMotion>(m_motions.Keys);
93 }
94
95 foreach (KeyframeMotion m in motions)
96 {
97 try
98 {
99 m.OnTimer(TickDuration);
100 }
101 catch (Exception)
102 {
103 // Don't stop processing
104 }
105 }
106 }
107 catch (Exception)
108 {
109 // Keep running no matter what
110 }
111 finally
112 {
113 Monitor.Exit(m_timerLock);
114 }
115 }
116
117 public static void Add(KeyframeMotion motion)
118 {
119 KeyframeTimer timer;
120
121 if (motion.Scene == null)
122 return;
123
124 lock (m_timers)
125 {
126 if (!m_timers.TryGetValue(motion.Scene, out timer))
127 {
128 timer = new KeyframeTimer(motion.Scene);
129 m_timers[motion.Scene] = timer;
130
131 if (!SceneManager.Instance.AllRegionsReady)
132 {
133 // Start the timers only once all the regions are ready. This is required
134 // when using megaregions, because the megaregion is correctly configured
135 // only after all the regions have been loaded. (If we don't do this then
136 // when the prim moves it might think that it crossed into a region.)
137 SceneManager.Instance.OnRegionsReadyStatusChange += delegate(SceneManager sm)
138 {
139 if (sm.AllRegionsReady)
140 timer.Start();
141 };
142 }
143
144 // Check again, in case the regions were started while we were adding the event handler
145 if (SceneManager.Instance.AllRegionsReady)
146 {
147 timer.Start();
148 }
149 }
150 }
151
152 lock (timer.m_lockObject)
153 {
154 timer.m_motions[motion] = null;
155 }
156 }
157
158 public static void Remove(KeyframeMotion motion)
159 {
160 KeyframeTimer timer;
161
162 if (motion.Scene == null)
163 return;
164
165 lock (m_timers)
166 {
167 if (!m_timers.TryGetValue(motion.Scene, out timer))
168 {
169 return;
170 }
171 }
172
173 lock (timer.m_lockObject)
174 {
175 timer.m_motions.Remove(motion);
176 }
177 }
178 }
179
180 [Serializable]
181 public class KeyframeMotion
182 {
183 //private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
184
185 public enum PlayMode : int
186 {
187 Forward = 0,
188 Reverse = 1,
189 Loop = 2,
190 PingPong = 3
191 };
192
193 [Flags]
194 public enum DataFormat : int
195 {
196 Translation = 2,
197 Rotation = 1
198 }
199
200 [Serializable]
201 public struct Keyframe
202 {
203 public Vector3? Position;
204 public Quaternion? Rotation;
205 public Quaternion StartRotation;
206 public int TimeMS;
207 public int TimeTotal;
208 public Vector3 AngularVelocity;
209 public Vector3 StartPosition;
210 };
211
212 private Vector3 m_serializedPosition;
213 private Vector3 m_basePosition;
214 private Quaternion m_baseRotation;
215
216 private Keyframe m_currentFrame;
217
218 private List<Keyframe> m_frames = new List<Keyframe>();
219
220 private Keyframe[] m_keyframes;
221
222 // skip timer events.
223 //timer.stop doesn't assure there aren't event threads still being fired
224 [NonSerialized()]
225 private bool m_timerStopped;
226
227 [NonSerialized()]
228 private bool m_isCrossing;
229
230 [NonSerialized()]
231 private bool m_waitingCrossing;
232
233 // retry position for cross fail
234 [NonSerialized()]
235 private Vector3 m_nextPosition;
236
237 [NonSerialized()]
238 private SceneObjectGroup m_group;
239
240 private PlayMode m_mode = PlayMode.Forward;
241 private DataFormat m_data = DataFormat.Translation | DataFormat.Rotation;
242
243 private bool m_running = false;
244
245 [NonSerialized()]
246 private bool m_selected = false;
247
248 private int m_iterations = 0;
249
250 private int m_skipLoops = 0;
251
252 [NonSerialized()]
253 private Scene m_scene;
254
255 public Scene Scene
256 {
257 get { return m_scene; }
258 }
259
260 public DataFormat Data
261 {
262 get { return m_data; }
263 }
264
265 public bool Selected
266 {
267 set
268 {
269 if (m_group != null)
270 {
271 if (!value)
272 {
273 // Once we're let go, recompute positions
274 if (m_selected)
275 UpdateSceneObject(m_group);
276 }
277 else
278 {
279 // Save selection position in case we get moved
280 if (!m_selected)
281 {
282 StopTimer();
283 m_serializedPosition = m_group.AbsolutePosition;
284 }
285 }
286 }
287 m_isCrossing = false;
288 m_waitingCrossing = false;
289 m_selected = value;
290 }
291 }
292
293 private void StartTimer()
294 {
295 KeyframeTimer.Add(this);
296 m_timerStopped = false;
297 }
298
299 private void StopTimer()
300 {
301 m_timerStopped = true;
302 KeyframeTimer.Remove(this);
303 }
304
305 public static KeyframeMotion FromData(SceneObjectGroup grp, Byte[] data)
306 {
307 KeyframeMotion newMotion = null;
308
309 try
310 {
311 using (MemoryStream ms = new MemoryStream(data))
312 {
313 BinaryFormatter fmt = new BinaryFormatter();
314 newMotion = (KeyframeMotion)fmt.Deserialize(ms);
315 }
316
317 newMotion.m_group = grp;
318
319 if (grp != null)
320 {
321 newMotion.m_scene = grp.Scene;
322 if (grp.IsSelected)
323 newMotion.m_selected = true;
324 }
325
326 newMotion.m_timerStopped = false;
327 newMotion.m_running = true;
328 newMotion.m_isCrossing = false;
329 newMotion.m_waitingCrossing = false;
330 }
331 catch
332 {
333 newMotion = null;
334 }
335
336 return newMotion;
337 }
338
339 public void UpdateSceneObject(SceneObjectGroup grp)
340 {
341 m_isCrossing = false;
342 m_waitingCrossing = false;
343 StopTimer();
344
345 if (grp == null)
346 return;
347
348 m_group = grp;
349 m_scene = grp.Scene;
350
351 Vector3 grppos = grp.AbsolutePosition;
352 Vector3 offset = grppos - m_serializedPosition;
353 // avoid doing it more than once
354 // current this will happen dragging a prim to other region
355 m_serializedPosition = grppos;
356
357 m_basePosition += offset;
358 m_nextPosition += offset;
359
360 m_currentFrame.StartPosition += offset;
361 m_currentFrame.Position += offset;
362
363 for (int i = 0; i < m_frames.Count; i++)
364 {
365 Keyframe k = m_frames[i];
366 k.StartPosition += offset;
367 k.Position += offset;
368 m_frames[i]=k;
369 }
370
371 if (m_running)
372 Start();
373 }
374
375 public KeyframeMotion(SceneObjectGroup grp, PlayMode mode, DataFormat data)
376 {
377 m_mode = mode;
378 m_data = data;
379
380 m_group = grp;
381 if (grp != null)
382 {
383 m_basePosition = grp.AbsolutePosition;
384 m_baseRotation = grp.GroupRotation;
385 m_scene = grp.Scene;
386 }
387
388 m_timerStopped = true;
389 m_isCrossing = false;
390 m_waitingCrossing = false;
391 }
392
393 public void SetKeyframes(Keyframe[] frames)
394 {
395 m_keyframes = frames;
396 }
397
398 public KeyframeMotion Copy(SceneObjectGroup newgrp)
399 {
400 StopTimer();
401
402 KeyframeMotion newmotion = new KeyframeMotion(null, m_mode, m_data);
403
404 newmotion.m_group = newgrp;
405 newmotion.m_scene = newgrp.Scene;
406
407 if (m_keyframes != null)
408 {
409 newmotion.m_keyframes = new Keyframe[m_keyframes.Length];
410 m_keyframes.CopyTo(newmotion.m_keyframes, 0);
411 }
412
413 newmotion.m_frames = new List<Keyframe>(m_frames);
414
415 newmotion.m_basePosition = m_basePosition;
416 newmotion.m_baseRotation = m_baseRotation;
417
418 if (m_selected)
419 newmotion.m_serializedPosition = m_serializedPosition;
420 else
421 {
422 if (m_group != null)
423 newmotion.m_serializedPosition = m_group.AbsolutePosition;
424 else
425 newmotion.m_serializedPosition = m_serializedPosition;
426 }
427
428 newmotion.m_currentFrame = m_currentFrame;
429
430 newmotion.m_iterations = m_iterations;
431 newmotion.m_running = m_running;
432
433 if (m_running && !m_waitingCrossing)
434 StartTimer();
435
436 return newmotion;
437 }
438
439 public void Delete()
440 {
441 m_running = false;
442 StopTimer();
443 m_isCrossing = false;
444 m_waitingCrossing = false;
445 m_frames.Clear();
446 m_keyframes = null;
447 }
448
449 public void Start()
450 {
451 m_isCrossing = false;
452 m_waitingCrossing = false;
453 if (m_keyframes != null && m_group != null && m_keyframes.Length > 0)
454 {
455 StartTimer();
456 m_running = true;
457 m_group.Scene.EventManager.TriggerMovingStartEvent(m_group.RootPart.LocalId);
458 }
459 else
460 {
461 m_running = false;
462 StopTimer();
463 }
464 }
465
466 public void Stop()
467 {
468 m_running = false;
469 m_isCrossing = false;
470 m_waitingCrossing = false;
471
472 StopTimer();
473
474 m_basePosition = m_group.AbsolutePosition;
475 m_baseRotation = m_group.GroupRotation;
476
477 m_group.RootPart.Velocity = Vector3.Zero;
478 m_group.RootPart.AngularVelocity = Vector3.Zero;
479 m_group.SendGroupRootTerseUpdate();
480// m_group.RootPart.ScheduleTerseUpdate();
481 m_frames.Clear();
482 }
483
484 public void Pause()
485 {
486 m_running = false;
487 StopTimer();
488
489 m_group.RootPart.Velocity = Vector3.Zero;
490 m_group.RootPart.AngularVelocity = Vector3.Zero;
491 m_group.SendGroupRootTerseUpdate();
492// m_group.RootPart.ScheduleTerseUpdate();
493
494 }
495
496 private void GetNextList()
497 {
498 m_frames.Clear();
499 Vector3 pos = m_basePosition;
500 Quaternion rot = m_baseRotation;
501
502 if (m_mode == PlayMode.Loop || m_mode == PlayMode.PingPong || m_iterations == 0)
503 {
504 int direction = 1;
505 if (m_mode == PlayMode.Reverse || ((m_mode == PlayMode.PingPong) && ((m_iterations & 1) != 0)))
506 direction = -1;
507
508 int start = 0;
509 int end = m_keyframes.Length;
510
511 if (direction < 0)
512 {
513 start = m_keyframes.Length - 1;
514 end = -1;
515 }
516
517 for (int i = start; i != end ; i += direction)
518 {
519 Keyframe k = m_keyframes[i];
520
521 k.StartPosition = pos;
522 if (k.Position.HasValue)
523 {
524 k.Position = (k.Position * direction);
525// k.Velocity = (Vector3)k.Position / (k.TimeMS / 1000.0f);
526 k.Position += pos;
527 }
528 else
529 {
530 k.Position = pos;
531// k.Velocity = Vector3.Zero;
532 }
533
534 k.StartRotation = rot;
535 if (k.Rotation.HasValue)
536 {
537 if (direction == -1)
538 k.Rotation = Quaternion.Conjugate((Quaternion)k.Rotation);
539 k.Rotation = rot * k.Rotation;
540 }
541 else
542 {
543 k.Rotation = rot;
544 }
545
546/* ang vel not in use for now
547
548 float angle = 0;
549
550 float aa = k.StartRotation.X * k.StartRotation.X + k.StartRotation.Y * k.StartRotation.Y + k.StartRotation.Z * k.StartRotation.Z + k.StartRotation.W * k.StartRotation.W;
551 float bb = ((Quaternion)k.Rotation).X * ((Quaternion)k.Rotation).X + ((Quaternion)k.Rotation).Y * ((Quaternion)k.Rotation).Y + ((Quaternion)k.Rotation).Z * ((Quaternion)k.Rotation).Z + ((Quaternion)k.Rotation).W * ((Quaternion)k.Rotation).W;
552 float aa_bb = aa * bb;
553
554 if (aa_bb == 0)
555 {
556 angle = 0;
557 }
558 else
559 {
560 float ab = k.StartRotation.X * ((Quaternion)k.Rotation).X +
561 k.StartRotation.Y * ((Quaternion)k.Rotation).Y +
562 k.StartRotation.Z * ((Quaternion)k.Rotation).Z +
563 k.StartRotation.W * ((Quaternion)k.Rotation).W;
564 float q = (ab * ab) / aa_bb;
565
566 if (q > 1.0f)
567 {
568 angle = 0;
569 }
570 else
571 {
572 angle = (float)Math.Acos(2 * q - 1);
573 }
574 }
575
576 k.AngularVelocity = (new Vector3(0, 0, 1) * (Quaternion)k.Rotation) * (angle / (k.TimeMS / 1000));
577 */
578 k.TimeTotal = k.TimeMS;
579
580 m_frames.Add(k);
581
582 pos = (Vector3)k.Position;
583 rot = (Quaternion)k.Rotation;
584 }
585
586 m_basePosition = pos;
587 m_baseRotation = rot;
588
589 m_iterations++;
590 }
591 }
592
593 public void OnTimer(double tickDuration)
594 {
595 if (m_skipLoops > 0)
596 {
597 m_skipLoops--;
598 return;
599 }
600
601 if (m_timerStopped) // trap events still in air even after a timer.stop
602 return;
603
604 if (m_group == null)
605 return;
606
607 bool update = false;
608
609 if (m_selected)
610 {
611 if (m_group.RootPart.Velocity != Vector3.Zero)
612 {
613 m_group.RootPart.Velocity = Vector3.Zero;
614 m_group.SendGroupRootTerseUpdate();
615
616 }
617 return;
618 }
619
620 if (m_isCrossing)
621 {
622 // if crossing and timer running then cross failed
623 // wait some time then
624 // retry to set the position that evtually caused the outbound
625 // if still outside region this will call startCrossing below
626 m_isCrossing = false;
627 m_group.AbsolutePosition = m_nextPosition;
628 if (!m_isCrossing)
629 {
630 StopTimer();
631 StartTimer();
632 }
633 return;
634 }
635
636 if (m_frames.Count == 0)
637 {
638 if (!m_running) return;
639
640 GetNextList();
641
642 if (m_frames.Count == 0)
643 {
644 Stop();
645// Scene scene = m_group.Scene;
646//
647// IScriptModule[] scriptModules = scene.RequestModuleInterfaces<IScriptModule>();
648// foreach (IScriptModule m in scriptModules)
649// {
650// if (m == null)
651// continue;
652// m.PostObjectEvent(m_group.RootPart.UUID, "moving_end", new object[0]);
653// }
654
655 m_group.Scene.EventManager.TriggerMovingEndEvent(m_group.RootPart.LocalId);
656
657 return;
658 }
659
660 m_currentFrame = m_frames[0];
661 m_currentFrame.TimeMS += (int)tickDuration;
662
663 //force a update on a keyframe transition
664 update = true;
665 }
666
667 m_currentFrame.TimeMS -= (int)tickDuration;
668
669 // Do the frame processing
670 double remainingSteps = (double)m_currentFrame.TimeMS / tickDuration;
671
672 if (remainingSteps <= 0.0)
673 {
674 m_group.RootPart.Velocity = Vector3.Zero;
675 m_group.RootPart.AngularVelocity = Vector3.Zero;
676
677 m_nextPosition = (Vector3)m_currentFrame.Position;
678 m_group.AbsolutePosition = m_nextPosition;
679
680 // we are sending imediate updates, no doing force a extra terseUpdate
681 // m_group.UpdateGroupRotationR((Quaternion)m_currentFrame.Rotation);
682
683 m_group.RootPart.RotationOffset = (Quaternion)m_currentFrame.Rotation;
684 m_frames.RemoveAt(0);
685 if (m_frames.Count > 0)
686 m_currentFrame = m_frames[0];
687
688 update = true;
689 }
690 else
691 {
692 float completed = ((float)m_currentFrame.TimeTotal - (float)m_currentFrame.TimeMS) / (float)m_currentFrame.TimeTotal;
693 bool lastStep = m_currentFrame.TimeMS <= tickDuration;
694
695 Vector3 positionThisStep = m_currentFrame.StartPosition + (m_currentFrame.Position.Value - m_currentFrame.StartPosition) * completed;
696 Vector3 motionThisStep = positionThisStep - m_group.AbsolutePosition;
697
698 float mag = Vector3.Mag(motionThisStep);
699
700 if ((mag >= 0.02f) || lastStep)
701 {
702 m_nextPosition = m_group.AbsolutePosition + motionThisStep;
703 m_group.AbsolutePosition = m_nextPosition;
704 update = true;
705 }
706
707 //int totalSteps = m_currentFrame.TimeTotal / (int)tickDuration;
708 //m_log.DebugFormat("KeyframeMotion.OnTimer: step {0}/{1}, curPosition={2}, finalPosition={3}, motionThisStep={4} (scene {5})",
709 // totalSteps - remainingSteps + 1, totalSteps, m_group.AbsolutePosition, m_currentFrame.Position, motionThisStep, m_scene.RegionInfo.RegionName);
710
711 if ((Quaternion)m_currentFrame.Rotation != m_group.GroupRotation)
712 {
713 Quaternion current = m_group.GroupRotation;
714
715 Quaternion step = Quaternion.Slerp(m_currentFrame.StartRotation, (Quaternion)m_currentFrame.Rotation, completed);
716 step.Normalize();
717/* use simpler change detection
718* float angle = 0;
719
720 float aa = current.X * current.X + current.Y * current.Y + current.Z * current.Z + current.W * current.W;
721 float bb = step.X * step.X + step.Y * step.Y + step.Z * step.Z + step.W * step.W;
722 float aa_bb = aa * bb;
723
724 if (aa_bb == 0)
725 {
726 angle = 0;
727 }
728 else
729 {
730 float ab = current.X * step.X +
731 current.Y * step.Y +
732 current.Z * step.Z +
733 current.W * step.W;
734 float q = (ab * ab) / aa_bb;
735
736 if (q > 1.0f)
737 {
738 angle = 0;
739 }
740 else
741 {
742 angle = (float)Math.Acos(2 * q - 1);
743 }
744 }
745
746 if (angle > 0.01f)
747*/
748 if(Math.Abs(step.X - current.X) > 0.001f
749 || Math.Abs(step.Y - current.Y) > 0.001f
750 || Math.Abs(step.Z - current.Z) > 0.001f
751 || lastStep)
752 // assuming w is a dependente var
753
754 {
755// m_group.UpdateGroupRotationR(step);
756 m_group.RootPart.RotationOffset = step;
757
758 //m_group.RootPart.UpdateAngularVelocity(m_currentFrame.AngularVelocity / 2);
759 update = true;
760 }
761 }
762 }
763
764 if (update)
765 {
766 m_group.SendGroupRootTerseUpdate();
767 }
768 }
769
770 public Byte[] Serialize()
771 {
772 StopTimer();
773
774 SceneObjectGroup tmp = m_group;
775 m_group = null;
776 if (!m_selected && tmp != null)
777 m_serializedPosition = tmp.AbsolutePosition;
778
779 using (MemoryStream ms = new MemoryStream())
780 {
781 BinaryFormatter fmt = new BinaryFormatter();
782 fmt.Serialize(ms, this);
783 m_group = tmp;
784 if (m_running && !m_waitingCrossing)
785 StartTimer();
786
787 return ms.ToArray();
788 }
789 }
790
791 public void StartCrossingCheck()
792 {
793 // timer will be restart by crossingFailure
794 // or never since crossing worked and this
795 // should be deleted
796 StopTimer();
797
798 m_isCrossing = true;
799 m_waitingCrossing = true;
800
801 // to remove / retune to smoth crossings
802 if (m_group.RootPart.Velocity != Vector3.Zero)
803 {
804 m_group.RootPart.Velocity = Vector3.Zero;
805 m_group.SendGroupRootTerseUpdate();
806// m_group.RootPart.ScheduleTerseUpdate();
807 }
808 }
809
810 public void CrossingFailure()
811 {
812 m_waitingCrossing = false;
813
814 if (m_group != null)
815 {
816 m_group.RootPart.Velocity = Vector3.Zero;
817 m_group.SendGroupRootTerseUpdate();
818// m_group.RootPart.ScheduleTerseUpdate();
819
820 if (m_running)
821 {
822 StopTimer();
823 m_skipLoops = 1200; // 60 seconds
824 StartTimer();
825 }
826 }
827 }
828 }
829}