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