Skip to content

Commit c3a9cdc

Browse files
mmannermclaude
andcommitted
docs(inputs): document ordering custom validators after authorize_controlled_targets
Answers "how do I run my own validation after the authorization helper": order it with `.after(authorize_controlled_targets::<S>)`. Adds the pattern to the helper rustdoc and a test (test_custom_validator_ordered_after_authorize, verified discriminating — fails when ordered `.before`). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8355587 commit c3a9cdc

2 files changed

Lines changed: 119 additions & 0 deletions

File tree

crates/inputs/inputs/src/server.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,15 @@ impl InputValidationAppExt for App {
177177
/// app.add_input_validator(authorize_controlled_targets::<MySequence>);
178178
/// ```
179179
///
180+
/// To run **your own** validation after this one — so it only sees authorized
181+
/// targets — order it with `.after(authorize_controlled_targets::<S>)`
182+
/// (validators in [`InputSystems::ValidateInputs`] are otherwise unordered):
183+
///
184+
/// ```ignore
185+
/// app.add_input_validator(authorize_controlled_targets::<MySequence>);
186+
/// app.add_input_validator(my_validator.after(authorize_controlled_targets::<MySequence>));
187+
/// ```
188+
///
180189
/// - Host-client inputs (`RemoteId::is_local`) are trusted in-process and
181190
/// skipped.
182191
/// - `InputTarget::PreSpawned` is identified by a hash, not an entity id, so it

crates/tests/src/client_server/input/leafwing.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,3 +977,113 @@ fn test_authorize_controlled_targets_passes_through_prespawned() {
977977
"authorize_controlled_targets stripped a PreSpawned target — it must pass through",
978978
);
979979
}
980+
981+
/// Documents the recommended way to run your own validator *after* the
982+
/// `authorize_controlled_targets` helper, so it only sees authorized targets:
983+
/// register it with `.after(authorize_controlled_targets::<S>)`.
984+
///
985+
/// Client 0 controls A and forges a target on uncontrolled B. The custom
986+
/// validator, ordered after the helper, records the targets it sees: it must
987+
/// see A but never the spoofed B (already stripped). Were it unordered (or
988+
/// before), it could observe B.
989+
#[test]
990+
fn test_custom_validator_ordered_after_authorize() {
991+
use bevy::ecs::resource::Resource;
992+
use bevy::ecs::schedule::IntoScheduleConfigs;
993+
use bevy::ecs::system::{Query, ResMut};
994+
use bevy::prelude::Entity;
995+
use lightyear::input::leafwing::input_message::LeafwingSequence;
996+
use lightyear_inputs::input_message::{InputMessage, InputTarget};
997+
use lightyear_inputs::prelude::server::{InputValidationAppExt, authorize_controlled_targets};
998+
use lightyear_messages::prelude::MessageReceiver;
999+
use lightyear_replication::prelude::ControlledBy;
1000+
1001+
#[derive(Resource, Default)]
1002+
struct SeenAfterAuth(Vec<Entity>);
1003+
1004+
fn record_after_auth(
1005+
mut seen: ResMut<SeenAfterAuth>,
1006+
mut receivers: Query<&mut MessageReceiver<InputMessage<LeafwingSequence<LeafwingInput1>>>>,
1007+
) {
1008+
for mut receiver in &mut receivers {
1009+
receiver.retain_messages(|msg| {
1010+
for data in &msg.inputs {
1011+
if let InputTarget::Entity(e) = data.target {
1012+
seen.0.push(e);
1013+
}
1014+
}
1015+
true
1016+
});
1017+
}
1018+
}
1019+
1020+
let mut stepper = ClientServerStepper::from_config(StepperConfig::with_netcode_clients(1));
1021+
stepper.server_app.init_resource::<SeenAfterAuth>();
1022+
// Recommended ordering: register your validator `.after` the helper.
1023+
stepper
1024+
.server_app
1025+
.add_input_validator(authorize_controlled_targets::<LeafwingSequence<LeafwingInput1>>);
1026+
stepper.server_app.add_input_validator(
1027+
record_after_auth.after(authorize_controlled_targets::<LeafwingSequence<LeafwingInput1>>),
1028+
);
1029+
1030+
let client_of_0 = stepper.client_of(0).id();
1031+
let entity_a = stepper
1032+
.server_app
1033+
.world_mut()
1034+
.spawn((
1035+
ActionState::<LeafwingInput1>::default(),
1036+
Replicate::to_clients(NetworkTarget::All),
1037+
ControlledBy {
1038+
owner: client_of_0,
1039+
lifetime: Default::default(),
1040+
},
1041+
))
1042+
.id();
1043+
let entity_b = stepper
1044+
.server_app
1045+
.world_mut()
1046+
.spawn((
1047+
ActionState::<LeafwingInput1>::default(),
1048+
Replicate::to_clients(NetworkTarget::All),
1049+
))
1050+
.id();
1051+
stepper.frame_step(10);
1052+
1053+
let local_a = stepper
1054+
.client(0)
1055+
.get::<MessageManager>()
1056+
.unwrap()
1057+
.entity_mapper
1058+
.get_local(entity_a)
1059+
.expect("A replicated");
1060+
let local_b = stepper
1061+
.client(0)
1062+
.get::<MessageManager>()
1063+
.unwrap()
1064+
.entity_mapper
1065+
.get_local(entity_b)
1066+
.expect("B replicated");
1067+
for local in [local_a, local_b] {
1068+
stepper.client_apps[0].world_mut().entity_mut(local).insert(
1069+
InputMap::<LeafwingInput1>::new([(LeafwingInput1::Jump, KeyCode::KeyA)]),
1070+
);
1071+
}
1072+
stepper.frame_step(1);
1073+
stepper.client_apps[0]
1074+
.world_mut()
1075+
.resource_mut::<ButtonInput<KeyCode>>()
1076+
.press(KeyCode::KeyA);
1077+
stepper.frame_step(10);
1078+
1079+
let seen = &stepper.server_app.world().resource::<SeenAfterAuth>().0;
1080+
assert!(
1081+
seen.contains(&entity_a),
1082+
"custom validator never saw the authorized target A — setup/ordering issue",
1083+
);
1084+
assert!(
1085+
!seen.contains(&entity_b),
1086+
"custom validator saw the spoofed target B; it ran before authorize_controlled_targets \
1087+
(the `.after(authorize_controlled_targets::<S>)` ordering didn't take effect)",
1088+
);
1089+
}

0 commit comments

Comments
 (0)