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.cs546
1 files changed, 409 insertions, 137 deletions
diff --git a/OpenSim/Region/Framework/Scenes/KeyframeMotion.cs b/OpenSim/Region/Framework/Scenes/KeyframeMotion.cs
index b7b0d27..e4e6f2c 100644
--- a/OpenSim/Region/Framework/Scenes/KeyframeMotion.cs
+++ b/OpenSim/Region/Framework/Scenes/KeyframeMotion.cs
@@ -38,8 +38,8 @@ namespace OpenSim.Region.Framework.Scenes
38 [Flags] 38 [Flags]
39 public enum DataFormat : int 39 public enum DataFormat : int
40 { 40 {
41 Translation = 1, 41 Translation = 2,
42 Rotation = 2 42 Rotation = 1
43 } 43 }
44 44
45 [Serializable] 45 [Serializable]
@@ -53,17 +53,42 @@ namespace OpenSim.Region.Framework.Scenes
53 public Vector3 AngularVelocity; 53 public Vector3 AngularVelocity;
54 }; 54 };
55 55
56 private Vector3 m_serializedPosition;
56 private Vector3 m_basePosition; 57 private Vector3 m_basePosition;
57 private Quaternion m_baseRotation; 58 private Quaternion m_baseRotation;
58 private Vector3 m_serializedPosition;
59 59
60 private Keyframe m_currentFrame; 60 private Keyframe m_currentFrame;
61
61 private List<Keyframe> m_frames = new List<Keyframe>(); 62 private List<Keyframe> m_frames = new List<Keyframe>();
62 63
63 private Keyframe[] m_keyframes; 64 private Keyframe[] m_keyframes;
64 65
65 [NonSerialized()] 66 [NonSerialized()]
66 protected Timer m_timer = new Timer(); 67 protected Timer m_timer = null;
68
69 // timer lock
70 [NonSerialized()]
71 private object m_onTimerLock;
72
73 // timer overrun detect
74 // prevents overlap or timer events threads frozen on the lock
75 [NonSerialized()]
76 private bool m_inOnTimer;
77
78 // skip timer events.
79 //timer.stop doesn't assure there aren't event threads still being fired
80 [NonSerialized()]
81 private bool m_timerStopped;
82
83 [NonSerialized()]
84 private bool m_isCrossing;
85
86 [NonSerialized()]
87 private bool m_waitingCrossing;
88
89 // retry position for cross fail
90 [NonSerialized()]
91 private Vector3 m_nextPosition;
67 92
68 [NonSerialized()] 93 [NonSerialized()]
69 private SceneObjectGroup m_group; 94 private SceneObjectGroup m_group;
@@ -88,54 +113,120 @@ namespace OpenSim.Region.Framework.Scenes
88 { 113 {
89 set 114 set
90 { 115 {
91 if (value) 116 if (m_group != null)
92 { 117 {
93 // Once we're let go, recompute positions 118 if (!value)
94 if (m_selected) 119 {
95 UpdateSceneObject(m_group); 120 // Once we're let go, recompute positions
96 } 121 if (m_selected)
97 else 122 UpdateSceneObject(m_group);
98 { 123 }
99 // Save selection position in case we get moved 124 else
100 if (!m_selected) 125 {
101 m_serializedPosition = m_group.AbsolutePosition; 126 // Save selection position in case we get moved
127 if (!m_selected)
128 {
129 StopTimer();
130 m_serializedPosition = m_group.AbsolutePosition;
131 }
132 }
102 } 133 }
103 m_selected = value; } 134 m_isCrossing = false;
135 m_waitingCrossing = false;
136 m_selected = value;
137 }
104 } 138 }
105 139
140 private void StartTimer()
141 {
142 if (m_timer == null)
143 return;
144 m_timerStopped = false;
145 m_timer.Start();
146 }
147
148 private void StopTimer()
149 {
150 if (m_timer == null || m_timerStopped)
151 return;
152 m_timerStopped = true;
153 m_timer.Stop();
154 }
155
156 private void RemoveTimer()
157 {
158 if (m_timer == null)
159 return;
160 m_timerStopped = true;
161 m_timer.Stop();
162 m_timer.Elapsed -= OnTimer;
163 m_timer = null;
164 }
165
166
106 public static KeyframeMotion FromData(SceneObjectGroup grp, Byte[] data) 167 public static KeyframeMotion FromData(SceneObjectGroup grp, Byte[] data)
107 { 168 {
108 MemoryStream ms = new MemoryStream(data); 169 KeyframeMotion newMotion = null;
109 170
110 BinaryFormatter fmt = new BinaryFormatter(); 171 try
172 {
173 MemoryStream ms = new MemoryStream(data);
174 BinaryFormatter fmt = new BinaryFormatter();
175
176 newMotion = (KeyframeMotion)fmt.Deserialize(ms);
177
178 newMotion.m_group = grp;
111 179
112 KeyframeMotion newMotion = (KeyframeMotion)fmt.Deserialize(ms); 180 if (grp != null && grp.IsSelected)
181 newMotion.m_selected = true;
113 182
114 // This will be started when position is updated 183 newMotion.m_onTimerLock = new object();
115 newMotion.m_timer = new Timer(); 184 newMotion.m_timerStopped = false;
116 newMotion.m_timer.Interval = (int)timerInterval; 185 newMotion.m_inOnTimer = false;
117 newMotion.m_timer.AutoReset = true; 186 newMotion.m_isCrossing = false;
118 newMotion.m_timer.Elapsed += newMotion.OnTimer; 187 newMotion.m_waitingCrossing = false;
188 }
189 catch
190 {
191 newMotion = null;
192 }
119 193
120 return newMotion; 194 return newMotion;
121 } 195 }
122 196
123 public void UpdateSceneObject(SceneObjectGroup grp) 197 public void UpdateSceneObject(SceneObjectGroup grp)
124 { 198 {
125 m_group = grp; 199// lock (m_onTimerLock)
126 Vector3 offset = grp.AbsolutePosition - m_serializedPosition;
127
128 m_basePosition += offset;
129 m_currentFrame.Position += offset;
130 for (int i = 0 ; i < m_frames.Count ; i++)
131 { 200 {
132 Keyframe k = m_frames[i]; 201 m_isCrossing = false;
133 k.Position += offset; 202 m_waitingCrossing = false;
134 m_frames[i] = k; 203 StopTimer();
135 } 204
205 if (grp == null)
206 return;
207
208 m_group = grp;
209 Vector3 grppos = grp.AbsolutePosition;
210 Vector3 offset = grppos - m_serializedPosition;
211 // avoid doing it more than once
212 // current this will happen draging a prim to other region
213 m_serializedPosition = grppos;
214
215 m_basePosition += offset;
216 m_currentFrame.Position += offset;
136 217
137 if (m_running) 218 m_nextPosition += offset;
138 Start(); 219
220 for (int i = 0; i < m_frames.Count; i++)
221 {
222 Keyframe k = m_frames[i];
223 k.Position += offset;
224 m_frames[i]=k;
225 }
226
227 if (m_running)
228 Start();
229 }
139 } 230 }
140 231
141 public KeyframeMotion(SceneObjectGroup grp, PlayMode mode, DataFormat data) 232 public KeyframeMotion(SceneObjectGroup grp, PlayMode mode, DataFormat data)
@@ -144,12 +235,17 @@ namespace OpenSim.Region.Framework.Scenes
144 m_data = data; 235 m_data = data;
145 236
146 m_group = grp; 237 m_group = grp;
147 m_basePosition = grp.AbsolutePosition; 238 if (grp != null)
148 m_baseRotation = grp.GroupRotation; 239 {
240 m_basePosition = grp.AbsolutePosition;
241 m_baseRotation = grp.GroupRotation;
242 }
149 243
150 m_timer.Interval = (int)timerInterval; 244 m_onTimerLock = new object();
151 m_timer.AutoReset = true; 245 m_timerStopped = true;
152 m_timer.Elapsed += OnTimer; 246 m_inOnTimer = false;
247 m_isCrossing = false;
248 m_waitingCrossing = false;
153 } 249 }
154 250
155 public void SetKeyframes(Keyframe[] frames) 251 public void SetKeyframes(Keyframe[] frames)
@@ -157,19 +253,93 @@ namespace OpenSim.Region.Framework.Scenes
157 m_keyframes = frames; 253 m_keyframes = frames;
158 } 254 }
159 255
256 public KeyframeMotion Copy(SceneObjectGroup newgrp)
257 {
258 StopTimer();
259
260 KeyframeMotion newmotion = new KeyframeMotion(null, m_mode, m_data);
261
262 newmotion.m_group = newgrp;
263
264 if (m_keyframes != null)
265 {
266 newmotion.m_keyframes = new Keyframe[m_keyframes.Length];
267 m_keyframes.CopyTo(newmotion.m_keyframes, 0);
268 }
269
270 newmotion.m_frames = new List<Keyframe>(m_frames);
271
272 newmotion.m_basePosition = m_basePosition;
273 newmotion.m_baseRotation = m_baseRotation;
274
275 newmotion.m_currentFrame = m_currentFrame;
276
277 if (m_selected)
278 newmotion.m_serializedPosition = m_serializedPosition;
279 else
280 {
281 if (m_group != null)
282 newmotion.m_serializedPosition = m_group.AbsolutePosition;
283 else
284 newmotion.m_serializedPosition = m_serializedPosition;
285 }
286
287 newmotion.m_iterations = m_iterations;
288 newmotion.m_running = m_running;
289
290 if (m_running && !m_waitingCrossing)
291 StartTimer();
292
293 return newmotion;
294 }
295
296 public void Delete()
297 {
298 m_running = false;
299 RemoveTimer();
300 m_isCrossing = false;
301 m_waitingCrossing = false;
302 m_frames.Clear();
303 m_keyframes = null;
304 }
305
160 public void Start() 306 public void Start()
161 { 307 {
162 if (m_keyframes.Length > 0) 308 m_isCrossing = false;
163 m_timer.Start(); 309 m_waitingCrossing = false;
164 m_running = true; 310 if (m_keyframes != null && m_group != null && m_keyframes.Length > 0)
311 {
312 if (m_timer == null)
313 {
314 m_timer = new Timer();
315 m_timer.Interval = timerInterval;
316 m_timer.AutoReset = true;
317 m_timer.Elapsed += OnTimer;
318 }
319 else
320 {
321 StopTimer();
322 m_timer.Interval = timerInterval;
323 }
324
325 m_inOnTimer = false;
326 StartTimer();
327 m_running = true;
328 }
329 else
330 {
331 m_running = false;
332 RemoveTimer();
333 }
165 } 334 }
166 335
167 public void Stop() 336 public void Stop()
168 { 337 {
169 // Failed object creation 338 m_running = false;
170 if (m_timer == null) 339 m_isCrossing = false;
171 return; 340 m_waitingCrossing = false;
172 m_timer.Stop(); 341
342 RemoveTimer();
173 343
174 m_basePosition = m_group.AbsolutePosition; 344 m_basePosition = m_group.AbsolutePosition;
175 m_baseRotation = m_group.GroupRotation; 345 m_baseRotation = m_group.GroupRotation;
@@ -179,17 +349,16 @@ namespace OpenSim.Region.Framework.Scenes
179 m_group.SendGroupRootTerseUpdate(); 349 m_group.SendGroupRootTerseUpdate();
180 350
181 m_frames.Clear(); 351 m_frames.Clear();
182 m_running = false;
183 } 352 }
184 353
185 public void Pause() 354 public void Pause()
186 { 355 {
356 m_running = false;
357 RemoveTimer();
358
187 m_group.RootPart.Velocity = Vector3.Zero; 359 m_group.RootPart.Velocity = Vector3.Zero;
188 m_group.RootPart.UpdateAngularVelocity(Vector3.Zero); 360 m_group.RootPart.UpdateAngularVelocity(Vector3.Zero);
189 m_group.SendGroupRootTerseUpdate(); 361 m_group.SendGroupRootTerseUpdate();
190
191 m_timer.Stop();
192 m_running = false;
193 } 362 }
194 363
195 private void GetNextList() 364 private void GetNextList()
@@ -222,9 +391,16 @@ namespace OpenSim.Region.Framework.Scenes
222 Keyframe k = m_keyframes[i]; 391 Keyframe k = m_keyframes[i];
223 392
224 if (k.Position.HasValue) 393 if (k.Position.HasValue)
225 k.Position = (k.Position * direction) + pos; 394 {
395 k.Position = (k.Position * direction);
396// k.Velocity = (Vector3)k.Position / (k.TimeMS / 1000.0f);
397 k.Position += pos;
398 }
226 else 399 else
400 {
227 k.Position = pos; 401 k.Position = pos;
402// k.Velocity = Vector3.Zero;
403 }
228 404
229 k.StartRotation = rot; 405 k.StartRotation = rot;
230 if (k.Rotation.HasValue) 406 if (k.Rotation.HasValue)
@@ -238,6 +414,8 @@ namespace OpenSim.Region.Framework.Scenes
238 k.Rotation = rot; 414 k.Rotation = rot;
239 } 415 }
240 416
417/* ang vel not in use for now
418
241 float angle = 0; 419 float angle = 0;
242 420
243 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; 421 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;
@@ -267,6 +445,7 @@ namespace OpenSim.Region.Framework.Scenes
267 } 445 }
268 446
269 k.AngularVelocity = (new Vector3(0, 0, 1) * (Quaternion)k.Rotation) * (angle / (k.TimeMS / 1000)); 447 k.AngularVelocity = (new Vector3(0, 0, 1) * (Quaternion)k.Rotation) * (angle / (k.TimeMS / 1000));
448 */
270 k.TimeTotal = k.TimeMS; 449 k.TimeTotal = k.TimeMS;
271 450
272 m_frames.Add(k); 451 m_frames.Add(k);
@@ -284,139 +463,232 @@ namespace OpenSim.Region.Framework.Scenes
284 463
285 protected void OnTimer(object sender, ElapsedEventArgs e) 464 protected void OnTimer(object sender, ElapsedEventArgs e)
286 { 465 {
287 if (m_frames.Count == 0) 466 if (m_timerStopped) // trap events still in air even after a timer.stop
288 { 467 return;
289 GetNextList();
290
291 if (m_frames.Count == 0)
292 {
293 Stop();
294 return;
295 }
296
297 m_currentFrame = m_frames[0];
298 }
299 468
300 if (m_selected) 469 if (m_inOnTimer) // don't let overruns to happen
301 { 470 {
302 if (m_group.RootPart.Velocity != Vector3.Zero) 471 m_log.Warn("[KeyFrame]: timer overrun");
303 {
304 m_group.RootPart.Velocity = Vector3.Zero;
305 m_group.SendGroupRootTerseUpdate();
306 }
307 return; 472 return;
308 } 473 }
309 474
310 // Do the frame processing 475 if (m_group == null)
311 double steps = (double)m_currentFrame.TimeMS / timerInterval; 476 return;
312 float complete = ((float)m_currentFrame.TimeTotal - (float)m_currentFrame.TimeMS) / (float)m_currentFrame.TimeTotal;
313 477
314 if (steps <= 1.0) 478 lock (m_onTimerLock)
315 { 479 {
316 m_currentFrame.TimeMS = 0;
317 480
318 m_group.AbsolutePosition = (Vector3)m_currentFrame.Position; 481 m_inOnTimer = true;
319 m_group.UpdateGroupRotationR((Quaternion)m_currentFrame.Rotation);
320 }
321 else
322 {
323 Vector3 v = (Vector3)m_currentFrame.Position - m_group.AbsolutePosition;
324 Vector3 motionThisFrame = v / (float)steps;
325 v = v * 1000 / m_currentFrame.TimeMS;
326 482
327 bool update = false; 483 bool update = false;
328 484
329 if (Vector3.Mag(motionThisFrame) >= 0.05f) 485 try
330 { 486 {
331 m_group.AbsolutePosition += motionThisFrame; 487 if (m_selected)
332 m_group.RootPart.Velocity = v; 488 {
333 update = true; 489 if (m_group.RootPart.Velocity != Vector3.Zero)
334 } 490 {
491 m_group.RootPart.Velocity = Vector3.Zero;
492 m_group.SendGroupRootTerseUpdate();
493 }
494 m_inOnTimer = false;
495 return;
496 }
335 497
336 if ((Quaternion)m_currentFrame.Rotation != m_group.GroupRotation) 498 if (m_isCrossing)
337 { 499 {
338 Quaternion current = m_group.GroupRotation; 500 // if crossing and timer running then cross failed
501 // wait some time then
502 // retry to set the position that evtually caused the outbound
503 // if still outside region this will call startCrossing below
504 m_isCrossing = false;
505 m_group.AbsolutePosition = m_nextPosition;
506 if (!m_isCrossing)
507 {
508 StopTimer();
509 m_timer.Interval = timerInterval;
510 StartTimer();
511 }
512 m_inOnTimer = false;
513 return;
514 }
339 515
340 Quaternion step = Quaternion.Slerp(m_currentFrame.StartRotation, (Quaternion)m_currentFrame.Rotation, complete); 516 if (m_frames.Count == 0)
517 {
518 GetNextList();
341 519
342 float angle = 0; 520 if (m_frames.Count == 0)
521 {
522 Stop();
523 m_inOnTimer = false;
524 return;
525 }
343 526
344 float aa = current.X * current.X + current.Y * current.Y + current.Z * current.Z + current.W * current.W; 527 m_currentFrame = m_frames[0];
345 float bb = step.X * step.X + step.Y * step.Y + step.Z * step.Z + step.W * step.W; 528 m_currentFrame.TimeMS += (int)timerInterval;
346 float aa_bb = aa * bb;
347 529
348 if (aa_bb == 0) 530 //force a update on a keyframe transition
531 update = true;
532 }
533
534 m_currentFrame.TimeMS -= (int)timerInterval;
535
536 // Do the frame processing
537 double steps = (double)m_currentFrame.TimeMS / timerInterval;
538
539 if (steps <= 0.0)
349 { 540 {
350 angle = 0; 541 m_group.RootPart.Velocity = Vector3.Zero;
542 m_group.RootPart.UpdateAngularVelocity(Vector3.Zero);
543
544 m_nextPosition = (Vector3)m_currentFrame.Position;
545 m_group.AbsolutePosition = m_nextPosition;
546
547 m_group.UpdateGroupRotationR((Quaternion)m_currentFrame.Rotation);
548
549 m_frames.RemoveAt(0);
550 if (m_frames.Count > 0)
551 m_currentFrame = m_frames[0];
552
553 update = true;
351 } 554 }
352 else 555 else
353 { 556 {
354 float ab = current.X * step.X + 557 float complete = ((float)m_currentFrame.TimeTotal - (float)m_currentFrame.TimeMS) / (float)m_currentFrame.TimeTotal;
355 current.Y * step.Y +
356 current.Z * step.Z +
357 current.W * step.W;
358 float q = (ab * ab) / aa_bb;
359 558
360 if (q > 1.0f) 559 Vector3 v = (Vector3)m_currentFrame.Position - m_group.AbsolutePosition;
560 Vector3 motionThisFrame = v / (float)steps;
561 v = v * 1000 / m_currentFrame.TimeMS;
562
563 if (Vector3.Mag(motionThisFrame) >= 0.05f)
361 { 564 {
362 angle = 0; 565 // m_group.AbsolutePosition += motionThisFrame;
566 m_nextPosition = m_group.AbsolutePosition + motionThisFrame;
567 m_group.AbsolutePosition = m_nextPosition;
568
569 m_group.RootPart.Velocity = v;
570 update = true;
363 } 571 }
364 else 572
573 if ((Quaternion)m_currentFrame.Rotation != m_group.GroupRotation)
365 { 574 {
366 angle = (float)Math.Acos(2 * q - 1); 575 Quaternion current = m_group.GroupRotation;
576
577 Quaternion step = Quaternion.Slerp(m_currentFrame.StartRotation, (Quaternion)m_currentFrame.Rotation, complete);
578/* use simpler change detection
579 * float angle = 0;
580
581 float aa = current.X * current.X + current.Y * current.Y + current.Z * current.Z + current.W * current.W;
582 float bb = step.X * step.X + step.Y * step.Y + step.Z * step.Z + step.W * step.W;
583 float aa_bb = aa * bb;
584
585 if (aa_bb == 0)
586 {
587 angle = 0;
588 }
589 else
590 {
591 float ab = current.X * step.X +
592 current.Y * step.Y +
593 current.Z * step.Z +
594 current.W * step.W;
595 float q = (ab * ab) / aa_bb;
596
597 if (q > 1.0f)
598 {
599 angle = 0;
600 }
601 else
602 {
603 angle = (float)Math.Acos(2 * q - 1);
604 }
605 }
606
607 if (angle > 0.01f)
608 */
609 if(Math.Abs(step.X - current.X) > 0.001f
610 || Math.Abs(step.Y - current.Y) > 0.001f
611 || Math.Abs(step.Z - current.Z) > 0.001f)
612 // assuming w is a dependente var
613
614 {
615 m_group.UpdateGroupRotationR(step);
616 //m_group.RootPart.UpdateAngularVelocity(m_currentFrame.AngularVelocity / 2);
617 update = true;
618 }
367 } 619 }
368 } 620 }
369 621
370 if (angle > 0.01f) 622 if (update)
371 { 623 m_group.SendGroupRootTerseUpdate();
372 m_group.UpdateGroupRotationR(step);
373 //m_group.RootPart.UpdateAngularVelocity(m_currentFrame.AngularVelocity / 2);
374 update = true;
375 }
376 }
377
378 if (update)
379 m_group.SendGroupRootTerseUpdate();
380 }
381
382 m_currentFrame.TimeMS -= (int)timerInterval;
383 624
384 if (m_currentFrame.TimeMS <= 0) 625 }
385 { 626 catch ( Exception ex)
386 m_group.RootPart.Velocity = Vector3.Zero; 627 {
387 m_group.RootPart.UpdateAngularVelocity(Vector3.Zero); 628 // still happening sometimes
388 m_group.SendGroupRootTerseUpdate(); 629 // lets try to see where
630 m_log.Warn("[KeyFrame]: timer overrun" + ex.Message);
631 }
389 632
390 m_frames.RemoveAt(0); 633 finally
391 if (m_frames.Count > 0) 634 {
392 m_currentFrame = m_frames[0]; 635 // make sure we do not let this frozen
636 m_inOnTimer = false;
637 }
393 } 638 }
394 } 639 }
395 640
396 public Byte[] Serialize() 641 public Byte[] Serialize()
397 { 642 {
643 StopTimer();
398 MemoryStream ms = new MemoryStream(); 644 MemoryStream ms = new MemoryStream();
399 m_timer.Stop();
400 645
401 BinaryFormatter fmt = new BinaryFormatter(); 646 BinaryFormatter fmt = new BinaryFormatter();
402 SceneObjectGroup tmp = m_group; 647 SceneObjectGroup tmp = m_group;
403 m_group = null; 648 m_group = null;
404 m_serializedPosition = tmp.AbsolutePosition; 649 if (!m_selected && tmp != null)
650 m_serializedPosition = tmp.AbsolutePosition;
405 fmt.Serialize(ms, this); 651 fmt.Serialize(ms, this);
406 m_group = tmp; 652 m_group = tmp;
653 if (m_running && !m_waitingCrossing)
654 StartTimer();
655
407 return ms.ToArray(); 656 return ms.ToArray();
408 } 657 }
409 658
659 public void StartCrossingCheck()
660 {
661 // timer will be restart by crossingFailure
662 // or never since crossing worked and this
663 // should be deleted
664 StopTimer();
665
666 m_isCrossing = true;
667 m_waitingCrossing = true;
668
669// to remove / retune to smoth crossings
670 if (m_group.RootPart.Velocity != Vector3.Zero)
671 {
672 m_group.RootPart.Velocity = Vector3.Zero;
673 m_group.SendGroupRootTerseUpdate();
674 }
675 }
676
410 public void CrossingFailure() 677 public void CrossingFailure()
411 { 678 {
412 // The serialization has stopped the timer, so let's wait a moment 679 m_waitingCrossing = false;
413 // then retry the crossing. We'll get back here if it fails. 680
414 Util.FireAndForget(delegate (object x) 681 if (m_group != null)
415 { 682 {
416 Thread.Sleep(60000); 683 m_group.RootPart.Velocity = Vector3.Zero;
417 if (m_running) 684 m_group.SendGroupRootTerseUpdate();
418 m_timer.Start(); 685
419 }); 686 if (m_running && m_timer != null)
687 {
688 m_timer.Interval = 60000;
689 StartTimer();
690 }
691 }
420 } 692 }
421 } 693 }
422} 694}