Line data Source code
1 : /* ragdoll.cpp: ragdoll physics support
2 : *
3 : * Libprimis supports limited client-side ragdolls to simulate corpses. Ragdolls
4 : * are created when actors are killed and support basic physical interaction with
5 : * terrain (but not other players).
6 : *
7 : * This file contains implementation functions for the ragdoll class. For the
8 : * class definition, see ragdoll.h.
9 : */
10 : #include "../libprimis-headers/cube.h"
11 : #include "../../shared/geomexts.h"
12 :
13 : #include <memory>
14 : #include <optional>
15 : #include <format>
16 :
17 : #include "interface/console.h"
18 : #include "interface/control.h"
19 :
20 : #include "render/rendergl.h"
21 :
22 : #include "world/entities.h"
23 : #include "world/octaworld.h"
24 : #include "world/physics.h"
25 : #include "world/bih.h"
26 :
27 : #include "model.h"
28 : #include "ragdoll.h"
29 :
30 : /* ragdollskel */
31 :
32 : //ragdollskel::tri
33 :
34 5 : bool ragdollskel::tri::shareverts(const tri &t) const
35 : {
36 15 : for(int i = 0; i < 3; ++i)
37 : {
38 43 : for(int j = 0; j < 3; ++j)
39 : {
40 33 : if(vert[i] == t.vert[j])
41 : {
42 3 : return true;
43 : }
44 : }
45 : }
46 2 : return false;
47 : }
48 :
49 : //ragdollskel
50 :
51 1 : void ragdollskel::setupjoints()
52 : {
53 4 : for(vert &i : verts)
54 : {
55 3 : i.weight = 0;
56 : }
57 2 : for(joint &j : joints)
58 : {
59 1 : j.weight = 0;
60 1 : vec pos(0, 0, 0);
61 4 : for(int k = 0; k < 3; ++k)
62 : {
63 3 : if(j.vert[k]>=0) //for each valid vertex (not -1) refered to by this joint (up to 3)
64 : {
65 3 : pos.add(verts[j.vert[k]].pos); //j.vert[k].pos is the positon of the vertex pointed to by the joint being used
66 3 : j.weight++;
67 3 : verts[j.vert[k]].weight++;
68 : }
69 : }
70 2 : for(int k = 0; k < j.weight; ++k)
71 : {
72 1 : j.weight = 1/j.weight;
73 : }
74 1 : pos.mul(j.weight);
75 :
76 1 : const tri &t = tris[j.tri];
77 1 : matrix4x3 &m = j.orient;
78 1 : const vec &v1 = verts[t.vert[0]].pos,
79 1 : &v2 = verts[t.vert[1]].pos,
80 1 : &v3 = verts[t.vert[2]].pos;
81 : /*
82 : * /|
83 : * /
84 : * m.c/
85 : * /
86 : * v1 / m.a v2
87 : * *---------->*
88 : * |
89 : * |
90 : * |
91 : * m.b|
92 : * v
93 : *
94 : *
95 : * *
96 : * v3
97 : *
98 : * ----→ ----→
99 : * m.c = v1 v2 x v1 v3
100 : * --→ ----→
101 : * m.b = m.c x v1 v2
102 : *
103 : */
104 1 : m.a = vec(v2).sub(v1).normalize(); //m.a has magnitude 1 at all times
105 1 : m.c.cross(m.a, vec(v3).sub(v1)).normalize(); //m.c will always have a magnitude <=1
106 1 : m.b.cross(m.c, m.a); //m.b will always have a magnitude <= m.c
107 1 : m.d = pos;
108 1 : m.transpose();
109 : }
110 4 : for(vert &i : verts)
111 : {
112 3 : if(i.weight)
113 : {
114 3 : i.weight = 1/i.weight;
115 : }
116 : }
117 1 : reljoints.clear();
118 1 : }
119 :
120 1 : void ragdollskel::setuprotfrictions()
121 : {
122 1 : rotfrictions.clear();
123 3 : for(size_t i = 0; i < tris.size(); i++)
124 : {
125 3 : for(size_t j = i+1; j < tris.size(); j++)
126 : {
127 1 : if(tris[i].shareverts(tris[j]))
128 : {
129 : rotfriction r;
130 1 : r.tri[0] = i;
131 1 : r.tri[1] = j;
132 1 : rotfrictions.push_back(r);
133 : }
134 : }
135 : }
136 1 : }
137 :
138 1 : void ragdollskel::setup()
139 : {
140 1 : setupjoints();
141 1 : setuprotfrictions();
142 :
143 1 : loaded = true;
144 1 : }
145 :
146 1 : void ragdollskel::addreljoint(int bone, int parent)
147 : {
148 1 : reljoints.emplace_back(reljoint{bone, parent});
149 1 : }
150 : /* ragdolldata */
151 :
152 3 : ragdolldata::ragdolldata(const ragdollskel *skel, float scale)
153 3 : : skel(skel),
154 3 : millis(lastmillis),
155 3 : collidemillis(0),
156 3 : lastmove(lastmillis),
157 3 : radius(0),
158 6 : tris(skel->tris.size()),
159 3 : animjoints(!skel->animjoints || skel->joints.empty() ? nullptr : new matrix4x3[skel->joints.size()]),
160 3 : reljoints(skel->reljoints.empty() ? nullptr : new dualquat[skel->reljoints.size()]),
161 6 : verts(skel->verts.size()),
162 3 : collisions(0),
163 3 : floating(0),
164 3 : unsticks(INT_MAX),
165 3 : timestep(0),
166 3 : scale(scale),
167 9 : rotfrictions(skel->rotfrictions.size())
168 : {
169 3 : }
170 :
171 3 : ragdolldata::~ragdolldata()
172 : {
173 3 : if(animjoints)
174 : {
175 1 : delete[] animjoints;
176 : }
177 3 : if(reljoints)
178 : {
179 0 : delete[] reljoints;
180 : }
181 3 : }
182 :
183 : /*
184 : seed particle position = avg(modelview * base2anim * spherepos)
185 : mapped transform = invert(curtri) * origtrig
186 : parented transform = parent{invert(curtri) * origtrig} * (invert(parent{base2anim}) * base2anim)
187 : */
188 :
189 1 : matrix4x3 ragdolldata::calcanimjoint(int i, const matrix4x3 &anim) const
190 : {
191 1 : const ragdollskel::joint &j = skel->joints[i];
192 1 : vec pos(0, 0, 0);
193 4 : for(int k = 0; k < 3; ++k)
194 : {
195 3 : if(j.vert[k]>=0)
196 : {
197 3 : pos.add(verts[j.vert[k]].pos);
198 : }
199 : }
200 1 : pos.mul(j.weight);
201 :
202 1 : const ragdollskel::tri &t = skel->tris[j.tri];
203 1 : matrix4x3 m;
204 1 : const vec &v1 = verts[t.vert[0]].pos,
205 1 : &v2 = verts[t.vert[1]].pos,
206 1 : &v3 = verts[t.vert[2]].pos;
207 1 : m.a = vec(v2).sub(v1).normalize();
208 1 : m.c.cross(m.a, vec(v3).sub(v1)).normalize();
209 1 : m.b.cross(m.c, m.a);
210 1 : m.d = pos;
211 1 : matrix4x3 result;
212 1 : result.transposemul(m, anim);
213 2 : return result;
214 : }
215 :
216 1 : void ragdolldata::calctris()
217 : {
218 1 : for(size_t i = 0; i < skel->tris.size(); i++)
219 : {
220 0 : const ragdollskel::tri &t = skel->tris[i];
221 0 : matrix3 &m = tris[i];
222 0 : const vec &v1 = verts[t.vert[0]].pos,
223 0 : &v2 = verts[t.vert[1]].pos,
224 0 : &v3 = verts[t.vert[2]].pos;
225 0 : m.a = vec(v2).sub(v1).normalize();
226 0 : m.c.cross(m.a, vec(v3).sub(v1)).normalize();
227 0 : m.b.cross(m.c, m.a);
228 : }
229 1 : }
230 :
231 1 : void ragdolldata::calcboundsphere()
232 : {
233 1 : center = vec(0, 0, 0);
234 3 : for(const vert &v : verts)
235 : {
236 2 : center.add(v.pos);
237 : }
238 1 : center.div(verts.size());
239 1 : radius = 0;
240 3 : for(const vert &v : verts)
241 : {
242 2 : radius = std::max(radius, v.pos.dist(center));
243 : }
244 1 : }
245 :
246 : VAR(ragdolltimestepmin, 1, 5, 50);
247 : VAR(ragdolltimestepmax, 1, 10, 50);
248 :
249 1 : void ragdolldata::init(const dynent *d)
250 : {
251 1 : float ts = ragdolltimestepmin/1000.0f;
252 3 : for(vert &v : verts)
253 : {
254 2 : (v.oldpos = v.pos).sub(vec(d->vel).add(d->falling).mul(ts));
255 : }
256 1 : timestep = ts;
257 :
258 1 : calctris();
259 1 : calcboundsphere();
260 1 : offset = d->o;
261 1 : offset.sub(skel->eye >= 0 ? verts[skel->eye].pos : center);
262 1 : offset.z += (d->eyeheight + d->aboveeye)/2;
263 1 : }
264 :
265 0 : void ragdolldata::constraindist()
266 : {
267 0 : for(const ragdollskel::distlimit &d : skel->distlimits)
268 : {
269 0 : vert &v1 = verts[d.vert[0]],
270 0 : &v2 = verts[d.vert[1]];
271 0 : vec dir = vec(v2.pos).sub(v1.pos);
272 0 : const float dist = dir.magnitude()/scale;
273 : float cdist;
274 0 : if(dist < d.mindist)
275 : {
276 0 : cdist = d.mindist;
277 : }
278 0 : else if(dist > d.maxdist)
279 : {
280 0 : cdist = d.maxdist;
281 : }
282 : else
283 : {
284 0 : continue;
285 : }
286 0 : if(dist > 1e-4f)
287 : {
288 0 : dir.mul(cdist*0.5f/dist);
289 : }
290 : else
291 : {
292 0 : dir = vec(0, 0, cdist*0.5f*scale);
293 : }
294 0 : const vec center = vec(v1.pos).add(v2.pos).mul(0.5f);
295 0 : v1.newpos.add(vec(center).sub(dir));
296 0 : v1.weight++;
297 0 : v2.newpos.add(vec(center).add(dir));
298 0 : v2.weight++;
299 : }
300 0 : }
301 :
302 0 : void ragdolldata::applyrotlimit(const ragdollskel::tri &t1, const ragdollskel::tri &t2, float angle, const vec &axis)
303 : {
304 0 : vert &v1a = verts[t1.vert[0]],
305 0 : &v1b = verts[t1.vert[1]],
306 0 : &v1c = verts[t1.vert[2]],
307 0 : &v2a = verts[t2.vert[0]],
308 0 : &v2b = verts[t2.vert[1]],
309 0 : &v2c = verts[t2.vert[2]];
310 : // vec() copy constructor used below to deal with the fact that vec.add/sub() are destructive operations
311 0 : const vec m1 = vec(v1a.pos).add(v1b.pos).add(v1c.pos).div(3),
312 0 : m2 = vec(v2a.pos).add(v2b.pos).add(v2c.pos).div(3);
313 0 : vec q1a, q1b, q1c, q2a, q2b, q2c;
314 0 : const float w1 = q1a.cross(axis, vec(v1a.pos).sub(m1)).magnitude() +
315 0 : q1b.cross(axis, vec(v1b.pos).sub(m1)).magnitude() +
316 0 : q1c.cross(axis, vec(v1c.pos).sub(m1)).magnitude(),
317 0 : w2 = q2a.cross(axis, vec(v2a.pos).sub(m2)).magnitude() +
318 0 : q2b.cross(axis, vec(v2b.pos).sub(m2)).magnitude() +
319 0 : q2c.cross(axis, vec(v2c.pos).sub(m2)).magnitude();
320 0 : angle /= w1 + w2 + 1e-9f;
321 0 : const float a1 = angle*w2,
322 0 : a2 = -angle*w1,
323 0 : s1 = std::sin(a1),
324 0 : s2 = std::sin(a2);
325 0 : const vec c1 = vec(axis).mul(1 - std::cos(a1)),
326 0 : c2 = vec(axis).mul(1 - std::cos(a2));
327 0 : v1a.newpos.add(vec().cross(c1, q1a).madd(q1a, s1).add(v1a.pos));
328 0 : v1a.weight++;
329 0 : v1b.newpos.add(vec().cross(c1, q1b).madd(q1b, s1).add(v1b.pos));
330 0 : v1b.weight++;
331 0 : v1c.newpos.add(vec().cross(c1, q1c).madd(q1c, s1).add(v1c.pos));
332 0 : v1c.weight++;
333 0 : v2a.newpos.add(vec().cross(c2, q2a).madd(q2a, s2).add(v2a.pos));
334 0 : v2a.weight++;
335 0 : v2b.newpos.add(vec().cross(c2, q2b).madd(q2b, s2).add(v2b.pos));
336 0 : v2b.weight++;
337 0 : v2c.newpos.add(vec().cross(c2, q2c).madd(q2c, s2).add(v2c.pos));
338 0 : v2c.weight++;
339 0 : }
340 :
341 0 : void ragdolldata::constrainrot()
342 : {
343 0 : calctris(); //set up shadowing matrix3 objects for skel->tris
344 0 : for(const ragdollskel::rotlimit &r : skel->rotlimits)
345 : {
346 0 : matrix3 rot;
347 0 : rot.mul(tris[r.tri[0]], r.middle);
348 0 : rot.multranspose(tris[r.tri[1]]);
349 :
350 0 : vec axis;
351 : float angle;
352 0 : const float tr = rot.trace();
353 : //calculate angle and axis from the matrix `rot`, if possible
354 0 : if(tr >= r.maxtrace || !rot.calcangleaxis(tr, angle, axis))
355 : {
356 0 : continue;
357 : }
358 0 : angle = r.maxangle - angle + 1e-3f;
359 0 : applyrotlimit(skel->tris[r.tri[0]], skel->tris[r.tri[1]], angle, axis);
360 : }
361 0 : }
362 :
363 0 : void ragdolldata::calcrotfriction()
364 : {
365 0 : for(size_t i = 0; i < skel->rotfrictions.size(); ++i)
366 : {
367 0 : rotfrictions[i].transposemul(tris[skel->rotfrictions[i].tri[0]], tris[skel->rotfrictions[i].tri[1]]);
368 : }
369 0 : }
370 :
371 0 : void ragdolldata::applyrotfriction(float ts)
372 : {
373 0 : static FVAR(ragdollrotfric, 0, 0.85f, 1);
374 0 : static FVAR(ragdollrotfricstop, 0, 0.1f, 1);
375 :
376 0 : calctris();
377 0 : float stopangle = 2*M_PI*ts*ragdollrotfricstop,
378 0 : rotfric = 1.0f - std::pow(ragdollrotfric, ts*1000.0f/ragdolltimestepmin);
379 0 : for(size_t i = 0; i < skel->rotfrictions.size(); ++i)
380 : {
381 0 : matrix3 rot;
382 0 : rot.mul(tris[ skel->rotfrictions[i].tri[0]], rotfrictions[i]);
383 0 : rot.multranspose(tris[ skel->rotfrictions[i].tri[1]]);
384 :
385 0 : vec axis;
386 : float angle;
387 0 : if(!rot.calcangleaxis(angle, axis))
388 : {
389 0 : continue;
390 : }
391 0 : angle *= -(std::fabs(angle) >= stopangle ? rotfric : 1.0f);
392 0 : applyrotlimit(skel->tris[skel->rotfrictions[i].tri[0]], skel->tris[ skel->rotfrictions[i].tri[1]], angle, axis);
393 : }
394 0 : for(vert &v : verts)
395 : {
396 0 : if(!v.weight)
397 : {
398 0 : continue;
399 : }
400 0 : v.pos = v.newpos.div(v.weight);
401 0 : v.newpos = vec(0, 0, 0);
402 0 : v.weight = 0;
403 : }
404 0 : }
405 :
406 0 : void ragdolldata::tryunstick(float speed, vec &cwall)
407 : {
408 : /* vec `unstuck` is the average position of all vertices with the `stuck` flag
409 : * that are not found to collide with the world, as well as all vertices without
410 : * the `stuck` flag.
411 : */
412 0 : vec unstuck(0, 0, 0);
413 0 : size_t stuck = 0;
414 0 : for(size_t i = 0; i < verts.size(); i++)
415 : {
416 0 : vert &v = verts[i];
417 0 : if(v.stuck)
418 : {
419 0 : if(collidevert(v.pos, vec(0, 0, 0), skel->verts[i].radius, cwall))
420 : {
421 0 : stuck++;
422 0 : continue;
423 : }
424 0 : v.stuck = false;
425 : }
426 0 : unstuck.add(v.pos);
427 : }
428 0 : unsticks = 0;
429 0 : if(!stuck || stuck >= verts.size())
430 : {
431 0 : return;
432 : }
433 0 : unstuck.div(verts.size() - stuck); //get average by dividing by # verts not stuck
434 :
435 0 : for(vert &v : verts)
436 : {
437 0 : if(v.stuck)
438 : {
439 0 : v.pos.add(vec(unstuck).sub(v.pos).rescale(speed));
440 0 : unsticks++;
441 : }
442 : }
443 : }
444 :
445 0 : void ragdolldata::constrain(vec &cwall)
446 : {
447 0 : static VAR(ragdollconstrain, 1, 7, 100); //number of iterations to run ragdolldata::constrain() for
448 : //note: this for loop does not use the loop variable `i` anywhere
449 0 : for(int i = 0; i < ragdollconstrain; ++i)
450 : {
451 0 : constraindist();
452 0 : for(vert &v : verts)
453 : {
454 0 : v.undo = v.pos;
455 0 : if(v.weight)
456 : {
457 0 : v.pos = v.newpos.div(v.weight);
458 0 : v.newpos = vec(0, 0, 0);
459 0 : v.weight = 0;
460 : }
461 : }
462 :
463 0 : constrainrot();
464 0 : for(size_t j = 0; j < verts.size(); j++)
465 : {
466 0 : vert &v = verts[j];
467 0 : if(v.weight)
468 : {
469 0 : v.pos = v.newpos.div(v.weight);
470 0 : v.newpos = vec(0, 0, 0);
471 0 : v.weight = 0;
472 : }
473 0 : if(v.pos != v.undo && collidevert(v.pos, vec(v.pos).sub(v.undo), skel->verts[j].radius, cwall))
474 : {
475 0 : vec dir = vec(v.pos).sub(v.oldpos);
476 0 : const float facing = dir.dot(cwall);
477 0 : if(facing < 0)
478 : {
479 0 : v.oldpos = vec(v.undo).sub(dir.msub(cwall, 2*facing));
480 : }
481 0 : v.pos = v.undo;
482 0 : v.collided = true;
483 : }
484 : }
485 : }
486 0 : }
487 :
488 0 : void ragdolldata::move(bool water, float ts)
489 : {
490 :
491 0 : static FVAR(ragdollbodyfric, 0, 0.95f, 1);
492 0 : static FVAR(ragdollbodyfricscale, 0, 2, 10);
493 0 : static FVAR(ragdollwaterfric, 0, 0.85f, 1);
494 0 : static FVAR(ragdollgroundfric, 0, 0.8f, 1);
495 0 : static FVAR(ragdollairfric, 0, 0.996f, 1);
496 0 : static FVAR(ragdollunstick, 0, 10, 1e3f);
497 0 : static VAR(ragdollexpireoffset, 0, 2500, 30000);
498 0 : static VAR(ragdollwaterexpireoffset, 0, 4000, 30000);
499 :
500 0 : if(collidemillis && lastmillis > collidemillis)
501 : {
502 0 : return;
503 : }
504 :
505 0 : calcrotfriction();
506 0 : const float tsfric = timestep ? ts/timestep : 1,
507 0 : airfric = ragdollairfric + std::min((ragdollbodyfricscale*collisions)/verts.size(), 1.0f)*(ragdollbodyfric - ragdollairfric);
508 0 : collisions = 0;
509 0 : vec collidewall(0,0,0);
510 :
511 0 : for(size_t i = 0; i < verts.size(); i++)
512 : {
513 0 : vert &v = verts[i];
514 0 : vec dpos = vec(v.pos).sub(v.oldpos);
515 0 : dpos.z -= 100*ts*ts;
516 0 : if(water)
517 : {
518 : //reinterpret cast pointer -> "random" seed value
519 0 : dpos.z += 0.25f*std::sin(detrnd(reinterpret_cast<size_t>(this)+i, 360)/RAD + lastmillis/10000.0f*M_PI)*ts;
520 : }
521 0 : dpos.mul(std::pow((water ? ragdollwaterfric : 1.0f) * (v.collided ? ragdollgroundfric : airfric), ts*1000.0f/ragdolltimestepmin)*tsfric);
522 0 : v.oldpos = v.pos;
523 0 : v.pos.add(dpos);
524 : }
525 0 : applyrotfriction(ts);
526 0 : for(size_t i = 0; i < verts.size(); i++)
527 : {
528 0 : vert &v = verts[i];
529 0 : if(v.pos.z < 0)
530 : {
531 0 : v.pos.z = 0;
532 0 : v.oldpos = v.pos;
533 0 : collisions++;
534 : }
535 0 : vec dir = vec(v.pos).sub(v.oldpos);
536 0 : v.collided = collidevert(v.pos, dir, skel->verts[i].radius, collidewall);
537 0 : if(v.collided)
538 : {
539 0 : v.pos = v.oldpos;
540 0 : v.oldpos.sub(dir.reflect(collidewall));
541 0 : collisions++;
542 : }
543 : }
544 0 : if(unsticks && ragdollunstick)
545 : {
546 0 : tryunstick(ts*ragdollunstick, collidewall);
547 : }
548 0 : timestep = ts;
549 0 : if(collisions)
550 : {
551 0 : floating = 0;
552 0 : if(!collidemillis)
553 : {
554 0 : collidemillis = lastmillis + (water ? ragdollwaterexpireoffset : ragdollexpireoffset);
555 : }
556 : }
557 0 : else if(++floating > 1 && lastmillis < collidemillis)
558 : {
559 0 : collidemillis = 0;
560 : }
561 0 : constrain(collidewall);
562 0 : calctris();
563 0 : calcboundsphere();
564 : }
565 :
566 0 : bool ragdolldata::collidevert(const vec &pos, const vec &dir, float radius, vec &cwall)
567 : {
568 : static struct vertent : physent
569 : {
570 0 : vertent()
571 0 : {
572 0 : type = PhysEnt_Bounce;
573 0 : radius = xradius = yradius = eyeheight = aboveeye = 1;
574 0 : }
575 0 : } v;
576 0 : v.o = pos;
577 0 : if(v.radius != radius) //(radius param != 1)
578 : {
579 0 : v.radius = v.xradius = v.yradius = v.eyeheight = v.aboveeye = radius;
580 : }
581 : //collide generated vertent (point with sphere bounding of r = radius)
582 : //with dir parameter
583 0 : return collide(&v, &cwall, dir, 0);
584 : }
585 :
586 : //used in iengine
587 0 : void moveragdoll(dynent *d)
588 : {
589 0 : static FVAR(ragdolleyesmooth, 0, 0.5f, 1);
590 0 : static VAR(ragdolleyesmoothmillis, 1, 250, 10000);
591 0 : if(!curtime || !d->ragdoll)
592 : {
593 0 : return;
594 : }
595 0 : if(!d->ragdoll->collidemillis || lastmillis < d->ragdoll->collidemillis)
596 : {
597 0 : const int lastmove = d->ragdoll->lastmove;
598 0 : while(d->ragdoll->lastmove + (lastmove == d->ragdoll->lastmove ? ragdolltimestepmin : ragdolltimestepmax) <= lastmillis)
599 : {
600 0 : const int timestep = std::min(ragdolltimestepmax, lastmillis - d->ragdoll->lastmove),
601 0 : material = rootworld.lookupmaterial(vec(d->ragdoll->center.x, d->ragdoll->center.y, d->ragdoll->center.z + d->ragdoll->radius/2));
602 0 : const bool water = (material&MatFlag_Volume) == Mat_Water;
603 0 : d->inwater = water ? material&MatFlag_Volume : Mat_Air;
604 0 : d->ragdoll->move(water, timestep/1000.0f);
605 0 : d->ragdoll->lastmove += timestep;
606 : }
607 : }
608 :
609 0 : vec eye = d->ragdoll->skel->eye >= 0 ? d->ragdoll->verts[d->ragdoll->skel->eye].pos : d->ragdoll->center;
610 0 : eye.add(d->ragdoll->offset);
611 0 : const float k = std::pow(ragdolleyesmooth, static_cast<float>(curtime)/ragdolleyesmoothmillis);
612 0 : d->o.lerp(eye, 1-k);
613 : }
614 :
615 1 : void cleanragdoll(dynent *d)
616 : {
617 1 : if(d->ragdoll)
618 : {
619 1 : delete d->ragdoll;
620 1 : d->ragdoll = nullptr;
621 : }
622 1 : }
|