Merge pull request #450 from OpenCommunityEdition/fix-scenery-shields-network

Scenery shields break, and tripmines go off, for clients as for the host
This commit is contained in:
bnunu authored and GitHub committed 2026-10-22 18:25:20 -04:00
commit 4cb621cd26
8 files changed
+254 -21

No files matched your search

+12 -1
View File
@@ -240,7 +240,18 @@ own, plays too; version 32 sends the host's mouse aim assist policy
ignore, and has a client's input say whether its player plays co-op with
friendly fire; version 33 sends with the game's settings whether co-op's
players hurt their AI allies (Server Setup's ALLY FRIENDLY FIRE: the host
then deals the players' hits of them no damage).
then deals the players' hits of them no damage); version 34 takes clients'
hits on, and sends the host's damage to, the map's scenery and devices
whose shield is all damage can break (Rat Race's energy shields, a Custom
Edition map's tripmines), which version 17 left out as having no body to
destroy, and has every machine run those shields' stun and recharge itself;
and the host deals what such an object deals itself (its shield or body
breaking: a tripmine a client's player threw going off under the host's
vehicle), which a client no longer reports and the host took from no one.
One made in the game, a tripmine, is at an index of its own on each machine:
the host takes a client's hit on its copy as one on the host's nearest where
the client had it, and a client the host's damage to one as damage to its
own copy nearest the damage.
`port/linux/network_format.txt` records the wire format of the current
version (`tools/network_format.py`): `test_linux_port.py` fails when the
+119 -13
View File
@@ -34,10 +34,19 @@ being hit looks and feels like on the clients).
- The damage to a unit goes to the machines of the players it concerns
(the unit's, its riders', the damage's owner's) and those sent the
unit's player this tick, which see it; a killing blow to all.
- Scenery and devices that can be destroyed (lights, panels) are the
map's, at the same index on every machine. A client reports its hits on
them like any other, and every client deals the host's damage to its own
copy, which breaks and plays its effects as the host's did.
- Scenery and devices that can be destroyed (lights, panels), or whose
shields can be broken (energy shields, a map's tripmines), are the map's,
at the same index on every machine. A client reports its hits on them like
any other, and every client deals the host's damage to its own copy, which
breaks and plays its effects as the host's did. Their shields' stun and
recharge each machine runs itself (damage.c's objects_update_shields), as
the host sends no state of them: the same damage, and the same recharge.
What one of them deals itself, its shield or body breaking (a tripmine
going off), the host deals whoever owns it, and no client reports: a
client's player's tripmine is set off where the host has it. One made in
the game (a tripmine a grenade's effect places) is at an index of its own
on each machine: a machine given another's index of one takes its own
nearest where the other had it (static_target_near).
*/
#include "cseries.h"
@@ -216,6 +225,10 @@ from the target */
#define REPORT_TARGET_TOLERANCE 3.0f
#define REPORT_TARGET_LEAD_TICKS 15.0f
#define REPORT_IMPACT_TOLERANCE 2.0f
/* ... and how far beyond its size a static target made in the game may be
from where another machine had its own copy (a tripmine: the same grenade's
landing on each machine, static_target_near) */
#define STATIC_TARGET_MATCH_TOLERANCE 1.0f
/* ... and from where the host had a player's unit or vehicle then (with how
far it moves in a few ticks: a client's copy runs a little ahead of the
host's word on it), and further for a vehicle's (its size, and as far as a
@@ -499,12 +512,31 @@ static boolean distributed_damage_to_data(
return TRUE;
}
/* whether the object is scenery or a device that damage can destroy */
/* whether the object is scenery or a device that damage can destroy, or
whose shield it can break (a shield that is all the object is: Rat Race's
energy shields, a map's tripmines) */
static boolean static_target(
long object_index)
{
return object_try_and_get_and_verify_type(object_index, STATIC_TARGET_TYPES) &&
object_get_maximum_body_vitality(object_index, TRUE) > 0.0f;
(object_get_maximum_body_vitality(object_index, TRUE) > 0.0f ||
object_get_maximum_shield_vitality(object_index, TRUE) > 0.0f);
}
boolean network_damage_static_target(
long object_index)
{
return static_target(object_index);
}
/* whether the damage is a static target's own (an effect of its shield or
body breaking: a map's tripmine going off), which the host deals whoever its
owner is: a client's player's tripmine is set off where the host has it (a
vehicle running over it), so their machine has no hit of it to report */
static boolean static_target_damage(
struct damage_data const *damage)
{
return damage->owner_object_index != NONE && static_target(damage->owner_object_index);
}
/* ---------- object_cause_damage (damage.c) */
@@ -652,8 +684,9 @@ boolean network_damage_deals(
damage_replay_player_effect(unit->unit.player_index, &effect, 0.0f);
}
/* a hit of this machine's own player's, on the host's object */
if (distributed_player_is_local(damage->owner_player_index) &&
/* a hit of this machine's own player's, on the host's object (not a
static target's own damage, which the host deals: static_target_damage) */
if (distributed_player_is_local(damage->owner_player_index) && !static_target_damage(damage) &&
(network_objects_client_has(object_index) || static_target(object_index)) &&
damage_report_count < MAXIMUM_HIT_REPORTS_PER_TICK)
{
@@ -678,9 +711,11 @@ boolean network_damage_deals(
return FALSE;
}
/* (a client's player's, whose machine reports it; but one whose machine
has left deals it here: their grenades and rockets still in flight) */
has left deals it here: their grenades and rockets still in flight; and
a static target's own, which no machine reports: static_target_damage) */
if (game_connection() == _game_connection_network_server && !damage_dealing_report &&
damage->owner_player_index != NONE && !distributed_player_is_local(damage->owner_player_index))
damage->owner_player_index != NONE && !distributed_player_is_local(damage->owner_player_index) &&
!static_target_damage(damage))
{
struct player_datum *owner = player_try_and_get(damage->owner_player_index);
@@ -1301,6 +1336,41 @@ static real distributed_distance_squared(
return dx * dx + dy * dy + dz * dz;
}
/* this machine's static target nearest the point where another machine had
one, within its size and the reach of the point, else NONE. A static target
made in the game (a tripmine a grenade's effect places) is at an index of
its own on each machine, as the map's are not, so another machine's index of
it names no such object here */
static long static_target_near(
real_point3d const *point,
real reach)
{
struct object_iterator iterator;
long nearest = NONE;
real nearest_distance = 0.0f;
object_iterator_new(&iterator, STATIC_TARGET_TYPES, 0);
while (object_iterator_next(&iterator))
{
real_point3d origin;
real distance;
real limit;
if (!static_target(iterator.index))
continue;
object_get_origin(iterator.index, &origin);
distance = distributed_distance_squared(point, &origin);
limit = object_get(iterator.index)->object.bounding_sphere_radius + reach;
if (distance <= limit * limit && (nearest == NONE || distance < nearest_distance))
{
nearest = iterator.index;
nearest_distance = distance;
}
}
return nearest;
}
/* where each player's unit and vehicle are this tick (and whether they
drive it), noted; and every few ticks where their unit is */
static void distributed_note_targets(
@@ -1962,8 +2032,26 @@ void network_damage_handle_reports(
for (index = 0; index < count; index++)
{
struct distributed_hit_report const *report = &reports[index];
struct distributed_hit_report found;
struct damage_data damage;
/* (a hit on the client's copy of a static target made in the game,
at an index of its own there: the host's where the client had it) */
if (distributed_object_index_valid(report->object_index) &&
distributed_point_valid(&report->target_position, REPORT_WORLD_BOUND) &&
!object_try_and_get_and_verify_type(report->object_index,
_object_mask_biped | _object_mask_vehicle | _object_mask_weapon | _object_mask_equipment) &&
!static_target(report->object_index))
{
long object_index = static_target_near(&report->target_position, STATIC_TARGET_MATCH_TOLERANCE);
if (object_index != NONE)
{
found = *report;
found.object_index = object_index;
report = &found;
}
}
if (!distributed_report_valid(machine_index, report, &damage))
{
damage_rejected_reports++;
@@ -2255,15 +2343,33 @@ void network_damage_handle_events(
}
break;
case _damage_event_static:
if (distributed_object_index_valid(event->object_index) && static_target(event->object_index) &&
distributed_damage_indices_valid(event->object_index, event->node_index, event->region_index,
{
long object_index = event->object_index;
/* (one made in the game, at an index of its own here: this machine's
copy at the damage, within its reach, an explosion's its radius) */
if (distributed_object_index_valid(object_index) && !static_target(object_index) &&
!object_try_and_get_and_verify_type(object_index, _object_mask_all))
{
real reach = REPORT_IMPACT_TOLERANCE;
if (TEST_FLAG(damage.flags, _damage_area_of_effect_bit) &&
tag_index_is_group(damage.definition_index, DAMAGE_EFFECT_DEFINITION_TAG))
{
reach += damage_effect_definition_get(damage.definition_index)->cutoff_radius;
}
object_index = static_target_near(&damage.origin, reach);
}
if (distributed_object_index_valid(object_index) && static_target(object_index) &&
distributed_damage_indices_valid(object_index, event->node_index, event->region_index,
event->material_index))
{
damage_replay_static(event->object_index, &damage, event->node_index, event->region_index,
damage_replay_static(object_index, &damage, event->node_index, event->region_index,
event->material_index);
}
break;
}
}
}
}
+1 -1
View File
@@ -66,7 +66,7 @@ is newer (network_client_manager.c). A host advertises it, with its netcode,
in its game's advertisement's reserved bytes (network_server_message_handler.c),
which hosts built before there was a version send as zeros: version 0.
Raise it with any change to what the machines send each other. */
#define HALO_PORT_NETWORK_VERSION 33
#define HALO_PORT_NETWORK_VERSION 34
/* ... the advertisement's reserved bytes: the version (a little-endian word),
then flags */
#define HALO_PORT_ADVERTISED_VERSION_OFFSET 0
+1 -1
View File
@@ -1,4 +1,4 @@
version 33
version 34
bound CHAT_TEXT_SIZE 64
bound DISCORD_ID_SIZE 24
bound DISCORD_NAME_SIZE 40
+10 -5
View File
@@ -149,15 +149,20 @@ void network_damage_player_effect(long player_index, struct damage_data const *d
void network_damage_aftermath(long object_index, struct damage_data const *damage, unsigned long being_damaged_flags,
real shield_damage, real body_damage, real body_damage_multiplier, short body_part, short node_index,
short region_index, short material_index, long victim_player_index);
boolean network_damage_static_target(long object_index);
/* set while a distributed client carries out a kill it does not decide (an
act of god: the host's word, network_distributed.c, or the world's) */
static boolean distributed_damage_authorized;
/* whether this machine runs objects' shields (their stun, recharge and
/* whether this machine runs the object's shields (their stun, recharge and
overcharge): not a client of the distributed netcode, which has the host's
(damage_set_network_state) */
#define objects_update_shields() (!network_game_distributed_client())
(damage_set_network_state), but for the map's scenery and devices that
damage can break, whose damage every machine deals as the host did and of
which the host sends no state (network_damage.c's static targets: a
scenery's shield, broken, came back on no client) */
#define objects_update_shields(object_index) \
(!network_game_distributed_client() || network_damage_static_target(object_index))
/* ---------- constants */
@@ -2033,10 +2038,10 @@ void object_damage_update(
FLAG(_object_die_act_of_god_no_statistics_bit));
}
if (objects_update_shields())
if (objects_update_shields(object_index))
SET_FLAG(object->object.damage_flags, _object_shield_charging_bit, FALSE);
damage_flags = object->object.damage_flags;
if (objects_update_shields() &&
if (objects_update_shields(object_index) &&
object->object.maximum_shield_vitality > 0.f &&
!TEST_FLAG(damage_flags, _object_dead_bit))
{
@@ -71,6 +71,8 @@ static boolean distributed_damage_is_recoil(struct damage_data const *damage, lo
static void damage_replay_player_effect(long index, struct damage_data *damage, real amount) { }
static boolean network_objects_client_has(long index) { return TRUE; }
static boolean static_target(long index) { return FALSE; }
/* (no static targets here, so no damage is one's own: test_static_target) */
static boolean static_target_damage(struct damage_data const *damage) { return FALSE; }
static void distributed_damage_from_data(struct damage_data const *damage, struct damage_data *result) { *result = *damage; }
static void object_get_origin(long index, real_point3d *point) { memset(point, 0, sizeof(*point)); }
static long distributed_latest_host_time(void) { return 10; }
+72
View File
@@ -0,0 +1,72 @@
#include "harness.h"
/* network_damage.c's static_target: the map's scenery and devices whose hits
every machine shares, those with a body damage can destroy and those whose
shield is all it can break. Objects: 0 scenery with a body, 1 scenery with a
shield only (an energy shield, a tripmine), 2 scenery with neither, 3 a device
with a shield only, 4 a unit with a shield (not one: its own state is sent) */
enum { _object_mask_scenery = 1, _object_mask_device = 2, _object_mask_unit = 4 };
#define STATIC_TARGET_TYPES (_object_mask_scenery | _object_mask_device)
static struct { long mask; real body, shield; } objects[] = {
{ _object_mask_scenery, 50.0f, 0.0f }, { _object_mask_scenery, 0.0f, 80.0f }, { _object_mask_scenery, 0.0f, 0.0f },
{ _object_mask_device, 0.0f, 20.0f }, { _object_mask_unit, 75.0f, 75.0f },
};
static void *object_try_and_get_and_verify_type(long object_index,long mask)
{
return object_index>=0&&object_index<5&&(objects[object_index].mask&mask)?&objects[object_index]:NULL;
}
static real object_get_maximum_body_vitality(long object_index,boolean ignore_difficulty)
{
(void)ignore_difficulty;return objects[object_index].body;
}
static real object_get_maximum_shield_vitality(long object_index,boolean ignore_difficulty)
{
(void)ignore_difficulty;return objects[object_index].shield;
}
struct damage_data { long owner_object_index; };
/* where each object is, and its size (static_target_near) */
static real_point3d origins[] = { { 0, 0, 0 }, { 10, 0, 0 }, { 10.5f, 0, 0 }, { 13, 0, 0 }, { 10.1f, 0, 0 } };
struct object_datum { struct { real bounding_sphere_radius; } object; };
static struct object_datum datums[5] = { { { 0.2f } }, { { 0.2f } }, { { 0.2f } }, { { 0.2f } }, { { 0.5f } } };
#define object_get(index) (&datums[index])
static void object_get_origin(long object_index,real_point3d *origin){*origin=origins[object_index];}
struct object_iterator { long index; long next; unsigned long mask; };
static void object_iterator_new(struct object_iterator *iterator,unsigned long mask,byte flags)
{
(void)flags;iterator->next=0;iterator->mask=mask;iterator->index=NONE;
}
static void *object_iterator_next(struct object_iterator *iterator)
{
while(iterator->next<5){long index=iterator->next++;if(objects[index].mask&iterator->mask){iterator->index=index;return &datums[index];}}
return NULL;
}
#include "under_test.inc"
int main(int argc,char **argv)
{
const char *case_name=argc>1?argv[1]:"";
CASE("body") {
CHECK(static_target(0),"scenery with a body");
} else CASE("shield") {
CHECK(static_target(1),"scenery with a shield only");
CHECK(static_target(3),"a device with a shield only");
} else CASE("neither") {
CHECK(!static_target(2),"scenery damage cannot break");
CHECK(!static_target(4),"a unit, whose state the host sends");
CHECK(!static_target(NONE),"no object");
} else CASE("damage") {
/* a tripmine's own explosion (its owner the tripmine), and a player's grenade's (the grenade, no static target) */
struct damage_data tripmine={1},grenade={4},none={NONE},light={0};
CHECK(static_target_damage(&tripmine),"a shield scenery's own damage");
CHECK(static_target_damage(&light),"a destructible scenery's own damage");
CHECK(!static_target_damage(&grenade),"what a unit or projectile owns");
CHECK(!static_target_damage(&none),"damage with no owner object");
} else CASE("near") {
/* another machine's copy of a tripmine (object 1, a shield) where this machine has it, and not too far */
real_point3d at={10.3f,0,0},away={11.6f,0,0},nothing={30,0,0};
CHECK(static_target_near(&at,1.0f)==1,"the nearest static target, %ld",static_target_near(&at,1.0f));
CHECK(static_target_near(&away,1.0f)==NONE,"no unit, nothing damage cannot break, none farther");
CHECK(static_target_near(&nothing,1.0f)==NONE,"none within reach");
CHECK(static_target_near(&at,0.0f)==NONE,"its size and the reach only");
{ real_point3d between={12,0,0}; CHECK(static_target_near(&between,2.0f)==3,"the nearer of two, %ld",static_target_near(&between,2.0f)); }
} else return 2;
return 0;
}
+37
View File
@@ -0,0 +1,37 @@
"""The scenery and devices whose damage every machine shares: a body to destroy, or a shield to break."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from harness import CHECK_FAILED, build, function, mutated, read, run
def generated(fault=None):
source = read('port/linux/game/network_damage.c')
code = '\n'.join(function(source, name) for name in ('static_target', 'static_target_damage',
'distributed_distance_squared', 'static_target_near'))
if fault:
code = mutated(code, *fault)
return (('under_test.inc', code),)
@pytest.mark.parametrize('case', ['body', 'shield', 'neither', 'damage', 'near'])
def test_case(case):
status, output = run(build('static_target', generated()), case)
assert status == 0, output
@pytest.mark.parametrize('fault,case', [
# (the shield-only objects left out, as before: Rat Race's shields, a map's tripmines)
(('||\n\t\t\tobject_get_maximum_shield_vitality(object_index, TRUE) > 0.0f', ''), 'shield'),
(('STATIC_TARGET_TYPES)', 'STATIC_TARGET_TYPES | _object_mask_unit)'), 'neither'),
(('object_get_maximum_body_vitality(object_index, TRUE) > 0.0f ||', ''), 'body'),
(('damage->owner_object_index != NONE && static_target(damage->owner_object_index)', 'FALSE'), 'damage'),
(('distance < nearest_distance', 'distance > nearest_distance'), 'near'),
(('if (!static_target(iterator.index))\n\t\t\tcontinue;', ''), 'near'),
])
def test_negative_control(fault, case):
status, output = run(build('static_target', generated(fault)), case)
assert status == CHECK_FAILED, output