Skip to content

Commit 9d7f7d9

Browse files
fix(inputs): simplify BEI input replication (#1537)
1 parent 522ec6d commit 9d7f7d9

21 files changed

Lines changed: 636 additions & 719 deletions

File tree

crates/inputs/input_bei/src/marker.rs

Lines changed: 31 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
//! Add an [`InputMarker<C>`] component automatically to [`Action`] entities that need it
22
3-
use crate::setup::NetworkActionOf;
43
use bevy_ecs::prelude::*;
54
use bevy_ecs::relationship::Relationship;
65
use bevy_enhanced_input::prelude::*;
76
use bevy_replicon::client::confirm_history::ConfirmHistory;
87
use lightyear_connection::client::Client;
9-
use lightyear_replication::prelude::{Controlled, ControlledBy, HasAuthority};
8+
use lightyear_replication::prelude::{Controlled, ControlledBy};
109

1110
/// Marker component that indicates that the entity is actively listening for physical user inputs.
1211
///
@@ -40,132 +39,83 @@ fn action_targets_local_client<C: Component>(
4039

4140
/// Propagate the InputMarker component from the Context entity to the Action entities
4241
/// whenever an InputMarker is added to a Context entity.
43-
/// Skip replicated action entities (those received from remote clients).
42+
///
43+
/// `InputMarker<C>` on the context is the explicit local-input signal, so
44+
/// confirmed prespawned owner actions should still receive it. Remote
45+
/// rebroadcasted actions should be attached to contexts without this marker.
4446
pub(crate) fn propagate_input_marker<C: Component>(
4547
trigger: On<Add, InputMarker<C>>,
4648
actions: Query<&Actions<C>>,
47-
confirm: Query<(), With<ConfirmHistory>>,
4849
mut commands: Commands,
4950
) {
5051
if let Ok(actions) = actions.get(trigger.entity) {
5152
actions.iter().for_each(|action| {
52-
if confirm.get(action).is_ok() {
53-
return;
54-
}
5553
commands.entity(action).insert(InputMarker::<C>::default());
5654
});
5755
}
5856
}
5957

6058
/// When an Action entity is added to a Context entity that has an InputMarker,
6159
/// add the InputMarker to the Action entity as well.
62-
/// Skip replicated entities — those are received from remote clients and should not
63-
/// be marked as local input sources.
6460
pub(crate) fn add_input_marker_from_parent<C: Component>(
6561
trigger: On<Add, ActionOf<C>>,
66-
action_of: Query<&ActionOf<C>, Without<ConfirmHistory>>,
62+
action_of: Query<&ActionOf<C>>,
6763
context: Query<(), With<InputMarker<C>>>,
6864
mut commands: Commands,
6965
) {
70-
if let Ok(action_of) = action_of.get(trigger.entity)
71-
&& context.get(action_of.get()).is_ok()
72-
{
66+
let Ok(action_of) = action_of.get(trigger.entity) else {
67+
return;
68+
};
69+
if context.get(action_of.get()).is_ok() {
7370
commands
7471
.entity(trigger.entity)
7572
.insert(InputMarker::<C>::default());
7673
}
7774
}
7875

79-
/// If Bindings or ActionMock is added to an Action entity, add the InputMarker to that Action entity.
80-
/// Only add the marker on locally controlled action entities that already have a network-facing
81-
/// action mapping. This avoids emitting inputs for entities that the server cannot resolve yet.
76+
/// If Bindings or ActionMock is added to an Action entity, add the InputMarker
77+
/// to that Action entity once the action has a network-resolvable identity.
78+
///
79+
/// Replicated/prespawned actions become resolvable when [`ConfirmHistory`] is
80+
/// present, because the client's action entity can then be mapped back to the
81+
/// server action entity when an input message is sent.
8282
pub(crate) fn add_input_marker_from_binding<C: Component>(
8383
trigger: On<Add, (Bindings, ActionMock)>,
84-
action: Query<
85-
&ActionOf<C>,
86-
(
87-
With<ActionOf<C>>,
88-
With<NetworkActionOf<C>>,
89-
Without<ConfirmHistory>,
90-
),
91-
>,
84+
action: Query<&ActionOf<C>, (With<ConfirmHistory>, Without<InputMarker<C>>)>,
9285
contexts: Query<Option<&ControlledBy>, With<Controlled>>,
9386
clients: Query<(), With<Client>>,
9487
mut commands: Commands,
9588
) {
96-
if let Ok(action_of) = action.get(trigger.entity)
97-
&& action_targets_local_client(action_of, &contexts, &clients)
98-
{
89+
let Ok(action_of) = action.get(trigger.entity) else {
90+
return;
91+
};
92+
if action_targets_local_client(action_of, &contexts, &clients) {
9993
commands
10094
.entity(trigger.entity)
10195
.insert(InputMarker::<C>::default());
10296
}
10397
}
10498

105-
/// If authority is granted after the action entity already exists, add the InputMarker
106-
/// once the entity becomes locally controlled.
107-
pub(crate) fn add_input_marker_from_authority<C: Component>(
108-
trigger: On<Add, HasAuthority>,
109-
action: Query<
110-
&ActionOf<C>,
111-
(
112-
With<ActionOf<C>>,
113-
With<NetworkActionOf<C>>,
114-
Or<(With<Bindings>, With<ActionMock>)>,
115-
Without<ConfirmHistory>,
116-
),
117-
>,
118-
contexts: Query<Option<&ControlledBy>, With<Controlled>>,
119-
clients: Query<(), With<Client>>,
120-
mut commands: Commands,
121-
) {
122-
if let Ok(action_of) = action.get(trigger.entity)
123-
&& action_targets_local_client(action_of, &contexts, &clients)
124-
{
125-
commands
126-
.entity(trigger.entity)
127-
.insert(InputMarker::<C>::default());
128-
}
129-
}
130-
131-
/// If the network-facing action mapping becomes available after the entity already has bindings,
132-
/// start treating it as a local input source at that point.
133-
pub(crate) fn add_input_marker_from_network_action<C: Component>(
134-
trigger: On<Add, NetworkActionOf<C>>,
99+
/// If an existing bound action becomes network-resolvable, add the InputMarker
100+
/// once it targets the local client.
101+
pub(crate) fn add_input_marker_when_action_becomes_ready<C: Component>(
102+
trigger: On<Add, ConfirmHistory>,
135103
action: Query<
136104
&ActionOf<C>,
137105
(
138-
With<ActionOf<C>>,
139-
With<NetworkActionOf<C>>,
106+
With<ConfirmHistory>,
140107
Or<(With<Bindings>, With<ActionMock>)>,
141-
Without<ConfirmHistory>,
108+
Without<InputMarker<C>>,
142109
),
143110
>,
144111
contexts: Query<Option<&ControlledBy>, With<Controlled>>,
145112
clients: Query<(), With<Client>>,
146113
mut commands: Commands,
147114
) {
148-
if let Ok(action_of) = action.get(trigger.entity)
149-
&& action_targets_local_client(action_of, &contexts, &clients)
150-
{
151-
commands
152-
.entity(trigger.entity)
153-
.insert(InputMarker::<C>::default());
154-
}
155-
}
156-
157-
/// If a prespawned action entity is confirmed but still targets a locally controlled context,
158-
/// keep using it as a local input source.
159-
pub(crate) fn add_input_marker_from_confirmed_controlled_action<C: Component>(
160-
trigger: On<Add, ConfirmHistory>,
161-
action: Query<&ActionOf<C>, (With<ConfirmHistory>, Or<(With<Bindings>, With<ActionMock>)>)>,
162-
contexts: Query<Option<&ControlledBy>, With<Controlled>>,
163-
clients: Query<(), With<Client>>,
164-
mut commands: Commands,
165-
) {
166-
if let Ok(action_of) = action.get(trigger.entity)
167-
&& action_targets_local_client(action_of, &contexts, &clients)
168-
{
115+
let Ok(action_of) = action.get(trigger.entity) else {
116+
return;
117+
};
118+
if action_targets_local_client(action_of, &contexts, &clients) {
169119
commands
170120
.entity(trigger.entity)
171121
.insert(InputMarker::<C>::default());

crates/inputs/input_bei/src/plugin.rs

Lines changed: 46 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
#[cfg(any(feature = "client", feature = "server"))]
22
use crate::input_message::{BEIBuffer, BEIStateSequence};
33

4-
use crate::setup::InputRegistryPlugin;
5-
use bevy_app::{PreUpdate, prelude::*};
4+
#[cfg(feature = "client")]
5+
use crate::setup::resolve_pending_action_of;
6+
#[cfg(any(feature = "client", feature = "server"))]
7+
use crate::setup::{
8+
InputRegistryPlugin, deserialize_action_of, remove_action_of, serialize_action_of,
9+
write_action_of,
10+
};
11+
use bevy_app::prelude::*;
612
use bevy_ecs::prelude::*;
13+
#[cfg(any(feature = "client", feature = "server"))]
714
use bevy_ecs::schedule::IntoScheduleConfigs;
815
#[cfg(all(feature = "client", feature = "server"))]
916
use bevy_ecs::schedule::common_conditions::not;
@@ -12,19 +19,16 @@ use bevy_enhanced_input::EnhancedInputSystems;
1219
#[cfg(feature = "client")]
1320
use bevy_enhanced_input::action::TriggerState;
1421
use bevy_enhanced_input::context::InputContextAppExt;
15-
#[cfg(any(feature = "client", feature = "server"))]
1622
use bevy_enhanced_input::prelude::ActionOf;
1723
use bevy_reflect::TypePath;
18-
use bevy_replicon::prelude::{AppRuleExt, ReplicationMode, RuleFns};
24+
use bevy_replicon::prelude::{AppMarkerExt, AppRuleExt, ReplicationMode, RuleFns};
1925
use bevy_replicon::shared::replication::registry::receive_fns::MutWrite;
2026
use core::fmt::Debug;
2127
#[cfg(feature = "client")]
2228
use lightyear_core::prelude::is_in_rollback;
2329
#[cfg(feature = "client")]
2430
use lightyear_inputs::client::InputSystems;
2531
use lightyear_inputs::config::InputConfig;
26-
use lightyear_messages::plugin::MessageSystems;
27-
use lightyear_replication::ReplicationSystems;
2832
use serde::Serialize;
2933
use serde::de::DeserializeOwned;
3034

@@ -45,14 +49,37 @@ use serde::de::DeserializeOwned;
4549
/// # Action entities
4650
///
4751
/// BEI uses separate "action entities" with [`ActionOf<C>`] to represent
48-
/// individual actions. These entities need to exist on both client and server.
49-
/// The recommended approach is to use [`PreSpawned`] so both sides spawn them
50-
/// independently and match via a deterministic hash — this avoids the need
51-
/// for client-to-server entity replication.
52+
/// individual actions. In the server-authoritative flow, spawn those action
53+
/// entities on the server and replicate them to clients along with the context
54+
/// entity. The owning client should add local-only [`Bindings`] once its local
55+
/// controlled context has the replicated [`Action`] entity in its
56+
/// [`ActionOf<C>`]/`Actions<C>` relationship.
57+
///
58+
/// Replicating the action entity is also what lets remote clients receive
59+
/// rebroadcasted BEI input. Rebroadcasted [`BEIStateSequence`] messages target
60+
/// action entities, so a remote client needs a corresponding replicated action
61+
/// entity to resolve the target and buffer the remote player's input state.
5262
///
63+
/// The replicated [`Action`] component is structural: it recreates the typed BEI
64+
/// action entity on the receiver, but does not carry runtime input state. The
65+
/// action relationship is replicated directly through [`ActionOf<C>`].
66+
/// Lightyear uses a custom receive path for [`ActionOf<C>`] so the context
67+
/// entity is only inserted into Bevy's relationship component once Replicon's
68+
/// server-to-client entity map already contains it. If the action arrives first,
69+
/// the relationship is held as a local pending marker and resolved after the
70+
/// context mapping is available.
71+
/// Live action state is sent by [`BEIStateSequence`] input messages. The owning
72+
/// client adds [`InputMarker`] to local action entities, buffers BEI trigger
73+
/// state/value/time each tick, and sends those snapshots to the server. If input
74+
/// rebroadcasting is enabled, the server forwards those input messages to other
75+
/// clients so they can update remote action buffers for prediction.
76+
///
77+
/// [`Action`]: bevy_enhanced_input::prelude::Action
78+
/// [`Bindings`]: bevy_enhanced_input::prelude::Bindings
5379
/// [`BEIStateSequence`]: crate::input_message::BEIStateSequence
5480
/// [`ActionOf<C>`]: bevy_enhanced_input::prelude::ActionOf
55-
/// [`PreSpawned`]: lightyear_replication::prelude::PreSpawned
81+
/// [`InputMarker`]: crate::marker::InputMarker
82+
/// [`Replicate`]: lightyear_replication::prelude::Replicate
5683
pub struct InputPlugin<C> {
5784
pub config: InputConfig<C>,
5885
}
@@ -91,37 +118,16 @@ impl<
91118
app.add_input_context_to::<FixedPreUpdate, C>();
92119
// we register the context C entity so that it can be replicated from the server to the client
93120
app.replicate::<C>();
94-
95-
// We mirror ActionOf<C> into a separate component that stores the authoritative
96-
// remote entity. That avoids depending on sender-side entity mapping in replicon's
97-
// SerializeCtx.
98121
app.replicate_with((
99-
RuleFns::new(
100-
crate::setup::serialize_network_action_of::<C>,
101-
crate::setup::deserialize_network_action_of::<C>,
102-
),
103-
ReplicationMode::default(),
104-
));
105-
app.add_observer(InputRegistryPlugin::mirror_action_of_for_replication::<C>);
106-
app.add_observer(InputRegistryPlugin::insert_action_of_from_network::<C>);
107-
app.add_systems(
108-
PreUpdate,
109-
(
110-
InputRegistryPlugin::resolve_pending_network_action_of::<C>,
111-
InputRegistryPlugin::resolve_pending_action_of::<C>,
112-
)
113-
.chain()
114-
.after(ReplicationSystems::Receive)
115-
.before(MessageSystems::Receive),
116-
);
117-
122+
RuleFns::new(serialize_action_of::<C>, deserialize_action_of::<C>),
123+
ReplicationMode::Once,
124+
))
125+
.set_receive_fns::<ActionOf<C>>(write_action_of::<C>, remove_action_of::<C>);
118126
#[cfg(feature = "client")]
119127
{
120128
use crate::marker::{
121-
add_input_marker_from_authority, add_input_marker_from_binding,
122-
add_input_marker_from_confirmed_controlled_action,
123-
add_input_marker_from_network_action, add_input_marker_from_parent,
124-
propagate_input_marker,
129+
add_input_marker_from_binding, add_input_marker_from_parent,
130+
add_input_marker_when_action_becomes_ready, propagate_input_marker,
125131
};
126132
// for rebroadcasting inputs, we insert TriggerState (which inserts the InputBuffer) when ActionOf<C> is added
127133
// on an entity
@@ -130,9 +136,8 @@ impl<
130136
app.add_observer(propagate_input_marker::<C>);
131137
app.add_observer(add_input_marker_from_parent::<C>);
132138
app.add_observer(add_input_marker_from_binding::<C>);
133-
app.add_observer(add_input_marker_from_authority::<C>);
134-
app.add_observer(add_input_marker_from_network_action::<C>);
135-
app.add_observer(add_input_marker_from_confirmed_controlled_action::<C>);
139+
app.add_observer(add_input_marker_when_action_becomes_ready::<C>);
140+
app.add_systems(PreUpdate, resolve_pending_action_of::<C>);
136141

137142
if self.config.rebroadcast_inputs {
138143
app.add_observer(InputRegistryPlugin::on_rebroadcast_action_received::<C>);
@@ -147,8 +152,6 @@ impl<
147152
);
148153
}
149154

150-
app.add_observer(InputRegistryPlugin::add_action_of_replicate::<C>);
151-
152155
app.add_plugins(lightyear_inputs::client::ClientInputPlugin::<
153156
BEIStateSequence<C>,
154157
>::new(self.config));

0 commit comments

Comments
 (0)