Merge branch 'aweinstock/server-authoritative-physics' into 'master'

Aweinstock/server authoritative physics

See merge request veloren/veloren!2126
This commit is contained in:
Imbris 2021-04-15 22:24:00 +00:00
commit 85ee4c8cd1
12 changed files with 200 additions and 13 deletions

View File

@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added /disconnect_all_players admin command
- Added disconnectall CLI command
- One handed weapons can now be used and found in the world
- Players can now opt-in to server-authoritiative physics in gameplay settings.
### Changed
@ -56,6 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Item tooltips during trades will now inform the user of what ctrl-click and shift-click do
- International keyboards can now display more key names on Linux and Windows instead of `Unknown`.
- There is now a brief period after a character leaves the world where they cannot rejoin until their data is saved
- Certain uses of client-authoritative physics now subject the player to server-authoritative physics.
### Removed

View File

@ -48,6 +48,7 @@
"hud.settings.free_look_behavior": "Free look behavior",
"hud.settings.auto_walk_behavior": "Auto walk behavior",
"hud.settings.camera_clamp_behavior": "Camera clamp behavior",
"hud.settings.player_physics_behavior": "Player physics (experimental)",
"hud.settings.stop_auto_walk_on_input": "Stop auto walk on movement",
"hud.settings.auto_camera": "Auto camera",
"hud.settings.reset_gameplay": "Reset to Defaults",

View File

@ -786,7 +786,8 @@ impl Client {
| ClientGeneral::UnlockSkill(_)
| ClientGeneral::RefundSkill(_)
| ClientGeneral::RequestSiteInfo(_)
| ClientGeneral::UnlockSkillGroup(_) => &mut self.in_game_stream,
| ClientGeneral::UnlockSkillGroup(_)
| ClientGeneral::RequestPlayerPhysics { .. } => &mut self.in_game_stream,
//Only in game, terrain
ClientGeneral::TerrainChunkRequest { .. } => &mut self.terrain_stream,
//Always possible
@ -800,6 +801,12 @@ impl Client {
}
}
pub fn request_player_physics(&mut self, server_authoritative: bool) {
self.send_msg(ClientGeneral::RequestPlayerPhysics {
server_authoritative,
})
}
fn send_msg<S>(&mut self, msg: S)
where
S: Into<ClientMsg>,

View File

@ -81,6 +81,9 @@ pub enum ClientGeneral {
//Always possible
ChatMsg(String),
Terminate,
RequestPlayerPhysics {
server_authoritative: bool,
},
}
impl ClientMsg {
@ -117,7 +120,8 @@ impl ClientMsg {
| ClientGeneral::UnlockSkill(_)
| ClientGeneral::RefundSkill(_)
| ClientGeneral::RequestSiteInfo(_)
| ClientGeneral::UnlockSkillGroup(_) => {
| ClientGeneral::UnlockSkillGroup(_)
| ClientGeneral::RequestPlayerPhysics { .. } => {
c_type == ClientType::Game && presence.is_some()
},
//Always possible

View File

@ -32,3 +32,36 @@ pub enum GameMode {
/// server
#[derive(Copy, Clone, Default, Debug)]
pub struct PlayerEntity(pub Option<Entity>);
#[derive(Copy, Clone, Debug)]
pub struct PlayerPhysicsSetting {
/// true if the client wants server-authoratative physics (e.g. to use
/// airships properly)
pub client_optin: bool,
/// true if the server is forcing server-authoratative physics (e.g. as
/// punishment for wallhacking)
pub server_force: bool,
}
impl Default for PlayerPhysicsSetting {
fn default() -> PlayerPhysicsSetting {
PlayerPhysicsSetting {
client_optin: false,
server_force: false,
}
}
}
impl PlayerPhysicsSetting {
pub fn server_authoritative(&self) -> bool { self.client_optin || self.server_force }
pub fn client_authoritative(&self) -> bool { !self.server_authoritative() }
}
/// List of which players are using client-authoratative vs server-authoratative
/// physics, as a stop-gap until we can use server-authoratative physics for
/// everyone
#[derive(Clone, Default, Debug)]
pub struct PlayerPhysicsSettings {
pub settings: hashbrown::HashMap<uuid::Uuid, PlayerPhysicsSetting>,
}

View File

@ -10,7 +10,7 @@ use common::{
event::{EventBus, LocalEvent, ServerEvent},
outcome::Outcome,
region::RegionMap,
resources::{DeltaTime, GameMode, PlayerEntity, Time, TimeOfDay},
resources::{DeltaTime, GameMode, PlayerEntity, PlayerPhysicsSettings, Time, TimeOfDay},
slowjob::SlowJobPool,
terrain::{Block, TerrainChunk, TerrainGrid},
time::DayPeriod,
@ -278,6 +278,7 @@ impl State {
ecs.insert(SysMetrics::default());
ecs.insert(PhysicsMetrics::default());
ecs.insert(Trades::default());
ecs.insert(PlayerPhysicsSettings::default());
// Load plugins from asset directory
#[cfg(feature = "plugins")]

View File

@ -5,10 +5,10 @@ use crate::{
Tick,
};
use common::{
comp::{Collider, ForceUpdate, Inventory, InventoryUpdate, Last, Ori, Pos, Vel},
comp::{Collider, ForceUpdate, Inventory, InventoryUpdate, Last, Ori, Player, Pos, Vel},
outcome::Outcome,
region::{Event as RegionEvent, RegionMap},
resources::TimeOfDay,
resources::{PlayerPhysicsSettings, TimeOfDay},
terrain::TerrainChunkSize,
uid::Uid,
vol::RectVolSize,
@ -45,6 +45,8 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, InventoryUpdate>,
Write<'a, DeletedEntities>,
Write<'a, Vec<Outcome>>,
Read<'a, PlayerPhysicsSettings>,
ReadStorage<'a, Player>,
TrackedComps<'a>,
ReadTrackers<'a>,
);
@ -76,6 +78,8 @@ impl<'a> System<'a> for Sys {
mut inventory_updates,
mut deleted_entities,
mut outcomes,
player_physics_settings,
players,
tracked_comps,
trackers,
): Self::SystemData,
@ -215,8 +219,13 @@ impl<'a> System<'a> for Sys {
{
// Decide how regularly to send physics updates.
let send_now = if client_entity == &entity {
let player_physics_setting = players
.get(entity)
.and_then(|p| player_physics_settings.settings.get(&p.uuid()).copied())
.unwrap_or_default();
// Don't send client physics updates about itself unless force update is set
force_update.is_some()
// or the client is subject to server-authoritative physics
force_update.is_some() || player_physics_setting.server_authoritative()
} else if matches!(collider, Some(Collider::Voxel { .. })) {
// Things with a voxel collider (airships, etc.) need to have very stable
// physics so we always send updated for these where

View File

@ -1,7 +1,10 @@
use crate::{client::Client, presence::Presence, Settings};
use common::{
comp::{CanBuild, ControlEvent, Controller, ForceUpdate, Health, Ori, Pos, SkillSet, Vel},
comp::{
CanBuild, ControlEvent, Controller, ForceUpdate, Health, Ori, Player, Pos, SkillSet, Vel,
},
event::{EventBus, ServerEvent},
resources::PlayerPhysicsSettings,
terrain::TerrainGrid,
vol::ReadVol,
};
@ -9,7 +12,7 @@ use common_ecs::{Job, Origin, Phase, System};
use common_net::msg::{ClientGeneral, PresenceKind, ServerGeneral};
use common_sys::state::{BlockChange, BuildAreas};
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteStorage};
use tracing::{debug, trace};
use tracing::{debug, trace, warn};
impl Sys {
#[allow(clippy::too_many_arguments)]
@ -30,6 +33,8 @@ impl Sys {
controllers: &mut WriteStorage<'_, Controller>,
settings: &Read<'_, Settings>,
build_areas: &Read<'_, BuildAreas>,
player_physics_settings: &mut Write<'_, PlayerPhysicsSettings>,
maybe_player: &Option<&Player>,
msg: ClientGeneral,
) -> Result<(), crate::error::Error> {
let presence = match maybe_presence {
@ -93,13 +98,60 @@ impl Sys {
}
},
ClientGeneral::PlayerPhysics { pos, vel, ori } => {
let player_physics_setting = maybe_player.map(|p| {
player_physics_settings
.settings
.entry(p.uuid())
.or_default()
});
if matches!(presence.kind, PresenceKind::Character(_))
&& force_updates.get(entity).is_none()
&& healths.get(entity).map_or(true, |h| !h.is_dead)
&& player_physics_setting
.as_ref()
.map_or(true, |s| s.client_authoritative())
{
let _ = positions.insert(entity, pos);
let _ = velocities.insert(entity, vel);
let _ = orientations.insert(entity, ori);
let mut reject_update = false;
if let Some(mut setting) = player_physics_setting {
// If we detect any thresholds being exceeded, force server-authoritative
// physics for that player. This doesn't detect subtle hacks, but it
// prevents blatent ones and forces people to not debug physics hacks on the
// live server (and also mitigates some floating-point overflow crashes)
if let Some(prev_pos) = positions.get(entity) {
let value_squared = prev_pos.0.distance_squared(pos.0);
if value_squared > (5000.0f32).powf(2.0) {
setting.server_force = true;
reject_update = true;
warn!(
"PlayerPhysics position exceeded {:?} {:?} {:?}",
prev_pos,
pos,
value_squared.sqrt()
);
}
}
if vel.0.magnitude_squared() > (500.0f32).powf(2.0) {
setting.server_force = true;
reject_update = true;
warn!(
"PlayerPhysics velocity exceeded {:?} {:?}",
pos,
vel.0.magnitude()
);
}
}
if reject_update {
warn!(
"Rejected PlayerPhysics update {:?} {:?} {:?} {:?}",
pos, vel, ori, maybe_player
);
} else {
let _ = positions.insert(entity, pos);
let _ = velocities.insert(entity, vel);
let _ = orientations.insert(entity, ori);
}
}
},
ClientGeneral::BreakBlock(pos) => {
@ -156,6 +208,19 @@ impl Sys {
ClientGeneral::RequestSiteInfo(id) => {
server_emitter.emit(ServerEvent::RequestSiteInfo { entity, id });
},
ClientGeneral::RequestPlayerPhysics {
server_authoritative,
} => {
let player_physics_setting = maybe_player.map(|p| {
player_physics_settings
.settings
.entry(p.uuid())
.or_default()
});
if let Some(setting) = player_physics_setting {
setting.client_optin = server_authoritative;
}
},
_ => tracing::error!("not a client_in_game msg"),
}
Ok(())
@ -184,6 +249,8 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Controller>,
Read<'a, Settings>,
Read<'a, BuildAreas>,
Write<'a, PlayerPhysicsSettings>,
ReadStorage<'a, Player>,
);
const NAME: &'static str = "msg::in_game";
@ -209,12 +276,19 @@ impl<'a> System<'a> for Sys {
mut controllers,
settings,
build_areas,
mut player_physics_settings,
players,
): Self::SystemData,
) {
let mut server_emitter = server_event_bus.emitter();
for (entity, client, mut maybe_presence) in
(&entities, &mut clients, (&mut presences).maybe()).join()
for (entity, client, mut maybe_presence, player) in (
&entities,
&mut clients,
(&mut presences).maybe(),
players.maybe(),
)
.join()
{
let _ = super::try_recv_all(client, 2, |client, msg| {
Self::handle_client_in_game_msg(
@ -234,6 +308,8 @@ impl<'a> System<'a> for Sys {
&mut controllers,
&settings,
&build_areas,
&mut player_physics_settings,
&player,
msg,
)
});

View File

@ -44,6 +44,8 @@ widget_ids! {
auto_walk_behavior_list,
camera_clamp_behavior_text,
camera_clamp_behavior_list,
player_physics_behavior_text,
player_physics_behavior_list,
stop_auto_walk_on_input_button,
stop_auto_walk_on_input_label,
auto_camera_button,
@ -438,6 +440,43 @@ impl<'a> Widget for Gameplay<'a> {
}
}
// Player physics behavior
Text::new(
&self
.localized_strings
.get("hud.settings.player_physics_behavior"),
)
.down_from(state.ids.auto_walk_behavior_list, 10.0)
.right_from(state.ids.camera_clamp_behavior_text, 118.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.player_physics_behavior_text, ui);
let player_physics_selected =
self.global_state.settings.gameplay.player_physics_behavior as usize;
if let Some(clicked) = DropDownList::new(
&["Client-authoritative", "Server-authoritative"],
Some(player_physics_selected),
)
.w_h(200.0, 30.0)
.color(MENU_BG)
.label_color(TEXT_COLOR)
.label_font_id(self.fonts.cyri.conrod_id)
.down_from(state.ids.player_physics_behavior_text, 8.0)
.set(state.ids.player_physics_behavior_list, ui)
{
match clicked {
0 => events.push(ChangePlayerPhysicsBehavior {
server_authoritative: false,
}),
_ => events.push(ChangePlayerPhysicsBehavior {
server_authoritative: true,
}),
}
}
// Stop autowalk on input toggle
let stop_auto_walk_on_input_toggle = ToggleButton::new(
self.global_state.settings.gameplay.stop_auto_walk_on_input,

View File

@ -88,6 +88,9 @@ impl SessionState {
scene
.camera_mut()
.set_fov_deg(global_state.settings.graphics.fov);
client
.borrow_mut()
.request_player_physics(global_state.settings.gameplay.player_physics_behavior);
let hud = Hud::new(global_state, &client.borrow());
let walk_forward_dir = scene.camera().forward_xy();
let walk_right_dir = scene.camera().right_xy();

View File

@ -46,6 +46,7 @@ pub enum Gameplay {
ChangeFreeLookBehavior(PressBehavior),
ChangeAutoWalkBehavior(PressBehavior),
ChangeCameraClampBehavior(PressBehavior),
ChangePlayerPhysicsBehavior { server_authoritative: bool },
ChangeStopAutoWalkOnInput(bool),
ChangeAutoCamera(bool),
@ -224,6 +225,15 @@ impl SettingsChange {
Gameplay::ChangeCameraClampBehavior(behavior) => {
settings.gameplay.camera_clamp_behavior = behavior;
},
Gameplay::ChangePlayerPhysicsBehavior {
server_authoritative,
} => {
settings.gameplay.player_physics_behavior = server_authoritative;
session_state
.client
.borrow_mut()
.request_player_physics(server_authoritative);
},
Gameplay::ChangeStopAutoWalkOnInput(state) => {
settings.gameplay.stop_auto_walk_on_input = state;
},

View File

@ -14,6 +14,7 @@ pub struct GameplaySettings {
pub free_look_behavior: PressBehavior,
pub auto_walk_behavior: PressBehavior,
pub camera_clamp_behavior: PressBehavior,
pub player_physics_behavior: bool,
pub stop_auto_walk_on_input: bool,
pub auto_camera: bool,
}
@ -30,6 +31,7 @@ impl Default for GameplaySettings {
free_look_behavior: PressBehavior::Toggle,
auto_walk_behavior: PressBehavior::Toggle,
camera_clamp_behavior: PressBehavior::Toggle,
player_physics_behavior: false,
stop_auto_walk_on_input: true,
auto_camera: false,
}