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