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