use crate::config::{InputConfig, SharedInputConfig};
use crate::input_buffer::InputBuffer;
use crate::input_message::{
ActionStateQueryData, ActionStateSequence, InputMessage, InputSnapshot, InputTarget,
PerTargetData, StateMut, StateRef,
};
#[cfg(feature = "metrics")]
use crate::metric_handles::InputMetricHandles;
use crate::plugin::InputPlugin;
use crate::{HISTORY_DEPTH, InputChannel};
use alloc::{vec, vec::Vec};
use bevy_app::{
App, FixedPostUpdate, FixedPreUpdate, Plugin, PostUpdate, PreUpdate, RunFixedMainLoopSystems,
};
use bevy_ecs::entity::{MapEntities, UniqueEntitySlice};
use bevy_ecs::prelude::*;
#[cfg(feature = "interpolation")]
use bevy_time::Fixed;
use bevy_time::{Real, Time, Timer, TimerMode};
use bevy_utils::prelude::DebugName;
use lightyear_connection::network_topology::{NetworkTopology, NetworkingMetadata};
use lightyear_core::prelude::*;
use lightyear_core::tick::TickDuration;
#[cfg(feature = "interpolation")]
use lightyear_core::time::TickDelta;
#[cfg(feature = "interpolation")]
use lightyear_interpolation::prelude::*;
use lightyear_messages::multi::MultiMessageSender;
use lightyear_messages::plugin::MessageSystems;
#[cfg(feature = "prediction")]
use lightyear_messages::prelude::MessageReceiver;
use lightyear_messages::prelude::MessageSender;
use lightyear_prediction::prelude::*;
use lightyear_replication::prelude::{ControlledBy, PreSpawned};
use lightyear_sync::plugin::SyncSystems;
use lightyear_sync::prelude::{InputTimelineConfig, LocalTimelineSync, SyncedLocalTimeline};
use lightyear_transport::prelude::ChannelRegistry;
#[allow(unused_imports)]
use tracing::{debug, error, info, trace, warn};
#[deprecated(note = "Use InputSystems instead")]
pub type InputSet = InputSystems;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone, Copy)]
pub enum InputSystems {
ReceiveInputMessages,
WriteClientInputs,
BufferClientInputs,
PrepareInputMessage,
RestoreInputs,
UpdateRemoteInputTicks,
SendInputMessage,
CleanUp,
}
pub struct ClientInputPlugin<S: ActionStateSequence> {
config: InputConfig<S::Action>,
}
impl<S: ActionStateSequence> ClientInputPlugin<S> {
pub fn new(config: InputConfig<S::Action>) -> Self {
Self { config }
}
}
impl<S: ActionStateSequence> ClientInputPlugin<S> {
fn default() -> Self {
Self::new(InputConfig::default())
}
}
impl<S: ActionStateSequence + MapEntities> Plugin for ClientInputPlugin<S> {
fn build(&self, app: &mut App) {
if !app.is_plugin_added::<InputPlugin<S>>() {
app.add_plugins(InputPlugin::<S>::default());
}
app.init_resource::<SharedInputConfig>();
app.insert_resource(self.config);
app.init_resource::<MessageBuffer<S>>();
app.init_resource::<LocalTimelineSync>();
app.init_resource::<InputTimelineConfig>();
#[cfg(feature = "prediction")]
{
if !app.is_plugin_added::<LastConfirmedInputPlugin>() {
app.add_plugins(LastConfirmedInputPlugin);
}
}
app.configure_sets(
PreUpdate,
InputSystems::ReceiveInputMessages
.before(RunFixedMainLoopSystems::FixedMainLoop)
.after(RunFixedMainLoopSystems::BeforeFixedMainLoop),
);
app.configure_sets(
FixedPreUpdate,
(
InputSystems::WriteClientInputs,
InputSystems::BufferClientInputs,
)
.chain(),
);
app.configure_sets(FixedPostUpdate, InputSystems::RestoreInputs);
app.configure_sets(
PostUpdate,
((
InputSystems::PrepareInputMessage,
SyncSystems::Sync,
InputSystems::SendInputMessage,
InputSystems::CleanUp,
MessageSystems::Send,
)
.chain(),),
);
#[cfg(feature = "prediction")]
{
app.configure_sets(
PreUpdate,
InputSystems::ReceiveInputMessages
.after(MessageSystems::Receive)
.before(RollbackSystems::Check),
);
app.add_systems(
PreUpdate,
receive_remote_player_input_messages::<S>
.in_set(InputSystems::ReceiveInputMessages),
);
app.configure_sets(
PostUpdate,
InputSystems::UpdateRemoteInputTicks.after(SyncSystems::Sync),
);
app.add_systems(
PostUpdate,
update_last_confirmed_input::<S>.in_set(InputSystems::UpdateRemoteInputTicks),
);
}
app.add_systems(
FixedPreUpdate,
(buffer_action_state::<S>, get_action_state::<S>)
.chain()
.in_set(InputSystems::BufferClientInputs),
);
app.add_systems(
FixedPostUpdate,
(
get_delayed_action_state::<S>.in_set(InputSystems::RestoreInputs),
),
);
app.add_systems(
PostUpdate,
(
prepare_input_message::<S>.in_set(InputSystems::PrepareInputMessage),
clean_buffers::<S>.in_set(InputSystems::CleanUp),
send_input_messages::<S>.in_set(InputSystems::SendInputMessage),
),
);
app.add_observer(receive_local_timeline_shift::<S>);
}
}
#[cfg(feature = "prediction")]
struct LastConfirmedInputPlugin;
#[cfg(feature = "prediction")]
impl Plugin for LastConfirmedInputPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<LastConfirmedInput>();
app.add_systems(
PostUpdate,
finalize_last_confirmed_input.after(InputSystems::UpdateRemoteInputTicks),
);
}
}
#[cfg(feature = "prediction")]
fn finalize_last_confirmed_input(mut last_confirmed_input: ResMut<LastConfirmedInput>) {
last_confirmed_input.finalize_frame();
}
#[derive(Clone, Copy, Debug)]
enum InputRoute<'a> {
ClientServer { link: Entity, host_client: bool },
P2P(&'a [Entity]),
}
impl<'a> InputRoute<'a> {
#[inline]
fn from_topology(topology: &'a NetworkTopology) -> Option<Self> {
match topology {
NetworkTopology::Client(link) => Some(Self::ClientServer {
link: *link,
host_client: false,
}),
NetworkTopology::HostClient { client, .. } => Some(Self::ClientServer {
link: *client,
host_client: true,
}),
NetworkTopology::P2P { connected, .. } if !connected.is_empty() => {
Some(Self::P2P(connected.as_slice()))
}
NetworkTopology::Undefined
| NetworkTopology::Server(_)
| NetworkTopology::P2P { .. }
| NetworkTopology::Invalid(_) => None,
}
}
#[inline]
fn is_host_client(self) -> bool {
matches!(
self,
Self::ClientServer {
host_client: true,
..
}
)
}
#[inline]
fn requires_prespawned_targets(self) -> bool {
matches!(self, Self::P2P(_))
}
#[inline]
fn accepts_local_target(self, controlled_by: Option<&ControlledBy>) -> bool {
match self {
Self::ClientServer { link, .. } => {
!controlled_by.is_some_and(|controlled_by| controlled_by.owner != link)
}
Self::P2P(_) => true,
}
}
}
fn input_target(
route: InputRoute<'_>,
entity: Entity,
pre_spawned: Option<&PreSpawned>,
) -> InputTarget {
if let Some(hash) = pre_spawned.and_then(|pre_spawned| pre_spawned.hash) {
debug!(?hash, ?entity, "Sending input for prespawned entity");
return InputTarget::PreSpawned(hash);
}
assert!(
!route.requires_prespawned_targets(),
"P2P input target {entity:?} must have a PreSpawned component with a resolved hash"
);
InputTarget::Entity(entity)
}
#[inline]
fn matches_prespawned_target(
hash: u64,
p2p_link: Option<Entity>,
pre_spawned: &PreSpawned,
) -> bool {
pre_spawned.hash.is_some_and(|candidate| candidate == hash)
&& p2p_link.is_none_or(|link| pre_spawned.receiver == Some(link))
}
fn buffer_action_state<S: ActionStateSequence>(
#[cfg(feature = "metrics")] metric_handles: Res<InputMetricHandles<S>>,
timeline: SyncedLocalTimeline,
metadata: Res<NetworkingMetadata>,
rollback: Option<Res<Rollback>>,
mut action_state_query: Query<
(
Entity,
StateRef<S>,
&mut InputBuffer<S::Snapshot, S::Action>,
Option<&ControlledBy>,
),
(With<S::Marker>, Allow<PredictionDisable>),
>,
) {
let Some(route) = InputRoute::from_topology(&metadata.mode) else {
return;
};
if rollback.is_some() {
return;
}
let current_tick = timeline.current_tick();
let tick = current_tick + timeline.input_delay() as i32;
for (entity, action_state, mut input_buffer, controlled_by) in action_state_query.iter_mut() {
if !route.accepts_local_target(controlled_by) {
continue;
}
input_buffer.set(tick, S::to_snapshot(action_state));
trace!(
?entity,
?current_tick,
delayed_tick = ?tick,
input_buffer = %input_buffer.as_ref(),
"set action state in input buffer",
);
trace!(
target: "lightyear_debug::input",
kind = "buffer_action_state",
schedule = "FixedPreUpdate",
sample_point = "FixedPreUpdate",
entity = ?entity,
action = ?DebugName::type_name::<S::Action>(),
local_tick = current_tick.0,
input_tick = tick.0,
buffer_len = input_buffer.len(),
input_buffer = %input_buffer.as_ref(),
"buffered local action state"
);
#[cfg(feature = "metrics")]
metric_handles
.buffer_size(entity)
.set(input_buffer.len() as f64);
}
}
fn get_action_state<S: ActionStateSequence>(
tick_duration: Res<TickDuration>,
config: Res<InputConfig<S::Action>>,
local_timeline: Res<LocalTimeline>,
input_timeline: Res<LocalTimelineSync>,
input_timeline_config: Res<InputTimelineConfig>,
metadata: Res<NetworkingMetadata>,
rollback: Option<Res<Rollback>>,
mut action_state_query: Query<
(
Entity,
StateMut<S>,
&mut InputBuffer<S::Snapshot, S::Action>,
Has<S::Marker>,
),
Allow<PredictionDisable>,
>,
) {
let Some(route) = InputRoute::from_topology(&metadata.mode) else {
return;
};
if route.is_host_client() {
return;
}
let is_rollback = rollback.is_some();
let input_delay = input_timeline.input_delay() as i32;
let tick = local_timeline.tick();
if is_rollback && config.ignore_rollbacks {
return;
}
for (entity, action_state, mut input_buffer, is_local) in action_state_query.iter_mut() {
if !is_rollback && is_local && input_delay == 0 {
continue;
}
if let Some(snapshot) = input_buffer.get(tick) {
S::from_snapshot(S::State::into_inner(action_state), snapshot);
trace!(
?entity,
?tick,
?is_local,
?snapshot,
"fetched action state from input buffer: {:?}",
input_buffer
);
trace!(
target: "lightyear_debug::input",
kind = "get_action_state",
schedule = "FixedPreUpdate",
sample_point = "FixedPreUpdate",
entity = ?entity,
action = ?DebugName::type_name::<S::Action>(),
local_tick = tick.0,
input_tick = tick.0,
is_local,
is_rollback,
snapshot = ?snapshot,
buffer_len = input_buffer.len(),
input_buffer = %*input_buffer,
"restored action state from input buffer"
);
} else if !is_local && (config.rebroadcast_inputs || matches!(route, InputRoute::P2P(_))) {
if input_timeline_config.is_lockstep() {
error!("We are in lockstep mode but didn't receive an input for tick {tick:?}!");
}
let mut snapshot = S::to_snapshot(S::State::as_read_only(&action_state));
snapshot.decay_tick(tick_duration.0);
trace!(
?entity,
?tick,
"Action = {}, For remote input; no input for tick so we decay the ActionState to: {:?}",
DebugName::type_name::<S::Action>(),
snapshot
);
trace!(
target: "lightyear_debug::input",
kind = "decay_missing_remote_action_state",
schedule = "FixedPreUpdate",
sample_point = "FixedPreUpdate",
entity = ?entity,
action = ?DebugName::type_name::<S::Action>(),
local_tick = tick.0,
input_tick = tick.0,
is_rollback,
snapshot = ?snapshot,
buffer_len = input_buffer.len(),
"decayed missing remote action state"
);
S::from_snapshot(S::State::into_inner(action_state), &snapshot);
input_buffer.set(tick, snapshot);
}
}
}
fn get_delayed_action_state<S: ActionStateSequence>(
timeline: SyncedLocalTimeline,
metadata: Res<NetworkingMetadata>,
rollback: Option<Res<Rollback>>,
mut action_state_query: Query<
(
Entity,
StateMut<S>,
&InputBuffer<S::Snapshot, S::Action>,
Option<&ControlledBy>,
),
(With<S::Marker>, Allow<PredictionDisable>),
>,
) {
let Some(route) = InputRoute::from_topology(&metadata.mode) else {
return;
};
let is_rollback = rollback.is_some();
let input_delay_ticks = timeline.input_delay() as i32;
if is_rollback || input_delay_ticks == 0 {
return;
}
let tick = timeline.tick();
let delayed_tick = tick + input_delay_ticks;
for (entity, action_state, input_buffer, controlled_by) in action_state_query.iter_mut() {
if !route.accepts_local_target(controlled_by) {
continue;
}
if let Some(delayed_action_state) = input_buffer.get(delayed_tick) {
S::from_snapshot(S::State::into_inner(action_state), delayed_action_state);
trace!(
?entity,
?delayed_tick,
"fetched delayed action state from input buffer: {}",
input_buffer
);
trace!(
target: "lightyear_debug::input",
kind = "get_delayed_action_state",
schedule = "RunFixedMainLoop",
sample_point = "RunFixedMainLoop",
entity = ?entity,
action = ?DebugName::type_name::<S::Action>(),
local_tick = tick.0,
input_tick = delayed_tick.0,
snapshot = ?delayed_action_state,
buffer_len = input_buffer.len(),
input_buffer = %input_buffer,
"restored delayed action state"
);
}
}
}
fn clean_buffers<S: ActionStateSequence>(
timeline: Res<LocalTimeline>,
metadata: Res<NetworkingMetadata>,
prediction_manager: Option<Res<PredictionManager>>,
input_config: Res<InputTimelineConfig>,
mut input_buffer_query: Query<
&mut InputBuffer<S::Snapshot, S::Action>,
Allow<PredictionDisable>,
>,
) {
let Some(route) = InputRoute::from_topology(&metadata.mode) else {
return;
};
if route.is_host_client() {
return;
}
let old_tick = timeline.tick()
- input_history_depth(
prediction_manager
.as_deref()
.map(|manager| (manager, input_config.as_ref())),
);
for mut input_buffer in input_buffer_query.iter_mut() {
input_buffer.pop(old_tick);
}
}
fn input_history_depth(
prediction_manager: Option<(&PredictionManager, &InputTimelineConfig)>,
) -> u32 {
prediction_manager
.map(|(manager, input_config)| {
u32::from(
manager
.rollback_policy
.effective_max_rollback_ticks(input_config),
) + 1
})
.unwrap_or(0)
.max(HISTORY_DEPTH)
}
#[derive(Debug, Resource)]
pub(crate) struct MessageBuffer<S>(Vec<InputMessage<S>>);
impl<A> Default for MessageBuffer<A> {
fn default() -> Self {
Self(vec![])
}
}
fn prepare_input_message<S: ActionStateSequence>(
mut message_buffer: ResMut<MessageBuffer<S>>,
tick_duration: Res<TickDuration>,
input_config: Res<InputConfig<S::Action>>,
timeline: SyncedLocalTimeline,
metadata: Res<NetworkingMetadata>,
rollback: Option<Res<Rollback>>,
_channel_registry: Res<ChannelRegistry>,
input_buffer_query: Query<
(
Entity,
&InputBuffer<S::Snapshot, S::Action>,
Option<&PreSpawned>,
Option<&ControlledBy>,
),
(With<S::Marker>, Allow<PredictionDisable>),
>,
real_time: Res<Time<Real>>,
mut send_timer: Local<Option<Timer>>,
) {
let Some(route) = InputRoute::from_topology(&metadata.mode) else {
return;
};
if rollback.is_some() {
return;
}
let is_host_client = route.is_host_client();
if !input_config.send_interval.is_zero() {
let timer = send_timer
.get_or_insert_with(|| Timer::new(input_config.send_interval, TimerMode::Repeating));
timer.tick(real_time.delta());
if !timer.is_finished() {
return;
}
}
#[cfg(not(feature = "prediction"))]
if is_host_client {
return;
}
#[cfg(feature = "prediction")]
if is_host_client && !input_config.rebroadcast_inputs {
return;
}
let current_tick = timeline.current_tick();
let tick = current_tick + timeline.input_delay() as i32;
trace!(delayed_tick = ?tick, ?current_tick, "prepare_input_message");
trace!(
target: "lightyear_debug::input",
kind = "prepare_input_message_start",
schedule = "PostUpdate",
sample_point = "PostUpdate",
action = ?DebugName::type_name::<S::Action>(),
local_tick = current_tick.0,
input_tick = tick.0,
is_host_client,
"preparing input message"
);
let mut num_ticks: u32 = ((input_config.send_interval.as_nanos() / tick_duration.as_nanos())
+ 1)
.try_into()
.unwrap();
num_ticks *= input_config.packet_redundancy as u32;
let mut message = InputMessage::<S>::new(tick);
for (entity, input_buffer, pre_spawned, controlled_by) in input_buffer_query.iter() {
if !route.accepts_local_target(controlled_by) {
continue;
}
trace!(
?tick,
?entity,
"Preparing input message with buffer: {:?}",
input_buffer
);
let target = input_target(route, entity, pre_spawned);
if let Some(state_sequence) = S::build_from_input_buffer(input_buffer, num_ticks, tick) {
trace!(
target: "lightyear_debug::input",
kind = "prepare_input_message_target",
schedule = "PostUpdate",
sample_point = "PostUpdate",
entity = ?entity,
action = ?DebugName::type_name::<S::Action>(),
local_tick = current_tick.0,
input_tick = tick.0,
num_ticks = num_ticks,
buffer_len = input_buffer.len(),
target = ?target,
states = ?state_sequence,
"added target data to input message"
);
message.inputs.push(PerTargetData {
target,
states: state_sequence,
});
}
}
debug!(
?tick,
?num_ticks,
?is_host_client,
"sending input message for {:?}: {}",
DebugName::type_name::<S::Action>().shortname(),
message
);
trace!(
target: "lightyear_debug::input",
kind = "prepare_input_message_finish",
schedule = "PostUpdate",
sample_point = "PostUpdate",
action = ?DebugName::type_name::<S::Action>(),
local_tick = current_tick.0,
input_tick = tick.0,
end_tick = tick.0,
num_ticks = num_ticks,
num_targets = message.inputs.len(),
is_host_client,
message = ?message,
"prepared input message"
);
message_buffer.0.push(message);
}
#[cfg(feature = "prediction")]
fn receive_remote_player_input_messages<S: ActionStateSequence>(
mut commands: Commands,
tick_duration: Res<TickDuration>,
timeline: SyncedLocalTimeline,
input_config: Res<InputConfig<S::Action>>,
#[cfg(feature = "metrics")] mut input_metric_handles: ResMut<InputMetricHandles<S>>,
metadata: Res<NetworkingMetadata>,
last_confirmed_input: Res<LastConfirmedInput>,
prediction_manager: Option<Res<PredictionManager>>,
mut receivers: Query<&mut MessageReceiver<InputMessage<S>>>,
mut predicted_query: Query<
Option<&mut InputBuffer<S::Snapshot, S::Action>>,
(Without<S::Marker>, Allow<PredictionDisable>),
>,
prespawned: Query<(Entity, &PreSpawned)>,
) {
let Some(route) = InputRoute::from_topology(&metadata.mode) else {
return;
};
if route.is_host_client() {
return;
}
if matches!(route, InputRoute::ClientServer { .. }) && !input_config.rebroadcast_inputs {
return;
}
let Some(prediction_manager) = prediction_manager else {
return;
};
let tick = timeline.tick();
let mut received_relevant_input = false;
match route {
InputRoute::ClientServer { link, .. } => {
if let Ok(mut receiver) = receivers.get_mut(link) {
received_relevant_input |= receive_remote_player_input_messages_from_receiver::<S>(
&mut receiver,
&mut commands,
*tick_duration,
tick,
None,
&prediction_manager,
&mut predicted_query,
&prespawned,
#[cfg(feature = "metrics")]
&mut input_metric_handles,
);
}
}
InputRoute::P2P(links) => {
for link in links {
let Ok(mut receiver) = receivers.get_mut(*link) else {
continue;
};
received_relevant_input |= receive_remote_player_input_messages_from_receiver::<S>(
&mut receiver,
&mut commands,
*tick_duration,
tick,
Some(*link),
&prediction_manager,
&mut predicted_query,
&prespawned,
#[cfg(feature = "metrics")]
&mut input_metric_handles,
);
}
}
}
if received_relevant_input {
last_confirmed_input
.received_any_messages
.store(true, bevy_platform::sync::atomic::Ordering::Relaxed);
}
}
#[cfg(feature = "prediction")]
fn receive_remote_player_input_messages_from_receiver<S: ActionStateSequence>(
receiver: &mut MessageReceiver<InputMessage<S>>,
commands: &mut Commands,
tick_duration: TickDuration,
tick: Tick,
p2p_link: Option<Entity>,
prediction_manager: &PredictionManager,
predicted_query: &mut Query<
Option<&mut InputBuffer<S::Snapshot, S::Action>>,
(Without<S::Marker>, Allow<PredictionDisable>),
>,
prespawned: &Query<(Entity, &PreSpawned)>,
#[cfg(feature = "metrics")] input_metric_handles: &mut InputMetricHandles<S>,
) -> bool {
let mut received_relevant_input = false;
receiver.receive().for_each(|message| {
trace!(?message.end_tick, ?message, "received remote input message for action: {:?}", DebugName::type_name::<S::Action>());
trace!(
target: "lightyear_debug::input",
kind = "remote_input_message_recv",
schedule = "PreUpdate",
sample_point = "PreUpdate",
action = ?DebugName::type_name::<S::Action>(),
local_tick = tick.0,
end_tick = message.end_tick.0,
num_targets = message.inputs.len(),
rebroadcast = message.rebroadcast,
message = ?message,
"received remote player input message"
);
for target_data in message.inputs {
let Some(entity) = (match target_data.target {
InputTarget::Entity(entity) if p2p_link.is_none() => Some(entity),
InputTarget::Entity(entity) => {
error!(
?entity,
end_tick = ?message.end_tick,
"ignored P2P input target without a PreSpawned hash"
);
None
}
InputTarget::PreSpawned(hash) => {
prespawned.iter().find_map(|(entity, pre_spawned)| {
matches_prespawned_target(hash, p2p_link, pre_spawned).then_some(entity)
})
}
}) else {
if message.rebroadcast {
debug!(
target = ?target_data.target,
end_tick = ?message.end_tick,
"ignored stale remote player input message for unmapped entity"
);
} else if let Some(link) = p2p_link {
warn!(
?link,
target = ?target_data.target,
"could not find a PreSpawned P2P input target owned by the sending Link"
);
} else {
warn!("Could not find entity in entity_map for remote player input message {:?}", target_data.target);
}
continue;
};
debug!(
?tick, ?message.end_tick,
"received remote client input message for entity: {:?}. Applying to diff buffer.",
entity
);
trace!(
target: "lightyear_debug::input",
kind = "remote_input_target",
schedule = "PreUpdate",
sample_point = "PreUpdate",
entity = ?entity,
action = ?DebugName::type_name::<S::Action>(),
local_tick = tick.0,
end_tick = message.end_tick.0,
target = ?target_data.target,
states = ?target_data.states,
"applying remote input target data"
);
let Ok(input_buffer) = predicted_query.get_mut(entity) else {
if message.rebroadcast {
debug!(
?entity,
?target_data.states,
end_tick = ?message.end_tick,
"ignored stale remote player input message for unrecognized entity"
);
} else {
error!(?entity, ?target_data.states, end_tick = ?message.end_tick, "received input message for unrecognized entity");
}
continue
};
trace!(predicted=?entity, end_tick = ?message.end_tick, "update action diff buffer for remote player PREDICTED using input message");
#[cfg(feature = "metrics")]
if input_buffer.is_none() {
input_metric_handles.insert_entity(entity);
}
if let Some(mut input_buffer) = input_buffer {
if input_buffer.last_remote_tick.is_some_and(|t| t >= message.end_tick) {
trace!("Ignoring input message because our current last_remote_tick {:?} is more recent than the remote_end_tick {:?}", input_buffer.last_remote_tick, message.end_tick);
trace!(
target: "lightyear_debug::input",
kind = "remote_input_ignored_stale",
schedule = "PreUpdate",
sample_point = "PreUpdate",
entity = ?entity,
action = ?DebugName::type_name::<S::Action>(),
local_tick = tick.0,
end_tick = message.end_tick.0,
last_remote_tick = ?input_buffer.last_remote_tick,
"ignored stale remote input message"
);
continue
}
received_relevant_input = true;
update_buffer_from_remote_player_message::<S>(
target_data.states,
&mut input_buffer,
tick,
message.end_tick,
entity,
prediction_manager,
tick_duration,
#[cfg(feature = "metrics")]
&*input_metric_handles,
);
} else {
let mut input_buffer = InputBuffer::<S::Snapshot, S::Action>::default();
received_relevant_input = true;
update_buffer_from_remote_player_message::<S>(
target_data.states,
&mut input_buffer,
tick,
message.end_tick,
entity,
prediction_manager,
tick_duration,
#[cfg(feature = "metrics")]
&*input_metric_handles,
);
let mut action_state = S::State::base_value();
if let Some(last) = input_buffer.get_last() {
S::from_snapshot(S::State::as_mut(&mut action_state), last);
}
commands.entity(entity).insert((input_buffer, action_state));
};
}
});
received_relevant_input
}
#[cfg(feature = "prediction")]
fn update_last_confirmed_input<S: ActionStateSequence>(
timeline: SyncedLocalTimeline,
action_config: Res<InputConfig<S::Action>>,
input_config: Res<InputTimelineConfig>,
metadata: Res<NetworkingMetadata>,
mut last_confirmed_input: ResMut<LastConfirmedInput>,
predicted_query: Query<
&InputBuffer<S::Snapshot, S::Action>,
(Without<S::Marker>, Allow<PredictionDisable>),
>,
) {
let Some(route) = InputRoute::from_topology(&metadata.mode) else {
return;
};
if matches!(route, InputRoute::ClientServer { .. }) && !action_config.rebroadcast_inputs {
return;
}
let tick = timeline.tick();
if input_config.is_lockstep() && !matches!(metadata.mode, NetworkTopology::P2P { .. }) {
last_confirmed_input.tick.set_if_lower(tick);
return;
}
let mut received_for_all_clients = true;
predicted_query.iter().for_each(|buffer| {
if let Some(end_tick) = buffer.last_remote_tick {
last_confirmed_input.tick.set_if_lower(end_tick);
} else {
received_for_all_clients = false;
}
});
last_confirmed_input.received_for_all_clients &= received_for_all_clients;
trace!(
target: "lightyear_debug::input",
kind = "last_confirmed_input",
schedule = "PostUpdate",
sample_point = "PostUpdate",
action = ?DebugName::type_name::<S::Action>(),
local_tick = tick.0,
confirmed_tick = last_confirmed_input.tick.get().0,
"updated LastConfirmedInput"
);
}
#[cfg(feature = "prediction")]
fn update_buffer_from_remote_player_message<S: ActionStateSequence>(
sequence: S,
input_buffer: &mut InputBuffer<S::Snapshot, S::Action>,
tick: Tick,
end_tick: Tick,
entity: Entity,
prediction_manager: &PredictionManager,
tick_duration: TickDuration,
#[cfg(feature = "metrics")] input_metric_handles: &InputMetricHandles<S>,
) {
if let Some(mismatch) = sequence.update_buffer(input_buffer, end_tick, tick_duration.0) {
trace!(
target: "lightyear_debug::input",
kind = "remote_input_buffer_update",
schedule = "PreUpdate",
sample_point = "PreUpdate",
entity = ?entity,
action = ?DebugName::type_name::<S::Action>(),
local_tick = tick.0,
end_tick = end_tick.0,
mismatch_tick = mismatch.0,
buffer_len = input_buffer.len(),
last_remote_tick = ?input_buffer.last_remote_tick,
input_buffer = %input_buffer,
"updated remote input buffer with mismatch"
);
if let RollbackMode::Check = prediction_manager.rollback_policy.input
&& mismatch <= tick
{
debug!(
?entity,
?tick,
?end_tick,
?mismatch,
"Mismatch detected for remote player input message!",
);
prediction_manager
.earliest_mismatch_input
.has_mismatches
.store(true, bevy_platform::sync::atomic::Ordering::Relaxed);
prediction_manager
.earliest_mismatch_input
.tick
.set_if_lower(mismatch);
}
#[cfg(feature = "metrics")]
{
input_metric_handles.remote_player_receive().increment(1);
let margin = input_buffer.last_remote_tick.unwrap() - tick;
input_metric_handles
.remote_player_buffer_margin(entity)
.set(margin as f64);
input_metric_handles
.remote_player_buffer_size(entity)
.set(input_buffer.len() as f64);
}
};
trace!(
target: "lightyear_debug::input",
kind = "remote_input_buffer_state",
schedule = "PreUpdate",
sample_point = "PreUpdate",
entity = ?entity,
action = ?DebugName::type_name::<S::Action>(),
local_tick = tick.0,
end_tick = end_tick.0,
buffer_len = input_buffer.len(),
last_remote_tick = ?input_buffer.last_remote_tick,
input_buffer = %input_buffer,
"remote input buffer state after message"
);
}
#[cfg_attr(
not(feature = "interpolation"),
expect(
unused_mut,
reason = "InputMessage is mutated only when interpolation is enabled"
)
)]
fn send_input_messages<S: ActionStateSequence>(
input_config: Res<InputConfig<S::Action>>,
_local_timeline: SyncedLocalTimeline,
#[cfg(feature = "interpolation")] fixed_time: Res<Time<Fixed>>,
metadata: Res<NetworkingMetadata>,
mut message_buffer: ResMut<MessageBuffer<S>>,
mut senders: Query<&mut MessageSender<InputMessage<S>>>,
mut multi_sender: MultiMessageSender,
#[cfg(feature = "interpolation")] interpolation_timeline: Res<InterpolationTimeline>,
) {
let Some(route) = InputRoute::from_topology(&metadata.mode) else {
return;
};
let is_host_client = route.is_host_client();
#[cfg(not(feature = "prediction"))]
if is_host_client {
message_buffer.0.clear();
return;
}
#[cfg(feature = "prediction")]
if is_host_client && !input_config.rebroadcast_inputs {
message_buffer.0.clear();
return;
}
trace!(
"Number of input messages to send: {:?}",
message_buffer.0.len()
);
trace!(
target: "lightyear_debug::input",
kind = "send_input_messages",
schedule = "PostUpdate",
sample_point = "PostUpdate",
action = ?DebugName::type_name::<S::Action>(),
num_messages = message_buffer.0.len(),
is_host_client,
"sending buffered input messages"
);
#[cfg(feature = "interpolation")]
let interpolation_delay = match route {
InputRoute::ClientServer { .. } => {
if !interpolation_timeline.is_synced() {
return;
}
let mut delay = _local_timeline.instant(&fixed_time) - interpolation_timeline.now();
if delay.is_negative() {
delay = TickDelta::from(Tick(0));
}
Some(InterpolationDelay {
delay: delay.into(),
})
}
InputRoute::P2P(_) => None,
};
match route {
InputRoute::ClientServer { link, .. } => {
let Ok(mut sender) = senders.get_mut(link) else {
return;
};
for mut message in message_buffer.0.drain(..) {
#[cfg(feature = "interpolation")]
if input_config.lag_compensation {
message.interpolation_delay = interpolation_delay;
}
sender.send::<InputChannel>(message);
}
}
InputRoute::P2P(links) => {
let Some(links) = unique_p2p_links(links) else {
error!("cached P2P Link entities must be unique");
message_buffer.0.clear();
return;
};
for mut message in message_buffer.0.drain(..) {
#[cfg(feature = "interpolation")]
if input_config.lag_compensation {
message.interpolation_delay = interpolation_delay;
}
if let Err(error) =
multi_sender.send::<InputMessage<S>, InputChannel>(&message, links)
{
error!(%error, "failed to send input message to P2P Links");
}
}
}
}
}
fn unique_p2p_links(links: &[Entity]) -> Option<&UniqueEntitySlice> {
let has_duplicates = links
.iter()
.enumerate()
.any(|(index, link)| links[index + 1..].contains(link));
if has_duplicates {
return None;
}
Some(unsafe { UniqueEntitySlice::from_slice_unchecked(links) })
}
fn receive_local_timeline_shift<S: ActionStateSequence>(
trigger: On<LocalTimelineShift>,
mut message_buffer: ResMut<MessageBuffer<S>>,
mut input_buffer_query: Query<
&mut InputBuffer<S::Snapshot, S::Action>,
Allow<PredictionDisable>,
>,
) {
let delta = trigger.delta;
for mut input_buffer in input_buffer_query.iter_mut() {
let had_start_tick = input_buffer.start_tick.is_some();
shift_input_buffer_ticks(&mut input_buffer, delta);
if had_start_tick {
debug!(
"Receive local timeline shift {:?}. Updating input buffer start_tick to {:?}!",
trigger.event(),
input_buffer.start_tick
);
}
}
for message in message_buffer.0.iter_mut() {
message.end_tick = message.end_tick + delta;
}
}
fn shift_input_buffer_ticks<S, A>(input_buffer: &mut InputBuffer<S, A>, delta: i32) {
if let Some(start_tick) = input_buffer.start_tick {
input_buffer.start_tick = Some(start_tick + delta);
}
if let Some(last_remote_tick) = input_buffer.last_remote_tick {
input_buffer.last_remote_tick = Some(last_remote_tick + delta);
}
}
#[cfg(test)]
mod tests {
use super::*;
use lightyear_replication::prelude::Lifetime;
#[test]
fn input_route_uses_link_ownership_only_for_client_server() {
let mut world = World::new();
let client = world.spawn_empty().id();
let other = world.spawn_empty().id();
let owned_by_client = ControlledBy {
owner: client,
lifetime: Lifetime::SessionBased,
};
let owned_by_other = ControlledBy {
owner: other,
lifetime: Lifetime::SessionBased,
};
let client_server = NetworkTopology::Client(client);
let client_server_route = InputRoute::from_topology(&client_server).unwrap();
assert!(client_server_route.accepts_local_target(None));
assert!(client_server_route.accepts_local_target(Some(&owned_by_client)));
assert!(!client_server_route.accepts_local_target(Some(&owned_by_other)));
let p2p = NetworkTopology::P2P {
connected: [client, other].into_iter().collect(),
declared_links: 2,
};
let p2p_route = InputRoute::from_topology(&p2p).unwrap();
assert!(p2p_route.accepts_local_target(None));
assert!(p2p_route.accepts_local_target(Some(&owned_by_client)));
assert!(p2p_route.accepts_local_target(Some(&owned_by_other)));
}
#[test]
fn p2p_input_targets_use_prespawned_hashes() {
let mut world = World::new();
let link = world.spawn_empty().id();
let target = world.spawn_empty().id();
let topology = NetworkTopology::P2P {
connected: [link].into_iter().collect(),
declared_links: 1,
};
let route = InputRoute::from_topology(&topology).unwrap();
let prespawned = PreSpawned::new(0xCAFE);
assert_eq!(
input_target(route, target, Some(&prespawned)),
InputTarget::PreSpawned(0xCAFE)
);
}
#[test]
#[should_panic(expected = "must have a PreSpawned component with a resolved hash")]
fn p2p_input_targets_reject_unmapped_entities() {
let mut world = World::new();
let link = world.spawn_empty().id();
let target = world.spawn_empty().id();
let topology = NetworkTopology::P2P {
connected: [link].into_iter().collect(),
declared_links: 1,
};
let route = InputRoute::from_topology(&topology).unwrap();
let _ = input_target(route, target, None);
}
#[test]
fn client_server_input_targets_still_allow_entities() {
let mut world = World::new();
let link = world.spawn_empty().id();
let target = world.spawn_empty().id();
let topology = NetworkTopology::Client(link);
let route = InputRoute::from_topology(&topology).unwrap();
assert_eq!(
input_target(route, target, None),
InputTarget::Entity(target)
);
}
#[test]
fn p2p_input_targets_are_scoped_to_the_sending_link() {
let mut world = World::new();
let owner = world.spawn_empty().id();
let other = world.spawn_empty().id();
let pre_spawned = PreSpawned::new(0xCAFE).for_receiver(owner);
assert!(matches_prespawned_target(0xCAFE, Some(owner), &pre_spawned));
assert!(!matches_prespawned_target(
0xCAFE,
Some(other),
&pre_spawned
));
assert!(matches_prespawned_target(0xCAFE, None, &pre_spawned));
}
#[test]
fn input_history_depth_covers_prediction_rollback_window() {
assert_eq!(input_history_depth(None), HISTORY_DEPTH);
let input_config = InputTimelineConfig::default();
let mut manager = PredictionManager {
rollback_policy: RollbackPolicy {
max_rollback_ticks: 100,
..Default::default()
},
..Default::default()
};
assert_eq!(input_history_depth(Some((&manager, &input_config))), 101);
manager.rollback_policy.max_rollback_ticks = 5;
assert_eq!(
input_history_depth(Some((&manager, &input_config))),
HISTORY_DEPTH
);
}
}