diff --git a/port/linux/game/network_coop.c b/port/linux/game/network_coop.c index 9e38dd31..0c189e94 100644 --- a/port/linux/game/network_coop.c +++ b/port/linux/game/network_coop.c @@ -945,6 +945,10 @@ static struct real power; } host_sent_devices[MAXIMUM_OBJECTS_PER_MAP]; +/* host: the structure BSP each client player's machine last said it has, +NONE before its first input */ +static short host_player_structure_bsps[MAXIMUM_TRACKED_PLAYERS]; + /* host, each tick: the devices that moved or changed power (and, with the resent state, all that ever have) */ static void host_send_device_states( @@ -1903,6 +1907,7 @@ void network_coop_new_game( csmemset(host_sent_transforms, 0, sizeof(host_sent_transforms)); csmemset(host_sent_looks, 0, sizeof(host_sent_looks)); csmemset(host_sent_devices, 0, sizeof(host_sent_devices)); + csmemset(host_player_structure_bsps, NONE, sizeof(host_player_structure_bsps)); csmemset(&host_sent_screen_effect, 0, sizeof(host_sent_screen_effect)); csmemset(&host_resend, 0, sizeof(host_resend)); csmemset(&players_vitality, 0, sizeof(players_vitality)); @@ -2582,6 +2587,21 @@ word network_coop_device_group_entry_size( return sizeof(struct distributed_coop_device_group); } +void network_coop_note_player_structure_bsp( + short player_index, + short structure_bsp_index) +{ + if (player_index >= 0 && player_index < MAXIMUM_TRACKED_PLAYERS) + host_player_structure_bsps[player_index] = structure_bsp_index; +} + +boolean network_coop_player_has_structure_bsp( + long player_index) +{ + return !network_coop_active() || player_get(player_index)->local_player_index != NONE || + host_player_structure_bsps[DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index)] == global_structure_bsp_index_get(); +} + word network_coop_device_state_entry_size( void) { diff --git a/port/linux/game/network_coop.h b/port/linux/game/network_coop.h index 306f6ee7..30c330d7 100644 --- a/port/linux/game/network_coop.h +++ b/port/linux/game/network_coop.h @@ -52,6 +52,13 @@ void network_coop_note_device_snap(short group_index); /* unit_scripting_commands.c: a script set a unit's maximum or current vitality; TRUE if it was a co-op player's, now set on every player's */ boolean network_coop_set_players_vitality(long unit_index, boolean maximum, real body, real shield); +/* host: whether the player's machine has the host's structure BSP loaded +(always outside co-op, and for the host's own players). Until a client has, +the host takes none of its players' movement, which falls there with no +floor, and lets them trigger no BSP switch (players.c). */ +boolean network_coop_player_has_structure_bsp(long player_index); +/* network_distributed.c: the structure BSP a client's input says it has */ +void network_coop_note_player_structure_bsp(short player_index, short structure_bsp_index); /* units.c: a vehicle left the vehicle carrying it (a Pelican's Warthog drop) */ void network_coop_vehicle_dropped(long vehicle_index, long carrier_index); diff --git a/port/linux/game/network_distributed.c b/port/linux/game/network_distributed.c index 73aa6497..454f0a50 100644 --- a/port/linux/game/network_distributed.c +++ b/port/linux/game/network_distributed.c @@ -320,7 +320,9 @@ ticks before it */ struct distributed_player_input { byte player_index; - byte pad[3]; + /* co-op: the structure BSP the client has loaded */ + byte structure_bsp_index; + byte pad[2]; /* the client's tick */ long tick; /* the latest host tick the client has had a message of (the host @@ -2083,9 +2085,14 @@ static void distributed_apply_predictions( { distributed_predictions[player_index].valid = FALSE; distributed_predictions[player_index].taken_host_time = NONE; - /* (of the unit it has now: not one of a life before) */ - if (unit_index != NONE && unit_index == state->unit_index) + /* (of the unit it has now: not one of a life before; and not + from a co-op client still loading the host's BSP, whose player + falls there with no floor under it) */ + if (unit_index != NONE && unit_index == state->unit_index && + network_coop_player_has_structure_bsp(unit_get(unit_index)->unit.player_index)) + { distributed_take_prediction(player_index, unit_index, &bound); + } } /* (what the host's next tick starts from) */ if (unit_index != NONE) @@ -2257,6 +2264,7 @@ static void distributed_client_send_inputs( if (!update_client_distributed_input(local_player_index, &input->tick, &input->action, input->control_flags)) continue; input->player_index = distributed_player_to_byte(player_index); + input->structure_bsp_index = (byte)global_structure_bsp_index_get(); input->host_time = distributed_host_time; count++; } @@ -2330,6 +2338,7 @@ static void distributed_handle_inputs( /* (a spectator's buttons pick whom it watches: not counted) */ if (network_coop_active() && player->unit_index != NONE) distributed_note_input_actions(input->player_index, &action); + network_coop_note_player_structure_bsp(input->player_index, input->structure_bsp_index); update_server_handle_distributed_input(DATUM_INDEX_NEW(input->player_index, player->identifier), input->tick, &action, input->control_flags, DISTRIBUTED_INPUT_HISTORY); if (input->host_time != NONE && (host_time == NONE || input->host_time > host_time)) @@ -3001,10 +3010,13 @@ static void distributed_send_pings( } /* Co-op: the host's structure BSP. Clients switch BSP only when told -(their own trigger volumes don't, players.c). It is resent regularly so a -client that lost a message or joined late catches up. */ +(their own trigger volumes don't, players.c). A switch is sent at once and +reliably; it is also resent regularly so a client that joined late catches +up. */ #define STRUCTURE_BSP_INTERVAL_TICKS (TICKS_PER_SECOND / 2) +static short distributed_sent_structure_bsp_index = NONE; + struct distributed_structure_bsp { short structure_bsp_index; @@ -3021,10 +3033,14 @@ static void distributed_send_structure_bsp( void) { struct distributed_structure_bsp_message message; + short index = global_structure_bsp_index_get(); + boolean switched = index != distributed_sent_structure_bsp_index; - message.structure_bsp.structure_bsp_index = global_structure_bsp_index_get(); + message.structure_bsp.structure_bsp_index = index; message.structure_bsp.pad = 0; - distributed_send(&message, _distributed_message_structure_bsp, 1, (word)sizeof(message), _distributed_to_clients); + distributed_send(&message, _distributed_message_structure_bsp, 1, (word)sizeof(message), + switched ? _distributed_to_clients_reliably : _distributed_to_clients); + distributed_sent_structure_bsp_index = index; } static void distributed_handle_structure_bsp( @@ -3287,10 +3303,15 @@ void network_distributed_tick( distributed_statistics_due = FALSE; if (game_time_get() % PING_INTERVAL_TICKS == 0) distributed_send_pings(); + /* (before the players, so clients load a new BSP before they hear + where the host moved everyone into it) */ + if (network_coop_active() && (global_structure_bsp_index_get() != distributed_sent_structure_bsp_index || + game_time_get() % STRUCTURE_BSP_INTERVAL_TICKS == 0)) + { + distributed_send_structure_bsp(); + } distributed_host_send_players(); network_actors_host_tick(); - if (network_coop_active() && game_time_get() % STRUCTURE_BSP_INTERVAL_TICKS == 0) - distributed_send_structure_bsp(); network_coop_host_tick(); distributed_send_pickups(); if (game_time_get() % GAME_STATE_INTERVAL_TICKS == 0) diff --git a/port/linux/game/network_objects.c b/port/linux/game/network_objects.c index a59b562d..3f0f6a62 100644 --- a/port/linux/game/network_objects.c +++ b/port/linux/game/network_objects.c @@ -1795,6 +1795,10 @@ void network_objects_apply_vehicle_predictions( { continue; } + /* (not from a co-op client still loading the host's BSP: the + vehicle falls there with no ground under it) */ + if (!network_coop_player_has_structure_bsp(driver->unit.player_index)) + continue; dx = state->position.x - vehicle->object.position.x; dy = state->position.y - vehicle->object.position.y; dz = state->position.z - vehicle->object.position.z; diff --git a/source/game/players.c b/source/game/players.c index 17dfb825..98623f10 100644 --- a/source/game/players.c +++ b/source/game/players.c @@ -470,9 +470,10 @@ static struct long respawn_wait_since; /* when each player's unit left the loaded structure BSP */ long stranded_since[NETWORK_GAME_MAXIMUM_PLAYER_COUNT]; - /* no BSP switch trigger counts before this game time (one has just - switched, and the players are being brought into the new BSP) */ - long bsp_switch_allowed_time; + /* the structure BSP loaded, and when it last switched, by a trigger, + a script or a skipped cutscene (players_coop_bsp_switch_ready) */ + short structure_bsp_index; + long bsp_switch_time; /* the structure BSPs the players have been in this map, by bit */ word visited_structure_bsps; /* where each player last stood on the ground, and on which BSP: where @@ -2771,8 +2772,10 @@ someone who is only crossing a seam or riding over a gap. */ #define COOP_STRANDED_TICKS (2 * TICKS_PER_SECOND) /* After a BSP switch, how long the co-op host lets no other trigger switch -it while the players are brought into the new one */ +it while the players are brought into the new one, and the longest it then +waits for every machine to have loaded it */ #define COOP_BSP_SWITCH_SETTLE_TICKS (2 * TICKS_PER_SECOND) +#define COOP_BSP_SWITCH_WAIT_TICKS (10 * TICKS_PER_SECOND) /* Going back to a BSP the team has already been in needs two thirds of the living players at the trigger: inside it, or this near the player in it @@ -2987,10 +2990,37 @@ static boolean players_coop_in_bsp_switch_trigger( return FALSE; } +/* port: whether a network co-op host lets any trigger switch the BSP yet. +After a switch it waits COOP_BSP_SWITCH_SETTLE_TICKS while the players are +brought in, then until every player's machine has loaded the new BSP, so no +client falls two switches behind. A client stuck longer than +COOP_BSP_SWITCH_WAIT_TICKS no longer holds the team. */ +static boolean players_coop_bsp_switch_ready( + void) +{ + long since = game_time_get() - players_coop_state.bsp_switch_time; + struct data_iterator iterator; + struct player_datum *player; + + if (since < COOP_BSP_SWITCH_SETTLE_TICKS) + return FALSE; + if (since >= COOP_BSP_SWITCH_WAIT_TICKS) + return TRUE; + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + if (player->unit_index != NONE && !network_coop_player_has_structure_bsp(iterator.datum_index)) + return FALSE; + } + + return TRUE; +} + /* port: whether a network co-op host lets the unit on this trigger switch -the BSP, which brings the whole team along. Anyone can lead the team into a -BSP it hasn't been in; going back to one needs two thirds of the team -there, so one player can't drag everyone back through the level. */ +the BSP, which brings the whole team along. Not while the unit's machine +is still loading the BSP the host has. Anyone can lead the team into a BSP +it hasn't been in; going back to one needs two thirds of the team there, +so one player can't drag everyone back through the level. */ static boolean players_coop_bsp_switch_allowed( short bsp_switch_trigger_volume_index, long unit_index) @@ -3004,6 +3034,8 @@ static boolean players_coop_bsp_switch_allowed( short living_count = 0; short gathered_count = 0; + if (!network_coop_player_has_structure_bsp(unit_get(unit_index)->unit.player_index)) + return FALSE; if (!VALID_INDEX(volume->destination_structure_bsp_index, MAXIMUM_STRUCTURE_BSPS_PER_SCENARIO) || !TEST_FLAG(players_coop_state.visited_structure_bsps, volume->destination_structure_bsp_index)) { @@ -3053,7 +3085,7 @@ static void player_teleport_on_bsp_switch( whoever crossed it, as split screen does. Only one BSP is ever loaded, so anyone left elsewhere would stand outside it, or on a trigger that switches it straight back (players_update_after_game - waits COOP_BSP_SWITCH_SETTLE_TICKS before another switch). */ + waits for players_coop_bsp_switch_ready before another switch). */ if (network_coop_active()) { outside_switch_trigger = TRUE; @@ -3093,6 +3125,10 @@ static void player_teleport_on_bsp_switch( player_index, source_unit_index, position); + /* port: a big co-op team runs out of room around the + crosser, so the rest go beside whoever is already in */ + if (!teleport_succeeded && network_coop_active()) + teleport_succeeded = player_place_beside_teammate(player_index); } else { @@ -4550,6 +4586,11 @@ void players_update_after_game( } if (VALID_INDEX(global_structure_bsp_index, MAXIMUM_STRUCTURE_BSPS_PER_SCENARIO)) SET_FLAG(players_coop_state.visited_structure_bsps, global_structure_bsp_index, TRUE); + if (players_coop_state.structure_bsp_index != global_structure_bsp_index) + { + players_coop_state.structure_bsp_index = global_structure_bsp_index; + players_coop_state.bsp_switch_time = game_time_get(); + } data_iterator_new(&iterator, player_data); while (player = data_iterator_next(&iterator)) @@ -4597,11 +4638,10 @@ void players_update_after_game( root_object_index = object_get_ultimate_parent(player->unit_index); root_object = object_get(root_object_index); /* port: a co-op client only switches BSP when the host does - (network_distributed.c), and the host not again until the players - are in the new one */ + (network_distributed.c), and the host not again until everyone + has the new one */ if (!TEST_FLAG(root_object->object.flags, _object_outside_of_map_bit) && - !(network_coop_active() && - (network_game_distributed_client() || game_time_get() < players_coop_state.bsp_switch_allowed_time))) + !(network_coop_active() && (network_game_distributed_client() || !players_coop_bsp_switch_ready()))) { scenario = global_scenario_get(); for (bsp_switch_trigger_volume_index = 0; @@ -4637,7 +4677,7 @@ void players_update_after_game( bsp_switch_trigger_volume_index; main_switch_structure_bsp( bsp_switch_trigger_volume->destination_structure_bsp_index); - players_coop_state.bsp_switch_allowed_time = game_time_get() + COOP_BSP_SWITCH_SETTLE_TICKS; + players_coop_state.bsp_switch_time = game_time_get(); } } }