Network: glass and destructible scenery break the same for everyone

Breakable surfaces (glass) are broken by each machine's own projectiles
with its own random damage, so a pane could shatter on the host and stay
whole, still blocking, on a client. In co-op the host now sends each break
(and a few again every refresh, for late joiners), and a client breaks the
same surface with its shards.

Destructible scenery and devices (lights, panels) are the map's, at the
same index on every machine, but only damage to units was synced: a
client's shots never damaged them, and what the host broke didn't break
or play its effects on the clients. A client now reports its hits on them
like any other, the host checks and deals them, and every client deals the
host's damage to its own copy, which breaks, plays its effects and is
destroyed as the host's was.

Both are new message kinds that a client without them ignores, so the
network version is unchanged.
This commit is contained in:
xshxdex98 committed 2026-10-05 13:00:01 -05:00
1 parent 13c14df95c
commit 7a1ffca28b
6 files changed
+161 -1

No files matched your search

+61
View File
@@ -29,6 +29,8 @@ that clients need to see is sent from this file:
every tick in rotation, those that have moved since the map loaded more
often. A scenario group is identified by its index, which is the same on
every machine; a device's own group by the device.
- Breakable surfaces (glass) the host breaks, each once as it breaks and
a few at a time again in rotation (for a late joiner or a lost message).
- Named objects (scenery and devices) the scripts create or destroy.
network_objects.c already handles units, vehicles, weapons and equipment.
Twice a second the host sends which object names currently exist. A
@@ -93,6 +95,8 @@ index and tag, since the map placed them at the same index everywhere.
#include "objects/object_types.h"
#include "objects/damage.h"
#include "objects/scenery.h"
#include "physics/breakable_surfaces.h"
#include "structures/structure_bsp_definitions.h"
#include "physics/collisions.h"
#include "rasterizer/rasterizer_cinematics.h"
#include "saved games/game_state.h"
@@ -187,6 +191,9 @@ enum
_coop_event_cutscene_skipped,
/* a unit opened (value TRUE) or closed: a dropship's doors */
_coop_event_unit_open,
/* a breakable surface broke: value its index, frame its BSP, reals the
damage's epicenter */
_coop_event_surface_broken,
};
/* distributed_coop_event.type of an effect: at a cutscene flag (value), or on
@@ -1805,6 +1812,14 @@ static void client_apply_event(
case _coop_event_cutscene_skipped:
client_stop_script_sounds();
break;
case _coop_event_surface_broken:
if (event->frame == global_structure_bsp_index)
{
real_point3d epicenter = { event->reals[0], event->reals[1], event->reals[2] };
breakable_surface_port_break(event->value, &epicenter);
}
break;
case _coop_event_unit_open:
if (distributed_object_index_valid(event->object_index) && network_objects_client_has(event->object_index) &&
object_try_and_get_and_verify_type(event->object_index, _object_mask_unit))
@@ -2430,6 +2445,51 @@ void network_coop_note_effect(
event->value = cutscene_flag_index;
}
void network_coop_note_surface_broken(
short breakable_surface_index,
real_point3d const *epicenter)
{
struct distributed_coop_event *event = event_new(_coop_event_surface_broken);
if (!event)
return;
event->value = breakable_surface_index;
event->frame = global_structure_bsp_index;
event->reals[0] = epicenter->x;
event->reals[1] = epicenter->y;
event->reals[2] = epicenter->z;
}
/* host: a few broken surfaces again each refresh, round them all, for a
machine that joined since or lost the message (broken from their middle) */
static void host_send_broken_surfaces(
void)
{
enum { SURFACES_PER_REFRESH = 8 };
static short cursor;
struct structure_bsp *structure_bsp = global_structure_bsp_get();
short count = (short)structure_bsp->breakable_surfaces.count;
short sent = 0;
short step;
if (!host_resend.refresh || count <= 0)
return;
for (step = 0; step < count && sent < SURFACES_PER_REFRESH; step++)
{
short index = (short)((cursor + step) % count);
if (!breakable_surface_extant(index))
{
struct structure_breakable_surface *surface = TAG_BLOCK_GET_ELEMENT(&structure_bsp->breakable_surfaces,
index, struct structure_breakable_surface);
network_coop_note_surface_broken(index, &surface->centroid);
sent++;
}
}
cursor = (short)((cursor + step) % count);
}
void network_coop_note_object_effect(
long effect_definition_index,
long object_index,
@@ -2537,6 +2597,7 @@ void network_coop_host_tick(
host_send_attachments();
host_send_looping_sounds();
host_send_hud_state();
host_send_broken_surfaces();
host_send_events();
}
+4
View File
@@ -88,6 +88,10 @@ void network_coop_note_attach(long parent_index, char const *parent_marker_name,
char const *child_marker_name);
void network_coop_note_detach(long parent_index, long child_index);
void network_coop_note_object_effect(long effect_definition_index, long object_index, char const *marker_name);
/* breakable_surfaces.c: a breakable surface (glass) broke, from damage at
`epicenter`. Each machine's own shots break glass too, but with their own
random damage, so the host's breaks are the ones everyone keeps. */
void network_coop_note_surface_broken(short breakable_surface_index, real_point3d const *epicenter);
void network_coop_note_scenery_animation(long object_index, long animation_graph_index, short animation_index,
short frame_index);
+48 -1
View File
@@ -34,6 +34,10 @@ 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.
*/
#include "cseries.h"
@@ -59,6 +63,8 @@ being hit looks and feels like on the clients).
void damage_replay_player_effect(long player_index, struct damage_data *damage, real total_damage);
void damage_replay_aftermath(long object_index, struct damage_data *damage, unsigned long being_damaged_flags,
real shield_damage, real body_damage, real body_damage_multiplier, short body_part);
void damage_replay_static(long object_index, struct damage_data *damage, short node_index, short region_index,
short material_index);
void damage_replay_kill(long object_index, struct damage_data *damage, short node_index, short region_index,
short material_index);
/* network_distributed.c's */
@@ -142,8 +148,14 @@ enum
_damage_event_player_effect,
_damage_event_aftermath,
_damage_event_kill,
/* damage to destructible scenery or a device, to every machine */
_damage_event_static,
};
/* the objects every machine has from the map, at the same index, which
damage can break (static_target) */
#define STATIC_TARGET_TYPES (_object_mask_scenery | _object_mask_device)
/* what a player's damage is, by how the game deals it (the shape of their
report of it) */
enum
@@ -483,6 +495,14 @@ static boolean distributed_damage_to_data(
return TRUE;
}
/* whether the object is scenery or a device that damage can destroy */
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_cause_damage (damage.c) */
/* whether the damage is a weapon's own at the unit that fires it (weapons.c:
@@ -629,7 +649,8 @@ 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) && network_objects_client_has(object_index) &&
if (distributed_player_is_local(damage->owner_player_index) &&
(network_objects_client_has(object_index) || static_target(object_index)) &&
damage_report_count < MAXIMUM_HIT_REPORTS_PER_TICK)
{
struct distributed_hit_report *report = &damage_reports[damage_report_count++];
@@ -736,6 +757,21 @@ void network_damage_aftermath(
if (game_connection() != _game_connection_network_server)
return;
if (static_target(object_index))
{
if (damage_event_count >= MAXIMUM_DAMAGE_EVENTS_PER_TICK - RESERVED_KILL_EVENTS)
return;
event = &damage_events[damage_event_count++];
csmemset(event, 0, sizeof(*event));
event->kind = _damage_event_static;
event->player_index = NO_PLAYER;
event->object_index = object_index;
distributed_damage_from_data(damage, &event->damage);
event->node_index = node_index;
event->region_index = region_index;
event->material_index = material_index;
return;
}
/* (units only: items and the like the objects' states place) */
unit = (struct unit_datum *)object_try_and_get_and_verify_type(object_index, _object_mask_unit);
if (!unit)
@@ -1719,6 +1755,8 @@ static boolean distributed_report_valid(
return FALSE;
target = (struct object_datum *)object_try_and_get_and_verify_type(report->object_index,
_object_mask_biped | _object_mask_vehicle | _object_mask_weapon | _object_mask_equipment);
if (!target && static_target(report->object_index))
target = object_get(report->object_index);
if (!target || !tag_index_is_group(report->damage.definition_index, DAMAGE_EFFECT_DEFINITION_TAG))
return FALSE;
/* numbers the game can take */
@@ -2160,6 +2198,15 @@ void network_damage_handle_events(
players_show_telefragged(victim_player_index);
}
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,
event->material_index))
{
damage_replay_static(event->object_index, &damage, event->node_index, event->region_index,
event->material_index);
}
break;
}
}
}
+14
View File
@@ -2654,6 +2654,20 @@ void damage_replay_kill(
distributed_damage_authorized = FALSE;
}
/* ... damage to scenery or a device, dealt as the host dealt it: the copy
breaks, plays its effects and is destroyed as the host's was */
void damage_replay_static(
long object_index,
struct damage_data *damage,
short node_index,
short region_index,
short material_index)
{
distributed_damage_authorized = TRUE;
object_cause_damage(damage, object_index, node_index, region_index, material_index, NULL);
distributed_damage_authorized = FALSE;
}
/* the host's: what damage_set_network_state takes */
void damage_get_network_state(
long object_index,
+28
View File
@@ -25,6 +25,7 @@ BREAKABLE_SURFACES.C
#include "sound/game_sound.h"
#include "structures/structure_bsp_definitions.h"
#include "tag_files/tag_groups.h"
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
/* ---------- structures */
@@ -189,6 +190,8 @@ void breakable_surface_damage(
BIT_VECTOR_SET_FLAG((long *)breakable_surface_flags_get(), breakable_surface_index, FALSE);
breakable_surface_effect(breakable_surface_index, damage_data, seed_surface_index);
/* port: and on network co-op's clients */
network_coop_note_surface_broken(breakable_surface_index, &damage_data->epicenter);
}
}
}
@@ -236,6 +239,8 @@ void breakable_surface_damage_area_of_effect(
breakable_surface_get(breakable_surface_index)->vitality = 0.0f;
BIT_VECTOR_SET_FLAG((long *)breakable_surface_flags_get(), breakable_surface_index, FALSE);
breakable_surface_effect(breakable_surface_index, damage_data, breakable_surface->collision_surface_index);
/* port: and on network co-op's clients */
network_coop_note_surface_broken(breakable_surface_index, &damage_data->epicenter);
}
}
}
@@ -244,6 +249,29 @@ void breakable_surface_damage_area_of_effect(
return;
}
void breakable_surface_port_break(
short breakable_surface_index,
real_point3d const *epicenter)
{
struct structure_bsp *structure_bsp = global_structure_bsp_get();
struct structure_breakable_surface *breakable_surface;
struct damage_data damage;
if (breakable_surface_index < 0 || breakable_surface_index >= structure_bsp->breakable_surfaces.count ||
!breakable_surface_extant(breakable_surface_index))
{
return;
}
breakable_surface = TAG_BLOCK_GET_ELEMENT(&structure_bsp->breakable_surfaces, breakable_surface_index,
struct structure_breakable_surface);
/* (the shards fly away from the epicenter, all the effect reads) */
csmemset(&damage, 0, sizeof(damage));
damage.epicenter = *epicenter;
breakable_surface_get(breakable_surface_index)->vitality = 0.0f;
BIT_VECTOR_SET_FLAG((long *)breakable_surface_flags_get(), breakable_surface_index, FALSE);
breakable_surface_effect(breakable_surface_index, &damage, breakable_surface->collision_surface_index);
}
/* ---------- private code */
static void breakable_surface_effect(
+6
View File
@@ -74,6 +74,12 @@ struct breakable_surface_particle_effect
struct breakable_surface_datum *breakable_surface_get(
short breakable_surface_index);
/* port: breaks a surface as damage at `epicenter` would, shards and all,
for a network co-op client the host told (port/linux/game/network_coop.c);
nothing if it is already broken */
void breakable_surface_port_break(
short breakable_surface_index,
real_point3d const *epicenter);
void breakable_surfaces_initialize(
void);
void breakable_surfaces_dispose(