diff --git a/port/linux/NETCODE.md b/port/linux/NETCODE.md index 717ecc0d..1e3799cf 100644 --- a/port/linux/NETCODE.md +++ b/port/linux/NETCODE.md @@ -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 diff --git a/port/linux/game/network_objects.c b/port/linux/game/network_objects.c index 64273967..9c5436f7 100644 --- a/port/linux/game/network_objects.c +++ b/port/linux/game/network_objects.c @@ -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; diff --git a/tools/harness/tests/limp_body_moves.c b/tools/harness/tests/limp_body_moves.c new file mode 100644 index 00000000..bd3f82fe --- /dev/null +++ b/tools/harness/tests/limp_body_moves.c @@ -0,0 +1,240 @@ +#include "harness.h" +#include + +#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; +} diff --git a/tools/harness/tests/test_limp_body_moves.py b/tools/harness/tests/test_limp_body_moves.py new file mode 100644 index 00000000..53e5aa32 --- /dev/null +++ b/tools/harness/tests/test_limp_body_moves.py @@ -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)