spectate mode :D

This commit is contained in:
Isse
2022-07-05 17:01:22 +02:00
committed by IsseW
parent cc6a4c92f5
commit 0471e78f41
18 changed files with 281 additions and 92 deletions

View File

@ -3,6 +3,7 @@ char_selection-delete_permanently = Permanently delete this Character?
char_selection-deleting_character = Deleting Character... char_selection-deleting_character = Deleting Character...
char_selection-change_server = Change Server char_selection-change_server = Change Server
char_selection-enter_world = Enter World char_selection-enter_world = Enter World
char_selection-spectate = Spectate World
char_selection-logout = Logout char_selection-logout = Logout
char_selection-create_new_character = Create New Character char_selection-create_new_character = Create New Character
char_selection-creating_character = Creating Character... char_selection-creating_character = Creating Character...

View File

@ -30,7 +30,7 @@ use common::{
slot::{EquipSlot, InvSlotId, Slot}, slot::{EquipSlot, InvSlotId, Slot},
CharacterState, ChatMode, ControlAction, ControlEvent, Controller, ControllerInputs, CharacterState, ChatMode, ControlAction, ControlEvent, Controller, ControllerInputs,
GroupManip, InputKind, InventoryAction, InventoryEvent, InventoryUpdateEvent, GroupManip, InputKind, InventoryAction, InventoryEvent, InventoryUpdateEvent,
MapMarkerChange, UtteranceKind, MapMarkerChange, Pos, UtteranceKind,
}, },
event::{EventBus, LocalEvent}, event::{EventBus, LocalEvent},
grid::Grid, grid::Grid,
@ -107,6 +107,7 @@ pub enum Event {
CharacterEdited(CharacterId), CharacterEdited(CharacterId),
CharacterError(String), CharacterError(String),
MapMarker(comp::MapMarkerUpdate), MapMarker(comp::MapMarkerUpdate),
SpectatePosition(Vec3<f32>),
} }
pub struct WorldData { pub struct WorldData {
@ -816,7 +817,8 @@ impl Client {
| ClientGeneral::RequestPlayerPhysics { .. } | ClientGeneral::RequestPlayerPhysics { .. }
| ClientGeneral::RequestLossyTerrainCompression { .. } | ClientGeneral::RequestLossyTerrainCompression { .. }
| ClientGeneral::AcknowledgePersistenceLoadError | ClientGeneral::AcknowledgePersistenceLoadError
| ClientGeneral::UpdateMapMarker(_) => { | ClientGeneral::UpdateMapMarker(_)
| ClientGeneral::SpectatePosition(_) => {
#[cfg(feature = "tracy")] #[cfg(feature = "tracy")]
{ {
ingame = 1.0; ingame = 1.0;
@ -881,6 +883,14 @@ impl Client {
self.presence = Some(PresenceKind::Character(character_id)); 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 /// Load the current players character list
pub fn load_character_list(&mut self) { pub fn load_character_list(&mut self) {
self.character_list.loading = true; self.character_list.loading = true;
@ -1334,6 +1344,18 @@ impl Client {
self.send_msg(ClientGeneral::UpdateMapMarker(event)); self.send_msg(ClientGeneral::UpdateMapMarker(event));
} }
pub fn spectate_position(&mut self, pos: Vec3<f32>) {
if let Some(position) = self
.state
.ecs()
.write_storage::<comp::Pos>()
.get_mut(self.entity())
{
position.0 = pos;
}
self.send_msg(ClientGeneral::SpectatePosition(pos));
}
/// Checks whether a player can swap their weapon+ability `Loadout` settings /// Checks whether a player can swap their weapon+ability `Loadout` settings
/// and sends the `ControlAction` event that signals to do the swap. /// and sends the `ControlAction` event that signals to do the swap.
pub fn swap_loadout(&mut self) { self.control_action(ControlAction::SwapEquippedWeapons) } pub fn swap_loadout(&mut self) { self.control_action(ControlAction::SwapEquippedWeapons) }
@ -2256,6 +2278,9 @@ impl Client {
ServerGeneral::WeatherUpdate(weather) => { ServerGeneral::WeatherUpdate(weather) => {
self.weather.weather_update(weather); self.weather.weather_update(weather);
}, },
ServerGeneral::SpectatePosition(pos) => {
frontend_events.push(Event::SpectatePosition(pos));
},
_ => unreachable!("Not a in_game message"), _ => unreachable!("Not a in_game message"),
} }
Ok(()) Ok(())
@ -2319,6 +2344,18 @@ impl Client {
self.set_view_distance(vd); 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"), _ => unreachable!("Not a character_screen msg"),
} }
Ok(()) Ok(())

View File

@ -79,6 +79,8 @@ pub enum ClientGeneral {
UnlockSkillGroup(SkillGroupKind), UnlockSkillGroup(SkillGroupKind),
RequestSiteInfo(SiteId), RequestSiteInfo(SiteId),
UpdateMapMarker(comp::MapMarkerChange), UpdateMapMarker(comp::MapMarkerChange),
SpectatePosition(Vec3<f32>),
//Only in Game, via terrain stream //Only in Game, via terrain stream
TerrainChunkRequest { TerrainChunkRequest {
key: Vec2<i32>, key: Vec2<i32>,
@ -138,7 +140,8 @@ impl ClientMsg {
| ClientGeneral::RequestPlayerPhysics { .. } | ClientGeneral::RequestPlayerPhysics { .. }
| ClientGeneral::RequestLossyTerrainCompression { .. } | ClientGeneral::RequestLossyTerrainCompression { .. }
| ClientGeneral::AcknowledgePersistenceLoadError | ClientGeneral::AcknowledgePersistenceLoadError
| ClientGeneral::UpdateMapMarker(_) => { | ClientGeneral::UpdateMapMarker(_)
| ClientGeneral::SpectatePosition(_) => {
c_type == ClientType::Game && presence.is_some() c_type == ClientType::Game && presence.is_some()
}, },
//Always possible //Always possible

View File

@ -140,6 +140,7 @@ pub enum ServerGeneral {
CharacterCreated(character::CharacterId), CharacterCreated(character::CharacterId),
CharacterEdited(character::CharacterId), CharacterEdited(character::CharacterId),
CharacterSuccess, CharacterSuccess,
SpectatorSuccess(Vec3<f32>),
//Ingame related //Ingame related
GroupUpdate(comp::group::ChangeNotification<Uid>), GroupUpdate(comp::group::ChangeNotification<Uid>),
/// Indicate to the client that they are invited to join a group /// Indicate to the client that they are invited to join a group
@ -199,6 +200,9 @@ pub enum ServerGeneral {
SiteEconomy(EconomyInfo), SiteEconomy(EconomyInfo),
MapMarker(comp::MapMarkerUpdate), MapMarker(comp::MapMarkerUpdate),
WeatherUpdate(WeatherGrid), WeatherUpdate(WeatherGrid),
/// Suggest the client to spectate a position. Called after client has
/// requested teleport etc.
SpectatePosition(Vec3<f32>),
} }
impl ServerGeneral { impl ServerGeneral {
@ -292,7 +296,7 @@ impl ServerMsg {
| ServerGeneral::CharacterCreated(_) => { | ServerGeneral::CharacterCreated(_) => {
c_type != ClientType::ChatOnly && presence.is_none() c_type != ClientType::ChatOnly && presence.is_none()
}, },
ServerGeneral::CharacterSuccess => { ServerGeneral::CharacterSuccess | ServerGeneral::SpectatorSuccess(_) => {
c_type == ClientType::Game && presence.is_none() c_type == ClientType::Game && presence.is_none()
}, },
//Ingame related //Ingame related
@ -312,7 +316,8 @@ impl ServerMsg {
| ServerGeneral::FinishedTrade(_) | ServerGeneral::FinishedTrade(_)
| ServerGeneral::SiteEconomy(_) | ServerGeneral::SiteEconomy(_)
| ServerGeneral::MapMarker(_) | ServerGeneral::MapMarker(_)
| ServerGeneral::WeatherUpdate(_) => { | ServerGeneral::WeatherUpdate(_)
| ServerGeneral::SpectatePosition(_) => {
c_type == ClientType::Game && presence.is_some() c_type == ClientType::Game && presence.is_some()
}, },
// Always possible // Always possible

View File

@ -108,6 +108,7 @@ pub enum ServerEvent {
entity: EcsEntity, entity: EcsEntity,
character_id: CharacterId, character_id: CharacterId,
}, },
InitSpectator(EcsEntity),
UpdateCharacterData { UpdateCharacterData {
entity: EcsEntity, entity: EcsEntity,
components: ( components: (

View File

@ -171,7 +171,8 @@ impl Client {
| ServerGeneral::CharacterActionError(_) | ServerGeneral::CharacterActionError(_)
| ServerGeneral::CharacterCreated(_) | ServerGeneral::CharacterCreated(_)
| ServerGeneral::CharacterEdited(_) | ServerGeneral::CharacterEdited(_)
| ServerGeneral::CharacterSuccess => { | ServerGeneral::CharacterSuccess
| ServerGeneral::SpectatorSuccess(_) => {
PreparedMsg::new(1, &g, &self.character_screen_stream_params) PreparedMsg::new(1, &g, &self.character_screen_stream_params)
}, },
//In-game related //In-game related
@ -188,7 +189,8 @@ impl Client {
| ServerGeneral::UpdatePendingTrade(_, _, _) | ServerGeneral::UpdatePendingTrade(_, _, _)
| ServerGeneral::FinishedTrade(_) | ServerGeneral::FinishedTrade(_)
| ServerGeneral::MapMarker(_) | ServerGeneral::MapMarker(_)
| ServerGeneral::WeatherUpdate(_) => { | ServerGeneral::WeatherUpdate(_)
| ServerGeneral::SpectatePosition(_) => {
PreparedMsg::new(2, &g, &self.in_game_stream_params) PreparedMsg::new(2, &g, &self.in_game_stream_params)
}, },
//In-game related, terrain //In-game related, terrain

View File

@ -6,6 +6,7 @@ use crate::{
client::Client, client::Client,
location::Locations, location::Locations,
login_provider::LoginProvider, login_provider::LoginProvider,
presence::Presence,
settings::{ settings::{
Ban, BanAction, BanInfo, EditableSetting, SettingError, WhitelistInfo, WhitelistRecord, Ban, BanAction, BanInfo, EditableSetting, SettingError, WhitelistInfo, WhitelistRecord,
}, },
@ -49,7 +50,7 @@ use common::{
weather, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect, weather, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect,
}; };
use common_net::{ use common_net::{
msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneral}, msg::{DisconnectReason, Notification, PlayerListUpdate, PresenceKind, ServerGeneral},
sync::WorldSyncExt, sync::WorldSyncExt,
}; };
use common_state::{BuildAreaError, BuildAreas}; use common_state::{BuildAreaError, BuildAreas};
@ -231,20 +232,38 @@ fn position_mut<T>(
}) })
.unwrap_or(entity); .unwrap_or(entity);
let mut maybe_pos = None;
let res = server let res = server
.state .state
.ecs() .ecs()
.write_storage::<comp::Pos>() .write_storage::<comp::Pos>()
.get_mut(entity) .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)); .ok_or_else(|| format!("Cannot get position for {:?}!", descriptor));
if res.is_ok() {
if let Some(pos) = maybe_pos {
if server
.state
.ecs()
.read_storage::<Presence>()
.get(entity)
.map(|presence| presence.kind == PresenceKind::Spectator)
.unwrap_or(false)
{
server.notify_client(entity, ServerGeneral::SpectatePosition(pos));
} else {
let _ = server let _ = server
.state .state
.ecs() .ecs()
.write_storage::<comp::ForceUpdate>() .write_storage::<comp::ForceUpdate>()
.insert(entity, comp::ForceUpdate); .insert(entity, comp::ForceUpdate);
} }
}
res res
} }

View File

@ -33,6 +33,11 @@ pub fn handle_initialize_character(
server.state.initialize_character_data(entity, character_id); 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( pub fn handle_loaded_character_data(
server: &mut Server, server: &mut Server,
entity: EcsEntity, entity: EcsEntity,

View File

@ -6,7 +6,8 @@ use common::event::{EventBus, ServerEvent, ServerEventDiscriminants};
use common_base::span; use common_base::span;
use entity_creation::{ use entity_creation::{
handle_beam, handle_create_npc, handle_create_ship, handle_create_waypoint, 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::{ use entity_manipulation::{
handle_aura, handle_bonk, handle_buff, handle_change_ability, handle_combo_change, handle_aura, handle_bonk, handle_buff, handle_change_ability, handle_combo_change,
@ -139,6 +140,7 @@ impl Server {
entity, entity,
character_id, character_id,
} => handle_initialize_character(self, entity, character_id), } => handle_initialize_character(self, entity, character_id),
ServerEvent::InitSpectator(entity) => handle_initialize_spectator(self, entity),
ServerEvent::UpdateCharacterData { entity, components } => { ServerEvent::UpdateCharacterData { entity, components } => {
let ( let (
body, body,

View File

@ -107,6 +107,8 @@ pub trait StateExt {
) -> EcsEntityBuilder; ) -> EcsEntityBuilder;
/// Insert common/default components for a new character joining the server /// Insert common/default components for a new character joining the server
fn initialize_character_data(&mut self, entity: EcsEntity, character_id: CharacterId); 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. /// Update the components associated with the entity's current character.
/// Performed after loading component data from the database /// Performed after loading component data from the database
fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents); 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::<SpawnPoint>().0;
if self.read_component_copied::<Uid>(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::<Client>().get(entity) {
client.send_fallible(ServerGeneral::SpectatorSuccess(spawn_point));
}
}
}
fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents) { fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents) {
let PersistedComponents { let PersistedComponents {
body, body,

View File

@ -7,7 +7,7 @@ use crate::{
EditableSettings, EditableSettings,
}; };
use common::{ use common::{
comp::{ChatType, Player, UnresolvedChatMsg}, comp::{Admin, AdminRole, ChatType, Player, UnresolvedChatMsg},
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
uid::Uid, uid::Uid,
}; };
@ -15,7 +15,7 @@ use common_ecs::{Job, Origin, Phase, System};
use common_net::msg::{ClientGeneral, ServerGeneral}; use common_net::msg::{ClientGeneral, ServerGeneral};
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, WriteExpect}; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, WriteExpect};
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use tracing::{debug, warn}; use tracing::debug;
impl Sys { impl Sys {
fn handle_client_character_screen_msg( fn handle_client_character_screen_msg(
@ -26,18 +26,43 @@ impl Sys {
character_updater: &mut WriteExpect<'_, CharacterUpdater>, character_updater: &mut WriteExpect<'_, CharacterUpdater>,
uids: &ReadStorage<'_, Uid>, uids: &ReadStorage<'_, Uid>,
players: &ReadStorage<'_, Player>, players: &ReadStorage<'_, Player>,
admins: &ReadStorage<'_, Admin>,
presences: &ReadStorage<'_, Presence>, presences: &ReadStorage<'_, Presence>,
editable_settings: &ReadExpect<'_, EditableSettings>, editable_settings: &ReadExpect<'_, EditableSettings>,
alias_validator: &ReadExpect<'_, AliasValidator>, alias_validator: &ReadExpect<'_, AliasValidator>,
msg: ClientGeneral, msg: ClientGeneral,
) -> Result<(), crate::error::Error> { ) -> 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 { match msg {
// Request spectator state // Request spectator state
ClientGeneral::Spectate => { ClientGeneral::Spectate => {
if players.contains(entity) { if let Some(admin) = admins.get(entity) && admin.0 >= AdminRole::Moderator {
warn!("Spectator mode not yet implemented on server"); send_join_messages()?;
server_emitter.emit(ServerEvent::InitSpectator(entity));
} else { } else {
debug!("dropped Spectate msg from unregistered client") debug!("dropped Spectate msg from unprivileged client")
} }
}, },
ClientGeneral::Character(character_id) => { ClientGeneral::Character(character_id) => {
@ -76,31 +101,14 @@ impl Sys {
character_id, character_id,
); );
send_join_messages()?;
// Start inserting non-persisted/default components for the entity // Start inserting non-persisted/default components for the entity
// while we load the DB data // while we load the DB data
server_emitter.emit(ServerEvent::InitCharacterData { server_emitter.emit(ServerEvent::InitCharacterData {
entity, entity,
character_id, 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 { } else {
debug!("Client is not yet registered"); debug!("Client is not yet registered");
@ -199,6 +207,7 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Uid>, ReadStorage<'a, Uid>,
ReadStorage<'a, Client>, ReadStorage<'a, Client>,
ReadStorage<'a, Player>, ReadStorage<'a, Player>,
ReadStorage<'a, Admin>,
ReadStorage<'a, Presence>, ReadStorage<'a, Presence>,
ReadExpect<'a, EditableSettings>, ReadExpect<'a, EditableSettings>,
ReadExpect<'a, AliasValidator>, ReadExpect<'a, AliasValidator>,
@ -218,6 +227,7 @@ impl<'a> System<'a> for Sys {
uids, uids,
clients, clients,
players, players,
admins,
presences, presences,
editable_settings, editable_settings,
alias_validator, alias_validator,
@ -235,6 +245,7 @@ impl<'a> System<'a> for Sys {
&mut character_updater, &mut character_updater,
&uids, &uids,
&players, &players,
&admins,
&presences, &presences,
&editable_settings, &editable_settings,
&alias_validator, &alias_validator,

View File

@ -3,8 +3,8 @@ use crate::TerrainPersistence;
use crate::{client::Client, presence::Presence, Settings}; use crate::{client::Client, presence::Presence, Settings};
use common::{ use common::{
comp::{ comp::{
Admin, CanBuild, ControlEvent, Controller, ForceUpdate, Health, Ori, Player, Pos, SkillSet, Admin, AdminRole, CanBuild, ControlEvent, Controller, ForceUpdate, Health, Ori, Player,
Vel, Pos, SkillSet, Vel,
}, },
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
link::Is, link::Is,
@ -14,7 +14,7 @@ use common::{
vol::ReadVol, vol::ReadVol,
}; };
use common_ecs::{Job, Origin, Phase, System}; 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 common_state::{BlockChange, BuildAreas};
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteStorage}; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteStorage};
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
@ -288,6 +288,13 @@ impl Sys {
ClientGeneral::UpdateMapMarker(update) => { ClientGeneral::UpdateMapMarker(update) => {
server_emitter.emit(ServerEvent::UpdateMapMarker { entity, 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::RequestCharacterList
| ClientGeneral::CreateCharacter { .. } | ClientGeneral::CreateCharacter { .. }
| ClientGeneral::EditCharacter { .. } | ClientGeneral::EditCharacter { .. }

View File

@ -1136,7 +1136,7 @@ impl Hud {
let character_id = match client.presence().unwrap() { let character_id = match client.presence().unwrap() {
PresenceKind::Character(id) => Some(id), PresenceKind::Character(id) => Some(id),
PresenceKind::Spectator => unreachable!("HUD creation in Spectator mode!"), PresenceKind::Spectator => None,
PresenceKind::Possessor => None, PresenceKind::Possessor => None,
}; };

View File

@ -144,6 +144,18 @@ impl PlayState for CharSelectionState {
Rc::clone(&self.client), 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 => { ui::Event::ClearCharacterListError => {
self.char_selection_ui.error = None; self.char_selection_ui.error = None;
}, },

View File

@ -127,6 +127,7 @@ image_ids_ice! {
pub enum Event { pub enum Event {
Logout, Logout,
Play(CharacterId), Play(CharacterId),
Spectate,
AddCharacter { AddCharacter {
alias: String, alias: String,
mainhand: Option<String>, mainhand: Option<String>,
@ -152,6 +153,7 @@ enum Mode {
new_character_button: button::State, new_character_button: button::State,
logout_button: button::State, logout_button: button::State,
enter_world_button: button::State, enter_world_button: button::State,
spectate_button: button::State,
yes_button: button::State, yes_button: button::State,
no_button: button::State, no_button: button::State,
}, },
@ -185,6 +187,7 @@ impl Mode {
new_character_button: Default::default(), new_character_button: Default::default(),
logout_button: Default::default(), logout_button: Default::default(),
enter_world_button: Default::default(), enter_world_button: Default::default(),
spectate_button: Default::default(),
yes_button: Default::default(), yes_button: Default::default(),
no_button: Default::default(), no_button: Default::default(),
} }
@ -283,6 +286,7 @@ enum Message {
Back, Back,
Logout, Logout,
EnterWorld, EnterWorld,
Spectate,
Select(CharacterId), Select(CharacterId),
Delete(usize), Delete(usize),
Edit(usize), Edit(usize),
@ -397,6 +401,7 @@ impl Controls {
ref mut new_character_button, ref mut new_character_button,
ref mut logout_button, ref mut logout_button,
ref mut enter_world_button, ref mut enter_world_button,
ref mut spectate_button,
ref mut yes_button, ref mut yes_button,
ref mut no_button, ref mut no_button,
} => { } => {
@ -676,36 +681,52 @@ impl Controls {
.padding(15) .padding(15)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill); .height(Length::Fill);
let mut bottom_content = vec![
let logout = neat_button( Container::new(neat_button(
logout_button, logout_button,
i18n.get("char_selection.logout").into_owned(), i18n.get("char_selection.logout").into_owned(),
FILL_FRAC_ONE, FILL_FRAC_ONE,
button_style, button_style,
Some(Message::Logout), Some(Message::Logout),
); ))
.width(Length::Fill)
.height(Length::Units(SMALL_BUTTON_HEIGHT))
.into(),
];
let enter_world = neat_button( 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(),
);
}
bottom_content.push(
Container::new(neat_button(
enter_world_button, enter_world_button,
i18n.get("char_selection.enter_world").into_owned(), i18n.get("char_selection.enter_world").into_owned(),
FILL_FRAC_TWO, FILL_FRAC_TWO,
button_style, button_style,
selected.map(|_| Message::EnterWorld), 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)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Units(52)) .height(Length::Units(52))
.center_x() .center_x()
.into(), .into(),
Space::new(Length::Fill, Length::Shrink).into(), );
])
.align_items(Align::End); 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()]) let content = Column::with_children(vec![top.into(), bottom.into()])
.width(Length::Fill) .width(Length::Fill)
@ -1410,6 +1431,11 @@ impl Controls {
events.push(Event::Play(selected)); events.push(Event::Play(selected));
} }
}, },
Message::Spectate => {
if matches!(self.mode, Mode::Select { .. }) {
events.push(Event::Spectate);
}
},
Message::Select(id) => { Message::Select(id) => {
if let Mode::Select { .. } = &mut self.mode { if let Mode::Select { .. } = &mut self.mode {
self.selected = Some(id); self.selected = Some(id);

View File

@ -130,11 +130,7 @@ fn clamp_and_modulate(ori: Vec3<f32>) -> Vec3<f32> {
/// e = floor(ln(near/(far - near))/ln(2)) /// e = floor(ln(near/(far - near))/ln(2))
/// db/dz = 2^(2-e) / ((1 / far - 1 / near) * (far)^2) /// db/dz = 2^(2-e) / ((1 / far - 1 / near) * (far)^2)
/// ``` /// ```
/// ///CameraMode::ThirdPerson
/// 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:
///
/// ```ignore /// ```ignore
/// abs(2^(-23) / (db/dz)). /// abs(2^(-23) / (db/dz)).
/// ``` /// ```
@ -320,13 +316,18 @@ impl Camera {
// Make sure aspect is valid // Make sure aspect is valid
let aspect = if aspect.is_normal() { aspect } else { 1.0 }; 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 { Self {
tgt_focus: Vec3::unit_z() * 10.0, tgt_focus: Vec3::unit_z() * 10.0,
focus: Vec3::unit_z() * 10.0, focus: Vec3::unit_z() * 10.0,
tgt_ori: Vec3::zero(), tgt_ori: Vec3::zero(),
ori: Vec3::zero(), ori: Vec3::zero(),
tgt_dist: 10.0, tgt_dist: dist,
dist: 10.0, dist,
tgt_fov: 1.1, tgt_fov: 1.1,
fov: 1.1, fov: 1.1,
tgt_fixate: 1.0, tgt_fixate: 1.0,
@ -652,6 +653,12 @@ impl Camera {
/// Set the focus position of the camera. /// Set the focus position of the camera.
pub fn set_focus_pos(&mut self, focus: Vec3<f32>) { self.tgt_focus = focus; } pub fn set_focus_pos(&mut self, focus: Vec3<f32>) { self.tgt_focus = focus; }
/// Set the focus position of the camera, without lerping.
pub fn force_focus_pos(&mut self, focus: Vec3<f32>) {
self.tgt_focus = focus;
self.focus = focus;
}
/// Get the aspect ratio of the camera. /// Get the aspect ratio of the camera.
pub fn get_aspect_ratio(&self) -> f32 { self.aspect } pub fn get_aspect_ratio(&self) -> f32 { self.aspect }
@ -695,7 +702,7 @@ impl Camera {
self.set_distance(MIN_ZOOM); self.set_distance(MIN_ZOOM);
}, },
CameraMode::Freefly => { CameraMode::Freefly => {
self.zoom_by(0.0, None); self.set_distance(MIN_ZOOM);
}, },
} }
} }
@ -714,7 +721,10 @@ impl Camera {
/// Cycle the camera to its next valid mode. If is_admin is false then only /// 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. /// modes which are accessible without admin access will be cycled to.
pub fn next_mode(&mut self, is_admin: bool) { 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 { self.set_mode(match self.mode {
CameraMode::ThirdPerson => CameraMode::FirstPerson, CameraMode::ThirdPerson => CameraMode::FirstPerson,
CameraMode::FirstPerson => { CameraMode::FirstPerson => {
@ -727,6 +737,7 @@ impl Camera {
CameraMode::Freefly => CameraMode::ThirdPerson, CameraMode::Freefly => CameraMode::ThirdPerson,
}); });
} }
}
/// Return a unit vector in the forward direction for the current camera /// Return a unit vector in the forward direction for the current camera
/// orientation /// orientation

View File

@ -37,6 +37,7 @@ use common::{
vol::ReadVol, vol::ReadVol,
}; };
use common_base::{prof_span, span}; use common_base::{prof_span, span};
use common_net::msg::PresenceKind;
use common_state::State; use common_state::State;
use comp::item::Reagent; use comp::item::Reagent;
use hashbrown::HashMap; use hashbrown::HashMap;
@ -299,10 +300,15 @@ impl Scene {
let terrain = Terrain::new(renderer, &data, lod.get_data(), sprite_render_context); 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 { Self {
data, data,
globals_bind_group, 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(), camera_input_state: Vec2::zero(),
event_lights: Vec::new(), event_lights: Vec::new(),

View File

@ -358,6 +358,9 @@ impl SessionState {
client::Event::MapMarker(event) => { client::Event::MapMarker(event) => {
self.hud.show.update_map_markers(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<Event>) -> PlayStateResult { fn tick(&mut self, global_state: &mut GlobalState, events: Vec<Event>) -> PlayStateResult {
span!(_guard, "tick", "<Session as PlayState>::tick"); span!(_guard, "tick", "<Session as PlayState>::tick");
// TODO: let mut client = self.client.borrow_mut(); // TODO: let mut client = self.client.borrow_mut();
// TODO: can this be a method on the session or are there borrowcheck issues? // TODO: can this be a method on the session or are there borrowcheck issues?
let (client_presence, client_registered) = { let (client_presence, client_registered) = {
let client = self.client.borrow(); let client = self.client.borrow();
(client.presence(), client.registered()) (client.presence(), client.registered())
}; };
if client_presence.is_some() { if let Some(presence) = client_presence {
let camera = self.scene.camera_mut(); let camera = self.scene.camera_mut();
// Clamp camera's vertical angle if the toggle is enabled // Clamp camera's vertical angle if the toggle is enabled
@ -425,7 +427,7 @@ impl PlayState for SessionState {
} }
// Compute camera data // Compute camera data
camera.compute_dependents(&*self.client.borrow().state().terrain()); camera.compute_dependents(&*client.state().terrain());
let camera::Dependents { let camera::Dependents {
cam_pos, cam_dir, .. cam_pos, cam_dir, ..
} = self.scene.camera().dependents(); } = self.scene.camera().dependents();
@ -480,6 +482,10 @@ impl PlayState for SessionState {
drop(client); drop(client);
if presence == PresenceKind::Spectator {
self.client.borrow_mut().spectate_position(cam_pos);
}
// Nearest block to consider with GameInput primary or secondary key. // Nearest block to consider with GameInput primary or secondary key.
let nearest_block_dist = find_shortest_distance(&[ let nearest_block_dist = find_shortest_distance(&[
mine_target.filter(|_| is_mining).map(|t| t.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 // The server should do its own filtering of which entities are sent
// to clients to prevent abuse. // to clients to prevent abuse.
let camera = self.scene.camera_mut(); 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 => { GameInput::Select => {
if !state { if !state {