diff --git a/assets/voxygen/i18n/en/char_selection.ftl b/assets/voxygen/i18n/en/char_selection.ftl index c2ebc0cbaf..ab23cff563 100644 --- a/assets/voxygen/i18n/en/char_selection.ftl +++ b/assets/voxygen/i18n/en/char_selection.ftl @@ -3,6 +3,7 @@ char_selection-delete_permanently = Permanently delete this Character? char_selection-deleting_character = Deleting Character... char_selection-change_server = Change Server char_selection-enter_world = Enter World +char_selection-spectate = Spectate World char_selection-logout = Logout char_selection-create_new_character = Create New Character char_selection-creating_character = Creating Character... diff --git a/client/src/lib.rs b/client/src/lib.rs index a0f04ecf5d..42122d1c68 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -30,7 +30,7 @@ use common::{ slot::{EquipSlot, InvSlotId, Slot}, CharacterState, ChatMode, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, InputKind, InventoryAction, InventoryEvent, InventoryUpdateEvent, - MapMarkerChange, UtteranceKind, + MapMarkerChange, Pos, UtteranceKind, }, event::{EventBus, LocalEvent}, grid::Grid, @@ -107,6 +107,7 @@ pub enum Event { CharacterEdited(CharacterId), CharacterError(String), MapMarker(comp::MapMarkerUpdate), + SpectatePosition(Vec3), } pub struct WorldData { @@ -816,7 +817,8 @@ impl Client { | ClientGeneral::RequestPlayerPhysics { .. } | ClientGeneral::RequestLossyTerrainCompression { .. } | ClientGeneral::AcknowledgePersistenceLoadError - | ClientGeneral::UpdateMapMarker(_) => { + | ClientGeneral::UpdateMapMarker(_) + | ClientGeneral::SpectatePosition(_) => { #[cfg(feature = "tracy")] { ingame = 1.0; @@ -881,6 +883,14 @@ impl Client { self.presence = Some(PresenceKind::Character(character_id)); } + /// Request a state transition to `ClientState::Spectate`. + pub fn request_spectate(&mut self) { + self.send_msg(ClientGeneral::Spectate); + + //Assume we are in_game unless server tells us otherwise + self.presence = Some(PresenceKind::Spectator); + } + /// Load the current players character list pub fn load_character_list(&mut self) { self.character_list.loading = true; @@ -1334,6 +1344,18 @@ impl Client { self.send_msg(ClientGeneral::UpdateMapMarker(event)); } + pub fn spectate_position(&mut self, pos: Vec3) { + if let Some(position) = self + .state + .ecs() + .write_storage::() + .get_mut(self.entity()) + { + position.0 = pos; + } + self.send_msg(ClientGeneral::SpectatePosition(pos)); + } + /// Checks whether a player can swap their weapon+ability `Loadout` settings /// and sends the `ControlAction` event that signals to do the swap. pub fn swap_loadout(&mut self) { self.control_action(ControlAction::SwapEquippedWeapons) } @@ -2256,6 +2278,9 @@ impl Client { ServerGeneral::WeatherUpdate(weather) => { self.weather.weather_update(weather); }, + ServerGeneral::SpectatePosition(pos) => { + frontend_events.push(Event::SpectatePosition(pos)); + }, _ => unreachable!("Not a in_game message"), } Ok(()) @@ -2319,6 +2344,18 @@ impl Client { self.set_view_distance(vd); } }, + ServerGeneral::SpectatorSuccess(spawn_point) => { + if let Some(vd) = self.view_distance { + self.state + .ecs() + .write_storage() + .insert(self.entity(), Pos(spawn_point)) + .expect("This shouldn't exist"); + events.push(Event::SpectatePosition(spawn_point)); + debug!("client is now in ingame state on server"); + self.set_view_distance(vd); + } + }, _ => unreachable!("Not a character_screen msg"), } Ok(()) diff --git a/common/net/src/msg/client.rs b/common/net/src/msg/client.rs index 05efe1f407..2cdf77201c 100644 --- a/common/net/src/msg/client.rs +++ b/common/net/src/msg/client.rs @@ -79,6 +79,8 @@ pub enum ClientGeneral { UnlockSkillGroup(SkillGroupKind), RequestSiteInfo(SiteId), UpdateMapMarker(comp::MapMarkerChange), + + SpectatePosition(Vec3), //Only in Game, via terrain stream TerrainChunkRequest { key: Vec2, @@ -138,7 +140,8 @@ impl ClientMsg { | ClientGeneral::RequestPlayerPhysics { .. } | ClientGeneral::RequestLossyTerrainCompression { .. } | ClientGeneral::AcknowledgePersistenceLoadError - | ClientGeneral::UpdateMapMarker(_) => { + | ClientGeneral::UpdateMapMarker(_) + | ClientGeneral::SpectatePosition(_) => { c_type == ClientType::Game && presence.is_some() }, //Always possible diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs index f0849271c4..2c7241614f 100644 --- a/common/net/src/msg/server.rs +++ b/common/net/src/msg/server.rs @@ -140,6 +140,7 @@ pub enum ServerGeneral { CharacterCreated(character::CharacterId), CharacterEdited(character::CharacterId), CharacterSuccess, + SpectatorSuccess(Vec3), //Ingame related GroupUpdate(comp::group::ChangeNotification), /// Indicate to the client that they are invited to join a group @@ -199,6 +200,9 @@ pub enum ServerGeneral { SiteEconomy(EconomyInfo), MapMarker(comp::MapMarkerUpdate), WeatherUpdate(WeatherGrid), + /// Suggest the client to spectate a position. Called after client has + /// requested teleport etc. + SpectatePosition(Vec3), } impl ServerGeneral { @@ -292,7 +296,7 @@ impl ServerMsg { | ServerGeneral::CharacterCreated(_) => { c_type != ClientType::ChatOnly && presence.is_none() }, - ServerGeneral::CharacterSuccess => { + ServerGeneral::CharacterSuccess | ServerGeneral::SpectatorSuccess(_) => { c_type == ClientType::Game && presence.is_none() }, //Ingame related @@ -312,7 +316,8 @@ impl ServerMsg { | ServerGeneral::FinishedTrade(_) | ServerGeneral::SiteEconomy(_) | ServerGeneral::MapMarker(_) - | ServerGeneral::WeatherUpdate(_) => { + | ServerGeneral::WeatherUpdate(_) + | ServerGeneral::SpectatePosition(_) => { c_type == ClientType::Game && presence.is_some() }, // Always possible diff --git a/common/src/event.rs b/common/src/event.rs index 46b5a15787..d540b80b5d 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -108,6 +108,7 @@ pub enum ServerEvent { entity: EcsEntity, character_id: CharacterId, }, + InitSpectator(EcsEntity), UpdateCharacterData { entity: EcsEntity, components: ( diff --git a/server/src/client.rs b/server/src/client.rs index e00aaca265..bd031c4681 100644 --- a/server/src/client.rs +++ b/server/src/client.rs @@ -171,7 +171,8 @@ impl Client { | ServerGeneral::CharacterActionError(_) | ServerGeneral::CharacterCreated(_) | ServerGeneral::CharacterEdited(_) - | ServerGeneral::CharacterSuccess => { + | ServerGeneral::CharacterSuccess + | ServerGeneral::SpectatorSuccess(_) => { PreparedMsg::new(1, &g, &self.character_screen_stream_params) }, //In-game related @@ -188,7 +189,8 @@ impl Client { | ServerGeneral::UpdatePendingTrade(_, _, _) | ServerGeneral::FinishedTrade(_) | ServerGeneral::MapMarker(_) - | ServerGeneral::WeatherUpdate(_) => { + | ServerGeneral::WeatherUpdate(_) + | ServerGeneral::SpectatePosition(_) => { PreparedMsg::new(2, &g, &self.in_game_stream_params) }, //In-game related, terrain diff --git a/server/src/cmd.rs b/server/src/cmd.rs index e5838becf6..f6dc922479 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -6,6 +6,7 @@ use crate::{ client::Client, location::Locations, login_provider::LoginProvider, + presence::Presence, settings::{ Ban, BanAction, BanInfo, EditableSetting, SettingError, WhitelistInfo, WhitelistRecord, }, @@ -49,7 +50,7 @@ use common::{ weather, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect, }; use common_net::{ - msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneral}, + msg::{DisconnectReason, Notification, PlayerListUpdate, PresenceKind, ServerGeneral}, sync::WorldSyncExt, }; use common_state::{BuildAreaError, BuildAreas}; @@ -231,19 +232,37 @@ fn position_mut( }) .unwrap_or(entity); + let mut maybe_pos = None; + let res = server .state .ecs() .write_storage::() .get_mut(entity) - .map(f) + .map(|pos| { + let res = f(pos); + maybe_pos = Some(pos.0); + res + }) .ok_or_else(|| format!("Cannot get position for {:?}!", descriptor)); - if res.is_ok() { - let _ = server + + if let Some(pos) = maybe_pos { + if server .state .ecs() - .write_storage::() - .insert(entity, comp::ForceUpdate); + .read_storage::() + .get(entity) + .map(|presence| presence.kind == PresenceKind::Spectator) + .unwrap_or(false) + { + server.notify_client(entity, ServerGeneral::SpectatePosition(pos)); + } else { + let _ = server + .state + .ecs() + .write_storage::() + .insert(entity, comp::ForceUpdate); + } } res } diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 1524d3a517..4afac192c5 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -33,6 +33,11 @@ pub fn handle_initialize_character( server.state.initialize_character_data(entity, character_id); } +pub fn handle_initialize_spectator(server: &mut Server, entity: EcsEntity) { + server.state.initialize_spectator_data(entity); + sys::subscription::initialize_region_subscription(server.state.ecs(), entity); +} + pub fn handle_loaded_character_data( server: &mut Server, entity: EcsEntity, diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index d8870b9c4e..62f3bf0873 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -6,7 +6,8 @@ use common::event::{EventBus, ServerEvent, ServerEventDiscriminants}; use common_base::span; use entity_creation::{ handle_beam, handle_create_npc, handle_create_ship, handle_create_waypoint, - handle_initialize_character, handle_loaded_character_data, handle_shockwave, handle_shoot, + handle_initialize_character, handle_initialize_spectator, handle_loaded_character_data, + handle_shockwave, handle_shoot, }; use entity_manipulation::{ handle_aura, handle_bonk, handle_buff, handle_change_ability, handle_combo_change, @@ -139,6 +140,7 @@ impl Server { entity, character_id, } => handle_initialize_character(self, entity, character_id), + ServerEvent::InitSpectator(entity) => handle_initialize_spectator(self, entity), ServerEvent::UpdateCharacterData { entity, components } => { let ( body, diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 8a9f47a112..265a48ad98 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -107,6 +107,8 @@ pub trait StateExt { ) -> EcsEntityBuilder; /// Insert common/default components for a new character joining the server fn initialize_character_data(&mut self, entity: EcsEntity, character_id: CharacterId); + /// Insert common/default components for a new spectator joining the server + fn initialize_spectator_data(&mut self, entity: EcsEntity); /// Update the components associated with the entity's current character. /// Performed after loading component data from the database fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents); @@ -523,6 +525,32 @@ impl StateExt for State { } } + fn initialize_spectator_data(&mut self, entity: EcsEntity) { + let spawn_point = self.ecs().read_resource::().0; + + if self.read_component_copied::(entity).is_some() { + // NOTE: By fetching the player_uid, we validated that the entity exists, and we + // call nothing that can delete it in any of the subsequent + // commands, so we can assume that all of these calls succeed, + // justifying ignoring the result of insertion. + self.write_component_ignore_entity_dead(entity, comp::Pos(spawn_point)); + + // Make sure physics components are updated + self.write_component_ignore_entity_dead(entity, comp::ForceUpdate); + + const INITIAL_VD: u32 = 5; //will be changed after login + self.write_component_ignore_entity_dead( + entity, + Presence::new(INITIAL_VD, PresenceKind::Spectator), + ); + + // Tell the client its request was successful. + if let Some(client) = self.ecs().read_storage::().get(entity) { + client.send_fallible(ServerGeneral::SpectatorSuccess(spawn_point)); + } + } + } + fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents) { let PersistedComponents { body, diff --git a/server/src/sys/msg/character_screen.rs b/server/src/sys/msg/character_screen.rs index 296abb3da0..7190a5a91a 100644 --- a/server/src/sys/msg/character_screen.rs +++ b/server/src/sys/msg/character_screen.rs @@ -7,7 +7,7 @@ use crate::{ EditableSettings, }; use common::{ - comp::{ChatType, Player, UnresolvedChatMsg}, + comp::{Admin, AdminRole, ChatType, Player, UnresolvedChatMsg}, event::{EventBus, ServerEvent}, uid::Uid, }; @@ -15,7 +15,7 @@ use common_ecs::{Job, Origin, Phase, System}; use common_net::msg::{ClientGeneral, ServerGeneral}; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, WriteExpect}; use std::sync::atomic::Ordering; -use tracing::{debug, warn}; +use tracing::debug; impl Sys { fn handle_client_character_screen_msg( @@ -26,18 +26,43 @@ impl Sys { character_updater: &mut WriteExpect<'_, CharacterUpdater>, uids: &ReadStorage<'_, Uid>, players: &ReadStorage<'_, Player>, + admins: &ReadStorage<'_, Admin>, presences: &ReadStorage<'_, Presence>, editable_settings: &ReadExpect<'_, EditableSettings>, alias_validator: &ReadExpect<'_, AliasValidator>, msg: ClientGeneral, ) -> Result<(), crate::error::Error> { + let mut send_join_messages = || -> Result<(), crate::error::Error> { + // Give the player a welcome message + if !editable_settings.server_description.is_empty() { + client.send(ServerGeneral::server_msg( + ChatType::CommandInfo, + &*editable_settings.server_description, + ))?; + } + + if !client.login_msg_sent.load(Ordering::Relaxed) { + if let Some(player_uid) = uids.get(entity) { + server_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg { + chat_type: ChatType::Online(*player_uid), + message: "".to_string(), + })); + + client.login_msg_sent.store(true, Ordering::Relaxed); + } + } + Ok(()) + }; match msg { // Request spectator state ClientGeneral::Spectate => { - if players.contains(entity) { - warn!("Spectator mode not yet implemented on server"); + if let Some(admin) = admins.get(entity) && admin.0 >= AdminRole::Moderator { + send_join_messages()?; + + server_emitter.emit(ServerEvent::InitSpectator(entity)); + } else { - debug!("dropped Spectate msg from unregistered client") + debug!("dropped Spectate msg from unprivileged client") } }, ClientGeneral::Character(character_id) => { @@ -76,31 +101,14 @@ impl Sys { character_id, ); + send_join_messages()?; + // Start inserting non-persisted/default components for the entity // while we load the DB data server_emitter.emit(ServerEvent::InitCharacterData { entity, character_id, }); - - // Give the player a welcome message - if !editable_settings.server_description.is_empty() { - client.send(ServerGeneral::server_msg( - ChatType::CommandInfo, - &*editable_settings.server_description, - ))?; - } - - if !client.login_msg_sent.load(Ordering::Relaxed) { - if let Some(player_uid) = uids.get(entity) { - server_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg { - chat_type: ChatType::Online(*player_uid), - message: "".to_string(), - })); - - client.login_msg_sent.store(true, Ordering::Relaxed); - } - } } } else { debug!("Client is not yet registered"); @@ -199,6 +207,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Uid>, ReadStorage<'a, Client>, ReadStorage<'a, Player>, + ReadStorage<'a, Admin>, ReadStorage<'a, Presence>, ReadExpect<'a, EditableSettings>, ReadExpect<'a, AliasValidator>, @@ -218,6 +227,7 @@ impl<'a> System<'a> for Sys { uids, clients, players, + admins, presences, editable_settings, alias_validator, @@ -235,6 +245,7 @@ impl<'a> System<'a> for Sys { &mut character_updater, &uids, &players, + &admins, &presences, &editable_settings, &alias_validator, diff --git a/server/src/sys/msg/in_game.rs b/server/src/sys/msg/in_game.rs index 6ceb6da12a..65f3d69e07 100644 --- a/server/src/sys/msg/in_game.rs +++ b/server/src/sys/msg/in_game.rs @@ -3,8 +3,8 @@ use crate::TerrainPersistence; use crate::{client::Client, presence::Presence, Settings}; use common::{ comp::{ - Admin, CanBuild, ControlEvent, Controller, ForceUpdate, Health, Ori, Player, Pos, SkillSet, - Vel, + Admin, AdminRole, CanBuild, ControlEvent, Controller, ForceUpdate, Health, Ori, Player, + Pos, SkillSet, Vel, }, event::{EventBus, ServerEvent}, link::Is, @@ -14,7 +14,7 @@ use common::{ vol::ReadVol, }; use common_ecs::{Job, Origin, Phase, System}; -use common_net::msg::{ClientGeneral, ServerGeneral}; +use common_net::msg::{ClientGeneral, PresenceKind, ServerGeneral}; use common_state::{BlockChange, BuildAreas}; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteStorage}; use tracing::{debug, trace, warn}; @@ -288,6 +288,13 @@ impl Sys { ClientGeneral::UpdateMapMarker(update) => { server_emitter.emit(ServerEvent::UpdateMapMarker { entity, update }); }, + ClientGeneral::SpectatePosition(pos) => { + if let Some(admin) = maybe_admin && admin.0 >= AdminRole::Moderator && presence.kind == PresenceKind::Spectator { + if let Some(position) = positions.get_mut(entity) { + position.0 = pos; + } + } + }, ClientGeneral::RequestCharacterList | ClientGeneral::CreateCharacter { .. } | ClientGeneral::EditCharacter { .. } diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index b651e9ec8a..38ba33e1c9 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -1136,7 +1136,7 @@ impl Hud { let character_id = match client.presence().unwrap() { PresenceKind::Character(id) => Some(id), - PresenceKind::Spectator => unreachable!("HUD creation in Spectator mode!"), + PresenceKind::Spectator => None, PresenceKind::Possessor => None, }; diff --git a/voxygen/src/menu/char_selection/mod.rs b/voxygen/src/menu/char_selection/mod.rs index 73fa288619..d0b83f2566 100644 --- a/voxygen/src/menu/char_selection/mod.rs +++ b/voxygen/src/menu/char_selection/mod.rs @@ -144,6 +144,18 @@ impl PlayState for CharSelectionState { Rc::clone(&self.client), ))); }, + ui::Event::Spectate => { + { + let mut c = self.client.borrow_mut(); + c.request_spectate(); + c.set_view_distance(global_state.settings.graphics.view_distance); + c.set_lod_distance(global_state.settings.graphics.lod_distance); + } + return PlayStateResult::Switch(Box::new(SessionState::new( + global_state, + Rc::clone(&self.client), + ))); + }, ui::Event::ClearCharacterListError => { self.char_selection_ui.error = None; }, diff --git a/voxygen/src/menu/char_selection/ui/mod.rs b/voxygen/src/menu/char_selection/ui/mod.rs index 049364bb78..cb05e6ed36 100644 --- a/voxygen/src/menu/char_selection/ui/mod.rs +++ b/voxygen/src/menu/char_selection/ui/mod.rs @@ -127,6 +127,7 @@ image_ids_ice! { pub enum Event { Logout, Play(CharacterId), + Spectate, AddCharacter { alias: String, mainhand: Option, @@ -152,6 +153,7 @@ enum Mode { new_character_button: button::State, logout_button: button::State, enter_world_button: button::State, + spectate_button: button::State, yes_button: button::State, no_button: button::State, }, @@ -185,6 +187,7 @@ impl Mode { new_character_button: Default::default(), logout_button: Default::default(), enter_world_button: Default::default(), + spectate_button: Default::default(), yes_button: Default::default(), no_button: Default::default(), } @@ -283,6 +286,7 @@ enum Message { Back, Logout, EnterWorld, + Spectate, Select(CharacterId), Delete(usize), Edit(usize), @@ -397,6 +401,7 @@ impl Controls { ref mut new_character_button, ref mut logout_button, ref mut enter_world_button, + ref mut spectate_button, ref mut yes_button, ref mut no_button, } => { @@ -676,36 +681,52 @@ impl Controls { .padding(15) .width(Length::Fill) .height(Length::Fill); + let mut bottom_content = vec![ + Container::new(neat_button( + logout_button, + i18n.get("char_selection.logout").into_owned(), + FILL_FRAC_ONE, + button_style, + Some(Message::Logout), + )) + .width(Length::Fill) + .height(Length::Units(SMALL_BUTTON_HEIGHT)) + .into(), + ]; - let logout = neat_button( - logout_button, - i18n.get("char_selection.logout").into_owned(), - FILL_FRAC_ONE, - button_style, - Some(Message::Logout), - ); - - let enter_world = neat_button( - enter_world_button, - i18n.get("char_selection.enter_world").into_owned(), - FILL_FRAC_TWO, - button_style, - selected.map(|_| Message::EnterWorld), - ); - - let bottom = Row::with_children(vec![ - Container::new(logout) - .width(Length::Fill) - .height(Length::Units(SMALL_BUTTON_HEIGHT)) - .into(), - Container::new(enter_world) + if client.is_moderator() { + bottom_content.push( + Container::new(neat_button( + spectate_button, + i18n.get("char_selection.spectate").into_owned(), + FILL_FRAC_TWO, + button_style, + Some(Message::Spectate), + )) .width(Length::Fill) .height(Length::Units(52)) .center_x() .into(), - Space::new(Length::Fill, Length::Shrink).into(), - ]) - .align_items(Align::End); + ); + } + + bottom_content.push( + Container::new(neat_button( + enter_world_button, + i18n.get("char_selection.enter_world").into_owned(), + FILL_FRAC_TWO, + button_style, + selected.map(|_| Message::EnterWorld), + )) + .width(Length::Fill) + .height(Length::Units(52)) + .center_x() + .into(), + ); + + bottom_content.push(Space::new(Length::Fill, Length::Shrink).into()); + + let bottom = Row::with_children(bottom_content).align_items(Align::End); let content = Column::with_children(vec![top.into(), bottom.into()]) .width(Length::Fill) @@ -1410,6 +1431,11 @@ impl Controls { events.push(Event::Play(selected)); } }, + Message::Spectate => { + if matches!(self.mode, Mode::Select { .. }) { + events.push(Event::Spectate); + } + }, Message::Select(id) => { if let Mode::Select { .. } = &mut self.mode { self.selected = Some(id); diff --git a/voxygen/src/scene/camera.rs b/voxygen/src/scene/camera.rs index c93d3474c7..c0ab43b928 100644 --- a/voxygen/src/scene/camera.rs +++ b/voxygen/src/scene/camera.rs @@ -130,11 +130,7 @@ fn clamp_and_modulate(ori: Vec3) -> Vec3 { /// e = floor(ln(near/(far - near))/ln(2)) /// db/dz = 2^(2-e) / ((1 / far - 1 / near) * (far)^2) /// ``` -/// -/// Then the maximum precision you can safely use to get a change in the -/// integer representation of the mantissa (assuming 32-bit floating points) -/// is around: -/// +///CameraMode::ThirdPerson /// ```ignore /// abs(2^(-23) / (db/dz)). /// ``` @@ -320,13 +316,18 @@ impl Camera { // Make sure aspect is valid let aspect = if aspect.is_normal() { aspect } else { 1.0 }; + let dist = match mode { + CameraMode::ThirdPerson => 10.0, + CameraMode::FirstPerson | CameraMode::Freefly => MIN_ZOOM, + }; + Self { tgt_focus: Vec3::unit_z() * 10.0, focus: Vec3::unit_z() * 10.0, tgt_ori: Vec3::zero(), ori: Vec3::zero(), - tgt_dist: 10.0, - dist: 10.0, + tgt_dist: dist, + dist, tgt_fov: 1.1, fov: 1.1, tgt_fixate: 1.0, @@ -652,6 +653,12 @@ impl Camera { /// Set the focus position of the camera. pub fn set_focus_pos(&mut self, focus: Vec3) { self.tgt_focus = focus; } + /// Set the focus position of the camera, without lerping. + pub fn force_focus_pos(&mut self, focus: Vec3) { + self.tgt_focus = focus; + self.focus = focus; + } + /// Get the aspect ratio of the camera. pub fn get_aspect_ratio(&self) -> f32 { self.aspect } @@ -695,7 +702,7 @@ impl Camera { self.set_distance(MIN_ZOOM); }, CameraMode::Freefly => { - self.zoom_by(0.0, None); + self.set_distance(MIN_ZOOM); }, } } @@ -714,18 +721,22 @@ impl Camera { /// Cycle the camera to its next valid mode. If is_admin is false then only /// modes which are accessible without admin access will be cycled to. - pub fn next_mode(&mut self, is_admin: bool) { - self.set_mode(match self.mode { - CameraMode::ThirdPerson => CameraMode::FirstPerson, - CameraMode::FirstPerson => { - if is_admin { - CameraMode::Freefly - } else { - CameraMode::ThirdPerson - } - }, - CameraMode::Freefly => CameraMode::ThirdPerson, - }); + pub fn next_mode(&mut self, is_admin: bool, is_spectator: bool) { + if is_spectator && is_admin { + self.set_mode(CameraMode::Freefly); + } else { + self.set_mode(match self.mode { + CameraMode::ThirdPerson => CameraMode::FirstPerson, + CameraMode::FirstPerson => { + if is_admin { + CameraMode::Freefly + } else { + CameraMode::ThirdPerson + } + }, + CameraMode::Freefly => CameraMode::ThirdPerson, + }); + } } /// Return a unit vector in the forward direction for the current camera diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 78ae2622cb..7ea99c2930 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -37,6 +37,7 @@ use common::{ vol::ReadVol, }; use common_base::{prof_span, span}; +use common_net::msg::PresenceKind; use common_state::State; use comp::item::Reagent; use hashbrown::HashMap; @@ -299,10 +300,15 @@ impl Scene { let terrain = Terrain::new(renderer, &data, lod.get_data(), sprite_render_context); + let camera_mode = match client.presence() { + Some(PresenceKind::Spectator) => CameraMode::Freefly, + _ => CameraMode::ThirdPerson, + }; + Self { data, globals_bind_group, - camera: Camera::new(resolution.x / resolution.y, CameraMode::ThirdPerson), + camera: Camera::new(resolution.x / resolution.y, camera_mode), camera_input_state: Vec2::zero(), event_lights: Vec::new(), diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index 2ada0a5b28..f5839e4748 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -358,6 +358,9 @@ impl SessionState { client::Event::MapMarker(event) => { self.hud.show.update_map_markers(event); }, + client::Event::SpectatePosition(pos) => { + self.scene.camera_mut().force_focus_pos(pos); + }, } } @@ -386,14 +389,13 @@ impl PlayState for SessionState { fn tick(&mut self, global_state: &mut GlobalState, events: Vec) -> PlayStateResult { span!(_guard, "tick", "::tick"); // TODO: let mut client = self.client.borrow_mut(); - // TODO: can this be a method on the session or are there borrowcheck issues? let (client_presence, client_registered) = { let client = self.client.borrow(); (client.presence(), client.registered()) }; - if client_presence.is_some() { + if let Some(presence) = client_presence { let camera = self.scene.camera_mut(); // Clamp camera's vertical angle if the toggle is enabled @@ -425,7 +427,7 @@ impl PlayState for SessionState { } // Compute camera data - camera.compute_dependents(&*self.client.borrow().state().terrain()); + camera.compute_dependents(&*client.state().terrain()); let camera::Dependents { cam_pos, cam_dir, .. } = self.scene.camera().dependents(); @@ -480,6 +482,10 @@ impl PlayState for SessionState { drop(client); + if presence == PresenceKind::Spectator { + self.client.borrow_mut().spectate_position(cam_pos); + } + // Nearest block to consider with GameInput primary or secondary key. let nearest_block_dist = find_shortest_distance(&[ mine_target.filter(|_| is_mining).map(|t| t.distance), @@ -887,7 +893,14 @@ impl PlayState for SessionState { // The server should do its own filtering of which entities are sent // to clients to prevent abuse. let camera = self.scene.camera_mut(); - camera.next_mode(self.client.borrow().is_moderator()); + let client = self.client.borrow(); + camera.next_mode( + client.is_moderator(), + client + .presence() + .map(|presence| presence == PresenceKind::Spectator) + .unwrap_or(false), + ); }, GameInput::Select => { if !state {