Bodies the host moves are no longer drawn floating or in walls on clients.

In large games, bodies on clients were drawn twisted, hanging in the air,
piled up or inside walls. There were two causes, both in how a client
takes the host's object states.

A body that comes to rest lies limp (bipeds.c): its nodes stay where they
are in the world and are no longer recomputed from the object. The game
stops this when something pushes the body (biped_accelerate). On a client,
that push comes from the damage aftermath, which is sent only to the
machines that see the body's player (network_damage.c). It never comes for
older bodies (a player's dead_unit_index is their latest) or for world
damage. When the host then moves the body, the client's copy moves with
the host's corrections and its own physics, but its pose stays where it
came to rest. A client now stops a body lying limp before it puts the body
somewhere and whenever the host says it is moving. units.c makes it lie
limp again, every tick, once it is at rest on the ground.

A client also takes the host's word that a body is at rest while its own
copy is still in the air, a little behind. Rest stops its physics, and
only its physics clears airborne, so the body kept the pose of its fall
(its root node about 0.36 above the ground) and never lay limp. A client
now clears airborne on a body the host has at rest: the host's is on the
ground, because it comes to rest only when it is not airborne.

Nothing sent changes, so the network version stays the same.

Checked:
- New harness test limp_body_moves takes the code from network_objects.c
  and bipeds.c. Its cases are pushed, landed, moved, blended and living.
  Each of its 5 negative controls (each line of the fix taken out) fails
  its case.
- The whole harness, run natively on Windows: 234 passed, and the
  failures are the environmental ones main also has on this machine.
- The Windows build has no warnings.
- local system link games on one machine (debug.network_test, bots and a
  kill every 2 seconds), with temporary instrumentation: each machine logs
  its bodies every second, and the host sets off a frag grenade's
  explosion under up to 4 resting bodies every 5 seconds. Result: bodies
  at rest drawn more than 0.15 from where the host draws them:
    4 machines, 120 s:  195 of 573 pairs before, 0 of 1221 after;
    32 machines, 150 s: 19479 of 96439 pairs before (20%), 157 of 71237
      after (0.2%), counting only pairs where the host and the client
      agree on the body's position. The rest are bodies still in their
      dying animation, a few ticks apart, at the same rate as on the
      host.
  Copies at rest but left airborne: 195 samples before and 0 after on 4
  machines, 10718 before and 0 after on 32.
- Not checked here: the Android and web builds (they don't build on this
  Windows machine), Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hkmoddandClaude Opus 5.5 committed 2026-10-12 04:46:27 +02:00
1 parent 86be005acc
commit e791cf9ae0
4 files changed
+360 -1

No files matched your search

+5 -1
View File
@@ -463,7 +463,11 @@ menus show a map without its hash, unless two versions are listed.
would leave a falling body hanging until its turn round all of them),
and a few of those at rest, round them all. A client takes the host's
word on whether each is at rest even when its copy is close enough to
leave where it is. A client puts its copies there, and the difference is
leave where it is. A body's pose goes with it: one lying limp that the
host moves stops lying limp (the blast that moved the host's reaches
only the machines that see the body's player), and one the host has at
rest is on the ground, so it lies limp again where it stops instead of
keeping the pose of its fall in the air. A client puts its copies there, and the difference is
drawn fading over a few ticks (`render_interpolation.c`) instead of a
jump. A client drives its own player's vehicle and sends where it is,
which the host takes within a tolerance, as it does its own player's
+46
View File
@@ -53,6 +53,7 @@ same datum index (identifier and all), so that any message can name one:
#include "models/model_animation_definitions.h"
#include "models/model_definitions.h"
#include "units/units.h"
#include "units/bipeds.h"
#include "units/biped_definitions.h"
#include "units/vehicle_definitions.h"
#include "units/vehicle_datum.h"
@@ -677,6 +678,48 @@ static boolean distributed_vehicle_unsteered(
unit_get(vehicle->unit.driver_object_index)->unit.player_index == NONE;
}
/* the object if it is a body (a dead biped), else NULL */
static struct biped_datum *distributed_body_get(
long object_index)
{
struct biped_datum *biped = (struct biped_datum *)object_try_and_get_and_verify_type(object_index,
_object_mask_biped);
return biped && TEST_FLAG(biped->object.damage_flags, _object_dead_bit) ? biped : NULL;
}
/* A body lying limp here, which the host moves: its pose, fixed in the world
where it came to rest (bipeds.c), would stay there as the body goes, drawn
hanging in the air or in a wall. Unfixed, as the push of the explosion that
moved the host's body would have unfixed it here (that push reaches only the
machines that see the body's player, network_damage.c); it lies limp again
where it comes to rest (units.c). */
static void distributed_body_unfix_pose(
long object_index)
{
if (distributed_body_get(object_index))
biped_stop_limp_body_physics(object_index);
}
/* A body at rest or moving as the host has it. Moving, its pose goes with
it, however close it is. At rest, it is on the ground, as the host's is:
not airborne, which only its physics clears, and rest stops its physics
(bipeds.c); left airborne, it would keep the pose of its fall, in the air,
and never lie limp. */
static void distributed_body_set_at_rest(
long object_index,
boolean at_rest)
{
struct biped_datum *biped = distributed_body_get(object_index);
if (!biped)
return;
if (!at_rest)
distributed_body_unfix_pose(object_index);
else
SET_FLAG(biped->biped.flags, _biped_airborne_bit, FALSE);
}
/* whether this machine's player drives the unit: it is their biped, or the
vehicle they drive */
static boolean distributed_unit_driven_here(
@@ -714,6 +757,7 @@ static void distributed_object_move(
offset.i = object->object.position.x - position->x;
offset.j = object->object.position.y - position->y;
offset.k = object->object.position.z - position->z;
distributed_body_unfix_pose(object_index);
object_set_position(object_index, position, forward, up);
if (velocity)
object->object.translational_velocity = *velocity;
@@ -784,6 +828,7 @@ boolean network_objects_reconcile(
offset.i = object->object.position.x - blended.x;
offset.j = object->object.position.y - blended.y;
offset.k = object->object.position.z - blended.z;
distributed_body_unfix_pose(object_index);
object_set_position(object_index, &blended, &valid_forward, &valid_up);
if (!distributed_object_follows_host(object_index))
render_interpolation_correct_object(object_index, &offset);
@@ -2600,6 +2645,7 @@ void network_objects_handle_states(
at_rest = TEST_FLAG(state->flags, _distributed_object_at_rest_bit) || distributed_vehicle_unsteered(state->object_index);
if (!own_vehicle)
SET_FLAG(object->object.flags, _object_at_rest_bit, at_rest);
distributed_body_set_at_rest(state->object_index, at_rest);
dx = state->position.x - object->object.position.x;
dy = state->position.y - object->object.position.y;
dz = state->position.z - object->object.position.z;
+240
View File
@@ -0,0 +1,240 @@
#include "harness.h"
#include <math.h>
#define SET_FLAG(v,b,s) ((v)=(s)?((v)|FLAG(b)):((v)&~FLAG(b)))
#include "config.inc"
/* One body on a client, on flat ground at z 0. Its drawn pose is its root
node: where the object is, raised by its pose (FALL_POSE in the pose of its
fall, airborne; 0 lying), recomputed each tick unless lying limp, when it
stays where it was in the world and relaxes onto the ground there (objects.c,
bipeds.c). */
#define BODY 0x12340007L
#define FALL_POSE 0.36f
#define RELAXATION_ITERATIONS 4
struct _object_datum
{
real_point3d position;
real_vector3d forward, up, translational_velocity, angular_velocity;
unsigned long flags, damage_flags;
short type;
};
struct _unit_datum { short unused; };
struct _biped_datum { unsigned long flags; byte limp_body_current_relaxation_iterations, limp_body_max_relaxation_iterations; };
struct biped_datum { long definition_index; struct _object_datum object; struct _unit_datum unit; struct _biped_datum biped; };
#define object_datum biped_datum
struct biped_definition { struct { unsigned long flags; } biped; };
static struct biped_datum body;
static struct biped_definition definition;
static boolean debug_biped_limp_body_disable;
static real_point3d node;
static int interpolations;
static struct biped_datum *object_get(long index) { CHECK(index == BODY, "not the body"); return &body; }
static void *object_get_and_verify_type(long index, unsigned long mask)
{
CHECK(index == BODY && TEST_FLAG(mask, body.object.type), "not a biped");
return &body;
}
static void *object_try_and_get_and_verify_type(long index, unsigned long mask)
{
return index == BODY && TEST_FLAG(mask, body.object.type) ? &body : NULL;
}
#define biped_get(index) ((struct biped_datum *)object_get_and_verify_type((index), _object_mask_biped))
static struct biped_definition *biped_definition_get(long index) { return &definition; }
static long biped_limp_noodle_get_max_relaxation_iterations(long index) { return RELAXATION_ITERATIONS; }
static void pose_compute(void)
{
node = body.object.position;
if (TEST_FLAG(body.biped.flags, _biped_airborne_bit))
node.z += FALL_POSE;
}
/* (objects.c: placed, its nodes are computed, fixed or not) */
static void object_set_position(long index, real_point3d const *position, real_vector3d const *forward, real_vector3d const *up)
{
CHECK(index == BODY, "not the body");
body.object.position = *position;
body.object.forward = *forward;
body.object.up = *up;
pose_compute();
}
static void render_interpolation_correct_object(long index, real_vector3d const *offset) { interpolations++; }
static boolean distributed_vehicle_unsteered(long index) { return FALSE; }
static boolean distributed_object_follows_host(long index) { return FALSE; }
static boolean distributed_transform_valid(real_point3d const *position, real_vector3d const *forward, real_vector3d const *up,
real_vector3d const *velocity, real_vector3d const *angular_velocity, real_vector3d *valid_forward, real_vector3d *valid_up)
{
*valid_forward = *forward;
*valid_up = *up;
return TRUE;
}
#include "under_test.inc"
static boolean limp(void) { return TEST_FLAG(body.biped.flags, _biped_limp_body_physics_active_bit); }
static boolean fixed(void) { return TEST_FLAG(body.object.flags, _object_do_not_recompute_node_matrices_bit); }
static real distance(real_point3d const *a, real_point3d const *b)
{
real dx = a->x - b->x, dy = a->y - b->y, dz = a->z - b->z;
return (real)sqrt(dx * dx + dy * dy + dz * dz);
}
/* a tick of the client's game: its physics, unless at rest (bipeds.c); the
limp body's relaxation; limp again where it can be, each tick at the end of
its dying animation (units.c); its nodes, unless fixed (objects.c) */
static void tick(void)
{
if (!TEST_FLAG(body.object.flags, _object_at_rest_bit))
{
body.object.position.x += body.object.translational_velocity.i / 30.0f;
body.object.position.y += body.object.translational_velocity.j / 30.0f;
body.object.position.z += body.object.translational_velocity.k / 30.0f;
if (body.object.position.z <= 0.0f)
{
body.object.position.z = 0.0f;
body.object.translational_velocity.i *= 0.8f;
body.object.translational_velocity.j *= 0.8f;
body.object.translational_velocity.k = 0.0f;
SET_FLAG(body.biped.flags, _biped_airborne_bit, FALSE);
}
else
{
body.object.translational_velocity.k -= 9.8f / 30.0f;
SET_FLAG(body.biped.flags, _biped_airborne_bit, TRUE);
}
}
if (limp() && body.biped.limp_body_current_relaxation_iterations < body.biped.limp_body_max_relaxation_iterations)
{
body.biped.limp_body_current_relaxation_iterations++;
node.z = 0.0f;
}
biped_start_limp_body_physics(BODY);
if (!fixed())
pose_compute();
}
/* the host's word on the body, as network_objects_handle_states takes it */
static void receive(real x, real y, real z, boolean at_rest)
{
real_point3d position;
real_vector3d forward = {1.0f, 0.0f, 0.0f}, up = {0.0f, 0.0f, 1.0f};
position.x = x;
position.y = y;
position.z = z;
SET_FLAG(body.object.flags, _object_at_rest_bit, at_rest);
distributed_body_set_at_rest(BODY, at_rest);
if (distance(&position, &body.object.position) > REMOTE_OBJECT_TOLERANCE)
{
network_objects_reconcile(BODY, &position, &forward, &up, &body.object.translational_velocity, NULL,
at_rest ? 0.0f : REMOTE_BLEND_DISTANCE);
}
}
/* a body come to rest on the ground at (x, y), lying limp and relaxed */
static void lying(real x, real y)
{
int i;
memset(&body, 0, sizeof(body));
body.object.type = _object_type_biped;
body.object.forward.i = 1.0f;
body.object.up.k = 1.0f;
body.object.position.x = x;
body.object.position.y = y;
SET_FLAG(body.object.damage_flags, _object_dead_bit, TRUE);
SET_FLAG(body.object.flags, _object_at_rest_bit, TRUE);
for (i = 0; i < 10; i++)
tick();
CHECK(limp() && fixed() && body.biped.limp_body_current_relaxation_iterations == RELAXATION_ITERATIONS,
"the fake world: a body at rest lies limp");
}
static void settled(char const *what)
{
int i;
for (i = 0; i < 10; i++)
tick();
CHECK(limp(), "%s: never lies limp again", what);
CHECK(distance(&node, &body.object.position) <= REMOTE_OBJECT_TOLERANCE, "%s: drawn %.2f from where it is (%.2f %.2f %.2f, at %.2f %.2f %.2f)",
what, distance(&node, &body.object.position), node.x, node.y, node.z,
body.object.position.x, body.object.position.y, body.object.position.z);
}
int main(int argc, char **argv)
{
char const *case_name = argv[1];
int i;
SET_FLAG(definition.biped.flags, _biped_uses_limp_body_physics_bit, TRUE);
/* An explosion this machine does not hear of pushes the host's body: it
moves, each word of the host's close enough to this copy's own physics
not to be corrected, and comes to rest a few metres away. */
CASE("pushed")
{
lying(0.0f, 0.0f);
receive(0.02f, 0.0f, 0.0f, FALSE);
body.object.translational_velocity.i = 12.0f;
for (i = 0; i < 30; i++)
{
tick();
receive(body.object.position.x, body.object.position.y, body.object.position.z, FALSE);
}
CHECK(body.object.position.x > 1.0f, "the fake world: the body is pushed away");
receive(body.object.position.x, body.object.position.y, body.object.position.z, TRUE);
settled("pushed");
}
/* The host's body lands; this copy, a little behind, still in the air
(close enough not to be corrected) when the host's word comes that it is
at rest. */
CASE("landed")
{
lying(0.0f, 0.0f);
receive(0.0f, 0.0f, 0.0f, FALSE);
body.object.position.z = 0.5f;
tick();
tick();
body.object.position.z = 0.03f;
CHECK(TEST_FLAG(body.biped.flags, _biped_airborne_bit) && !limp(), "the fake world: the copy is falling");
receive(0.0f, 0.0f, 0.0f, TRUE);
settled("landed");
CHECK(!TEST_FLAG(body.biped.flags, _biped_airborne_bit), "landed: left airborne");
}
/* The host's body, at rest, put somewhere else: it lies again there, its
relaxation from the start, not the animation's pose relaxed no further. */
CASE("moved")
{
lying(0.0f, 0.0f);
receive(3.0f, 1.0f, 0.0f, TRUE);
CHECK(body.object.position.x == 3.0f && interpolations == 1, "moved: not put where the host has it");
tick();
CHECK(limp() && body.biped.limp_body_current_relaxation_iterations == 0, "moved: relaxation not started again (%d)",
body.biped.limp_body_current_relaxation_iterations);
settled("moved");
}
/* a body brought halfway to where the host has it, a little at a time */
CASE("blended")
{
real_point3d position = {0.5f, 0.0f, 0.0f};
real_vector3d forward = {1.0f, 0.0f, 0.0f}, up = {0.0f, 0.0f, 1.0f};
lying(0.0f, 0.0f);
network_objects_reconcile(BODY, &position, &forward, &up, NULL, NULL, REMOTE_BLEND_DISTANCE);
CHECK(body.object.position.x == 0.25f, "the fake world: blended halfway");
CHECK(!limp(), "blended: still lying limp where it was");
settled("blended");
}
/* a living biped, as it was */
CASE("living")
{
lying(0.0f, 0.0f);
SET_FLAG(body.object.damage_flags, _object_dead_bit, FALSE);
SET_FLAG(body.biped.flags, _biped_airborne_bit, TRUE);
receive(0.0f, 0.0f, 0.0f, TRUE);
CHECK(TEST_FLAG(body.biped.flags, _biped_airborne_bit), "living: its airborne changed");
receive(2.0f, 0.0f, 0.0f, FALSE);
CHECK(limp() && fixed(), "living: its pose changed");
}
return 0;
}
@@ -0,0 +1,69 @@
"""A body lying limp on a client, which the host moves: its pose goes with it, and it lies limp again where it comes
to rest."""
import re
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from harness import CHECK_FAILED, build, enum_with, function, mutated, read, run
CASES = ['pushed', 'landed', 'moved', 'blended', 'living']
CONTROLS = [
('if (!at_rest)\n\t\tdistributed_body_unfix_pose(object_index);', 'if (!at_rest)\n\t\t;', 'pushed'),
('SET_FLAG(biped->biped.flags, _biped_airborne_bit, FALSE);', ';', 'landed'),
('position->z;\n\tdistributed_body_unfix_pose(object_index);', 'position->z;', 'moved'),
('blended.z;\n\t\tdistributed_body_unfix_pose(object_index);', 'blended.z;', 'blended'),
('return biped && TEST_FLAG(biped->object.damage_flags, _object_dead_bit) ? biped : NULL;', 'return biped;', 'living'),
]
def real_define(source, name):
return '#define %s %sf\n' % (name, re.search(r'#define\s+' + name + r'\s+([\d.]+)f', source).group(1))
def generated(fault=None):
network = read('port/linux/game/network_objects.c')
bipeds = read('source/units/bipeds.c')
config = '\n'.join([
enum_with(read('source/objects/object_types.h'), '_object_type_biped'),
enum_with(read('source/objects/objects.h'), '_object_at_rest_bit'),
enum_with(read('source/objects/objects.h'), '_object_dead_bit'),
enum_with(read('source/units/bipeds.h'), '_biped_airborne_bit'),
enum_with(read('source/units/biped_definitions.h'), '_biped_uses_limp_body_physics_bit'),
]) + '\n'
config += real_define(network, 'REMOTE_OBJECT_TOLERANCE')
config += real_define(read('port/linux/game/network_distributed.h'), 'REMOTE_BLEND_DISTANCE')
code = '\n'.join([
function(bipeds, 'biped_start_limp_body_physics'),
function(bipeds, 'biped_stop_limp_body_physics'),
function(network, 'distributed_body_get'),
function(network, 'distributed_body_unfix_pose'),
function(network, 'distributed_body_set_at_rest'),
function(network, 'distributed_object_move'),
function(network, 'network_objects_reconcile'),
])
if fault:
code = mutated(code, *fault)
return (('config.inc', config), ('under_test.inc', code))
@pytest.mark.parametrize('case', CASES)
def test_case(case):
status, output = run(build('limp_body_moves', generated()), case)
assert status == 0, output
@pytest.mark.parametrize('before,after,case', CONTROLS)
def test_negative_control(before, after, case):
status, output = run(build('limp_body_moves', generated((before, after))), case)
assert status == CHECK_FAILED, output
def test_host_word_on_rest():
# Each object state takes the host's at rest for a body too, after its own flag, before its correction.
states = function(read('port/linux/game/network_objects.c'), 'network_objects_handle_states')
flag = states.index('SET_FLAG(object->object.flags, _object_at_rest_bit, at_rest);')
body = states.index('distributed_body_set_at_rest(state->object_index, at_rest);')
assert flag < body < states.index('network_objects_reconcile(state->object_index', body)