mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge remote-tracking branch 'origin/master' into clientstates
This commit is contained in:
commit
aa963b7686
@ -71,7 +71,7 @@ check:
|
|||||||
tags:
|
tags:
|
||||||
- veloren-docker
|
- veloren-docker
|
||||||
script:
|
script:
|
||||||
- RUSTFLAGS="-D warnings" cargo check
|
- RUSTFLAGS="-D warnings" cargo check --locked
|
||||||
|
|
||||||
code-quality:
|
code-quality:
|
||||||
stage: check-compile
|
stage: check-compile
|
||||||
|
@ -24,6 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- New attack animation
|
- New attack animation
|
||||||
- weapon control system
|
- weapon control system
|
||||||
- Game pauses when in singleplayer and pause menu
|
- Game pauses when in singleplayer and pause menu
|
||||||
|
- Added authentication system (to play on the official server register on https://account.veloren.net)
|
||||||
|
- Added gamepad/controller support
|
||||||
|
- Added player feedback when attempting to pickup an item with a full inventory
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@ -31,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Removed highlighting of non-collectible sprites
|
- Removed highlighting of non-collectible sprites
|
||||||
- Fixed /give_exp ignoring player argument
|
- Fixed /give_exp ignoring player argument
|
||||||
- Extend run sfx to small animals to prevent sneak attacks by geese.
|
- Extend run sfx to small animals to prevent sneak attacks by geese.
|
||||||
|
- Decreased clientside latency of ServerEvent mediated effects (e.g. projectiles, inventory operations, etc)
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
4024
Cargo.lock
generated
4024
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -18,6 +18,9 @@ Currently the communication of contributors happens mainly on our [official Disc
|
|||||||
|
|
||||||
## Useful Links
|
## Useful Links
|
||||||
|
|
||||||
|
[Sign Up](https://account.veloren.net) - Here you can create an online account for Veloren.
|
||||||
|
This will be needed to play on auth-enabled servers, including the official server.
|
||||||
|
|
||||||
[The Book](https://book.veloren.net) - A collection of all important information relating to Veloren. It includes information on how to compile Veloren and how to contribute.
|
[The Book](https://book.veloren.net) - A collection of all important information relating to Veloren. It includes information on how to compile Veloren and how to contribute.
|
||||||
|
|
||||||
[Future Plans](https://gitlab.com/veloren/veloren/milestones) - Go here for information about Veloren's development roadmap and what we're currently working on.
|
[Future Plans](https://gitlab.com/veloren/veloren/milestones) - Go here for information about Veloren's development roadmap and what we're currently working on.
|
||||||
|
@ -459,6 +459,10 @@
|
|||||||
"peacock": {
|
"peacock": {
|
||||||
"keyword": "peacock",
|
"keyword": "peacock",
|
||||||
"generic": "Peacock"
|
"generic": "Peacock"
|
||||||
|
},
|
||||||
|
"eagle": {
|
||||||
|
"keyword": "eagle",
|
||||||
|
"generic": "Eagle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -88,6 +88,12 @@
|
|||||||
"voxygen.audio.sfx.inventory.consumable.food",
|
"voxygen.audio.sfx.inventory.consumable.food",
|
||||||
],
|
],
|
||||||
threshold: 0.3,
|
threshold: 0.3,
|
||||||
|
),
|
||||||
|
Inventory(CollectFailed): (
|
||||||
|
files: [
|
||||||
|
"voxygen.audio.sfx.inventory.add_failed",
|
||||||
|
],
|
||||||
|
threshold: 0.3,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
BIN
assets/voxygen/audio/sfx/inventory/add_failed.wav
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/audio/sfx/inventory/add_failed.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/audio/sfx/inventory/add_item.wav
(Stored with Git LFS)
BIN
assets/voxygen/audio/sfx/inventory/add_item.wav
(Stored with Git LFS)
Binary file not shown.
@ -65,6 +65,8 @@ VoxygenLocalization(
|
|||||||
"common.disclaimer": "Disclaimer",
|
"common.disclaimer": "Disclaimer",
|
||||||
"common.cancel": "Cancel",
|
"common.cancel": "Cancel",
|
||||||
"common.none": "None",
|
"common.none": "None",
|
||||||
|
"common.error": "Error",
|
||||||
|
"common.fatal_error": "Fatal Error",
|
||||||
|
|
||||||
// Message when connection to the server is lost
|
// Message when connection to the server is lost
|
||||||
"common.connection_lost": r#"Connection lost!
|
"common.connection_lost": r#"Connection lost!
|
||||||
@ -113,12 +115,25 @@ Thanks for taking the time to read this notice, we hope you enjoy the game!
|
|||||||
// Login process description
|
// Login process description
|
||||||
"main.login_process": r#"Information on the Login Process:
|
"main.login_process": r#"Information on the Login Process:
|
||||||
|
|
||||||
Put in any username. No Account needed yet.
|
If you are having issues signing in:
|
||||||
|
|
||||||
Character names and appearances will be saved locally.
|
Please note that you now need an account
|
||||||
|
to play on auth-enabled servers.
|
||||||
|
|
||||||
Levels/Items are not saved yet."#,
|
You can create an account over at
|
||||||
|
|
||||||
|
https://account.veloren.net."#,
|
||||||
|
"main.login.server_not_found": "Server not found",
|
||||||
|
"main.login.authentication_error": "Auth error on server",
|
||||||
|
"main.login.server_full": "Server is full",
|
||||||
|
"main.login.untrusted_auth_server": "Auth server not trusted",
|
||||||
|
"main.login.outdated_client_or_server": "ServerWentMad: Probably versions are incompatible, check for updates.",
|
||||||
|
"main.login.timeout": "Timeout: Server did not respond in time. (Overloaded or network issues).",
|
||||||
|
"main.login.server_shut_down": "Server shut down",
|
||||||
|
"main.login.already_logged_in": "You are already logged into the server.",
|
||||||
|
"main.login.network_error": "Network error",
|
||||||
|
"main.login.failed_sending_request": "Request to Auth server failed",
|
||||||
|
"main.login.client_crashed": "Client crashed",
|
||||||
|
|
||||||
/// End Main screen section
|
/// End Main screen section
|
||||||
|
|
||||||
|
@ -111,4 +111,32 @@
|
|||||||
center: ("npc.peacock.female.tail"),
|
center: ("npc.peacock.female.tail"),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
(Eagle, Male): (
|
||||||
|
head: (
|
||||||
|
offset: (-2.0, -2.0, -3.5),
|
||||||
|
center: ("npc.eagle.female.head"),
|
||||||
|
),
|
||||||
|
torso: (
|
||||||
|
offset: (-3.0, -4.5, -4.5),
|
||||||
|
center: ("npc.eagle.female.torso"),
|
||||||
|
),
|
||||||
|
tail: (
|
||||||
|
offset: (-2.0, -3.5, -3.5),
|
||||||
|
center: ("npc.eagle.female.tail"),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(Eagle, Female): (
|
||||||
|
head: (
|
||||||
|
offset: (-2.0, -2.0, -3.5),
|
||||||
|
center: ("npc.eagle.female.head"),
|
||||||
|
),
|
||||||
|
torso: (
|
||||||
|
offset: (-3.0, -4.5, -4.5),
|
||||||
|
center: ("npc.eagle.female.torso"),
|
||||||
|
),
|
||||||
|
tail: (
|
||||||
|
offset: (-2.0, -3.5, -3.5),
|
||||||
|
center: ("npc.eagle.female.tail"),
|
||||||
|
)
|
||||||
|
),
|
||||||
})
|
})
|
@ -143,4 +143,40 @@
|
|||||||
lateral: ("npc.peacock.female.leg_r"),
|
lateral: ("npc.peacock.female.leg_r"),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
(Eagle, Male): (
|
||||||
|
wing_l: (
|
||||||
|
offset: (-1.0, -3.5, -13.0),
|
||||||
|
lateral: ("npc.eagle.male.wing_l"),
|
||||||
|
),
|
||||||
|
wing_r: (
|
||||||
|
offset: (-1.0, -3.5, -13.0),
|
||||||
|
lateral: ("npc.eagle.male.wing_r"),
|
||||||
|
),
|
||||||
|
foot_l: (
|
||||||
|
offset: (-1.5, 0.0, -8.0),
|
||||||
|
lateral: ("npc.eagle.male.leg_l"),
|
||||||
|
),
|
||||||
|
foot_r: (
|
||||||
|
offset: (-1.5, 0.0, -8.0),
|
||||||
|
lateral: ("npc.eagle.male.leg_r"),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(Eagle, Female): (
|
||||||
|
wing_l: (
|
||||||
|
offset: (-1.0, -3.5, -13.0),
|
||||||
|
lateral: ("npc.eagle.female.wing_l"),
|
||||||
|
),
|
||||||
|
wing_r: (
|
||||||
|
offset: (-1.0, -3.5, -13.0),
|
||||||
|
lateral: ("npc.eagle.female.wing_r"),
|
||||||
|
),
|
||||||
|
foot_l: (
|
||||||
|
offset: (-1.5, 0.0, -8.0),
|
||||||
|
lateral: ("npc.eagle.female.leg_l"),
|
||||||
|
),
|
||||||
|
foot_r: (
|
||||||
|
offset: (-1.5, 0.0, -8.0),
|
||||||
|
lateral: ("npc.eagle.female.leg_r"),
|
||||||
|
)
|
||||||
|
),
|
||||||
})
|
})
|
BIN
assets/voxygen/voxel/npc/eagle/female/head.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/npc/eagle/female/head.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/npc/eagle/female/leg_l.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/npc/eagle/female/leg_l.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/npc/eagle/female/leg_r.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/npc/eagle/female/leg_r.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/npc/eagle/female/tail.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/npc/eagle/female/tail.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/npc/eagle/female/torso.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/npc/eagle/female/torso.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/npc/eagle/female/wing_l.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/npc/eagle/female/wing_l.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/npc/eagle/female/wing_r.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/npc/eagle/female/wing_r.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/npc/eagle/male/head.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/npc/eagle/male/head.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/npc/eagle/male/leg_l.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/npc/eagle/male/leg_l.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/npc/eagle/male/leg_r.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/npc/eagle/male/leg_r.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/npc/eagle/male/tail.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/npc/eagle/male/tail.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/npc/eagle/male/torso.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/npc/eagle/male/torso.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/npc/eagle/male/wing_l.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/npc/eagle/male/wing_l.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/npc/eagle/male/wing_r.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/npc/eagle/male/wing_r.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -386,7 +386,7 @@
|
|||||||
central: ("npc.lion.male.ears"),
|
central: ("npc.lion.male.ears"),
|
||||||
),
|
),
|
||||||
tail: (
|
tail: (
|
||||||
offset: (-0.5, -1.0, -8.0),
|
offset: (-0.5, -1.0, -1.0),
|
||||||
central: ("npc.lion.male.tail"),
|
central: ("npc.lion.male.tail"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -417,7 +417,7 @@
|
|||||||
),
|
),
|
||||||
tail: (
|
tail: (
|
||||||
offset: (-0.5, -1.0, -1.0),
|
offset: (-0.5, -1.0, -1.0),
|
||||||
central: ("npc.lion.male.tail"),
|
central: ("npc.lion.female.tail"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(Tarasque, Male): (
|
(Tarasque, Male): (
|
||||||
|
@ -51,7 +51,9 @@ fn main() {
|
|||||||
println!("Players online: {:?}", client.get_players());
|
println!("Players online: {:?}", client.get_players());
|
||||||
|
|
||||||
client
|
client
|
||||||
.register(comp::Player::new(username, None), password)
|
.register(username, password, |provider| {
|
||||||
|
provider == "https://auth.veloren.net"
|
||||||
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
|
@ -15,3 +15,4 @@ log = "0.4.8"
|
|||||||
specs = "0.15.1"
|
specs = "0.15.1"
|
||||||
vek = { version = "0.9.9", features = ["serde"] }
|
vek = { version = "0.9.9", features = ["serde"] }
|
||||||
hashbrown = { version = "0.6.2", features = ["rayon", "serde", "nightly"] }
|
hashbrown = { version = "0.6.2", features = ["rayon", "serde", "nightly"] }
|
||||||
|
authc = { git = "https://gitlab.com/veloren/auth.git", rev = "65571ade0d954a0e0bd995fdb314854ff146ab97" }
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use authc::AuthClientError;
|
||||||
use common::net::PostError;
|
use common::net::PostError;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -7,11 +8,18 @@ pub enum Error {
|
|||||||
ServerTimeout,
|
ServerTimeout,
|
||||||
ServerShutdown,
|
ServerShutdown,
|
||||||
TooManyPlayers,
|
TooManyPlayers,
|
||||||
InvalidAuth,
|
AlreadyLoggedIn,
|
||||||
|
AuthErr(String),
|
||||||
|
AuthClientError(AuthClientError),
|
||||||
|
AuthServerNotTrusted,
|
||||||
//TODO: InvalidAlias,
|
//TODO: InvalidAlias,
|
||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<PostError> for Error {
|
impl From<PostError> for Error {
|
||||||
fn from(err: PostError) -> Self { Error::Network(err) }
|
fn from(err: PostError) -> Self { Self::Network(err) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AuthClientError> for Error {
|
||||||
|
fn from(err: AuthClientError) -> Self { Self::AuthClientError(err) }
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ pub mod error;
|
|||||||
|
|
||||||
// Reexports
|
// Reexports
|
||||||
pub use crate::error::Error;
|
pub use crate::error::Error;
|
||||||
|
pub use authc::AuthClientError;
|
||||||
pub use specs::{
|
pub use specs::{
|
||||||
join::Join,
|
join::Join,
|
||||||
saveload::{Marker, MarkerAllocator},
|
saveload::{Marker, MarkerAllocator},
|
||||||
@ -13,11 +14,13 @@ pub use specs::{
|
|||||||
|
|
||||||
use byteorder::{ByteOrder, LittleEndian};
|
use byteorder::{ByteOrder, LittleEndian};
|
||||||
use common::{
|
use common::{
|
||||||
comp::{self, ControlEvent, Controller, ControllerInputs, InventoryManip},
|
comp::{
|
||||||
|
self, ControlEvent, Controller, ControllerInputs, InventoryManip, InventoryUpdateEvent,
|
||||||
|
},
|
||||||
event::{EventBus, SfxEvent, SfxEventItem},
|
event::{EventBus, SfxEvent, SfxEventItem},
|
||||||
msg::{
|
msg::{
|
||||||
validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, PlayerListUpdate,
|
validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, PlayerListUpdate,
|
||||||
RequestStateError, ServerError, ServerInfo, ServerMsg, MAX_BYTES_CHAT_MSG,
|
RegisterError, RequestStateError, ServerInfo, ServerMsg, MAX_BYTES_CHAT_MSG,
|
||||||
},
|
},
|
||||||
net::PostBox,
|
net::PostBox,
|
||||||
state::State,
|
state::State,
|
||||||
@ -86,13 +89,13 @@ impl Client {
|
|||||||
let mut postbox = PostBox::to(addr)?;
|
let mut postbox = PostBox::to(addr)?;
|
||||||
|
|
||||||
// Wait for initial sync
|
// Wait for initial sync
|
||||||
let (state, entity, server_info, world_map) = match postbox.next_message() {
|
let (state, entity, server_info, world_map) = match postbox.next_message()? {
|
||||||
Some(ServerMsg::InitialSync {
|
ServerMsg::InitialSync {
|
||||||
entity_package,
|
entity_package,
|
||||||
server_info,
|
server_info,
|
||||||
time_of_day,
|
time_of_day,
|
||||||
world_map: (map_size, world_map),
|
world_map: (map_size, world_map),
|
||||||
}) => {
|
} => {
|
||||||
// TODO: Display that versions don't match in Voxygen
|
// TODO: Display that versions don't match in Voxygen
|
||||||
if server_info.git_hash != common::util::GIT_HASH.to_string() {
|
if server_info.git_hash != common::util::GIT_HASH.to_string() {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
@ -105,6 +108,8 @@ impl Client {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::debug!("Auth Server: {:?}", server_info.auth_provider);
|
||||||
|
|
||||||
// Initialize `State`
|
// Initialize `State`
|
||||||
let mut state = State::default();
|
let mut state = State::default();
|
||||||
let entity = state.ecs_mut().apply_entity_package(entity_package);
|
let entity = state.ecs_mut().apply_entity_package(entity_package);
|
||||||
@ -129,9 +134,7 @@ impl Client {
|
|||||||
|
|
||||||
(state, entity, server_info, (world_map, map_size))
|
(state, entity, server_info, (world_map, map_size))
|
||||||
},
|
},
|
||||||
Some(ServerMsg::Error(ServerError::TooManyPlayers)) => {
|
ServerMsg::TooManyPlayers => return Err(Error::TooManyPlayers),
|
||||||
return Err(Error::TooManyPlayers);
|
|
||||||
},
|
|
||||||
_ => return Err(Error::ServerWentMad),
|
_ => return Err(Error::ServerWentMad),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -172,16 +175,40 @@ impl Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Request a state transition to `ClientState::Registered`.
|
/// Request a state transition to `ClientState::Registered`.
|
||||||
pub fn register(&mut self, player: comp::Player, password: String) -> Result<(), Error> {
|
pub fn register(
|
||||||
self.postbox
|
&mut self,
|
||||||
.send_message(ClientMsg::Register { player, password });
|
username: String,
|
||||||
|
password: String,
|
||||||
|
mut auth_trusted: impl FnMut(&str) -> bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
// Authentication
|
||||||
|
let token_or_username = self.server_info.auth_provider.as_ref().map(|addr|
|
||||||
|
// Query whether this is a trusted auth server
|
||||||
|
if auth_trusted(&addr) {
|
||||||
|
Ok(authc::AuthClient::new(addr)
|
||||||
|
.sign_in(&username, &password)?
|
||||||
|
.serialize())
|
||||||
|
} else {
|
||||||
|
Err(Error::AuthServerNotTrusted)
|
||||||
|
}
|
||||||
|
).unwrap_or(Ok(username))?;
|
||||||
|
|
||||||
|
self.postbox.send_message(ClientMsg::Register {
|
||||||
|
view_distance: self.view_distance,
|
||||||
|
token_or_username,
|
||||||
|
});
|
||||||
self.client_state = ClientState::Pending;
|
self.client_state = ClientState::Pending;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match self.postbox.next_message() {
|
match self.postbox.next_message()? {
|
||||||
Some(ServerMsg::StateAnswer(Err((RequestStateError::Denied, _)))) => {
|
ServerMsg::StateAnswer(Err((RequestStateError::RegisterDenied(err), state))) => {
|
||||||
break Err(Error::InvalidAuth);
|
self.client_state = state;
|
||||||
|
break Err(match err {
|
||||||
|
RegisterError::AlreadyLoggedIn => Error::AlreadyLoggedIn,
|
||||||
|
RegisterError::AuthError(err) => Error::AuthErr(err),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
Some(ServerMsg::StateAnswer(Ok(ClientState::Registered))) => break Ok(()),
|
ServerMsg::StateAnswer(Ok(ClientState::Registered)) => break Ok(()),
|
||||||
_ => {},
|
_ => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -195,10 +222,7 @@ impl Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Send disconnect message to the server
|
/// Send disconnect message to the server
|
||||||
pub fn request_logout(&mut self) {
|
pub fn request_logout(&mut self) { self.postbox.send_message(ClientMsg::Disconnect); }
|
||||||
self.postbox.send_message(ClientMsg::Disconnect);
|
|
||||||
self.client_state = ClientState::Pending;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Request a state transition to `ClientState::Registered` from an ingame
|
/// Request a state transition to `ClientState::Registered` from an ingame
|
||||||
/// state.
|
/// state.
|
||||||
@ -385,7 +409,7 @@ impl Client {
|
|||||||
// 3) Update client local data
|
// 3) Update client local data
|
||||||
|
|
||||||
// 4) Tick the client's LocalState
|
// 4) Tick the client's LocalState
|
||||||
self.state.tick(dt, add_foreign_systems);
|
self.state.tick(dt, add_foreign_systems, true);
|
||||||
|
|
||||||
// 5) Terrain
|
// 5) Terrain
|
||||||
let pos = self
|
let pos = self
|
||||||
@ -546,10 +570,8 @@ impl Client {
|
|||||||
if new_msgs.len() > 0 {
|
if new_msgs.len() > 0 {
|
||||||
for msg in new_msgs {
|
for msg in new_msgs {
|
||||||
match msg {
|
match msg {
|
||||||
ServerMsg::Error(e) => match e {
|
ServerMsg::TooManyPlayers => {
|
||||||
ServerError::TooManyPlayers => return Err(Error::ServerWentMad),
|
return Err(Error::ServerWentMad);
|
||||||
ServerError::InvalidAuth => return Err(Error::InvalidAuth),
|
|
||||||
//TODO: ServerError::InvalidAlias => return Err(Error::InvalidAlias),
|
|
||||||
},
|
},
|
||||||
ServerMsg::Shutdown => return Err(Error::ServerShutdown),
|
ServerMsg::Shutdown => return Err(Error::ServerShutdown),
|
||||||
ServerMsg::InitialSync { .. } => return Err(Error::ServerWentMad),
|
ServerMsg::InitialSync { .. } => return Err(Error::ServerWentMad),
|
||||||
@ -669,7 +691,19 @@ impl Client {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
ServerMsg::InventoryUpdate(inventory, event) => {
|
ServerMsg::InventoryUpdate(inventory, event) => {
|
||||||
|
match event {
|
||||||
|
InventoryUpdateEvent::CollectFailed => {
|
||||||
|
frontend_events.push(Event::Chat {
|
||||||
|
message: String::from(
|
||||||
|
"Failed to collect item. Your inventory may be full!",
|
||||||
|
),
|
||||||
|
chat_type: ChatType::Meta,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
self.state.write_component(self.entity, inventory);
|
self.state.write_component(self.entity, inventory);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
self.state
|
self.state
|
||||||
.ecs()
|
.ecs()
|
||||||
@ -692,10 +726,6 @@ impl Client {
|
|||||||
self.client_state = state;
|
self.client_state = state;
|
||||||
},
|
},
|
||||||
ServerMsg::StateAnswer(Err((error, state))) => {
|
ServerMsg::StateAnswer(Err((error, state))) => {
|
||||||
if error == RequestStateError::Denied {
|
|
||||||
warn!("Connection denied!");
|
|
||||||
return Err(Error::InvalidAuth);
|
|
||||||
}
|
|
||||||
warn!(
|
warn!(
|
||||||
"StateAnswer: {:?}. Server thinks client is in state {:?}.",
|
"StateAnswer: {:?}. Server thinks client is in state {:?}.",
|
||||||
error, state
|
error, state
|
||||||
@ -703,6 +733,7 @@ impl Client {
|
|||||||
},
|
},
|
||||||
ServerMsg::Disconnect => {
|
ServerMsg::Disconnect => {
|
||||||
frontend_events.push(Event::Disconnect);
|
frontend_events.push(Event::Disconnect);
|
||||||
|
self.postbox.send_message(ClientMsg::Terminate);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,9 +30,10 @@ hashbrown = { version = "0.6.2", features = ["rayon", "serde", "nightly"] }
|
|||||||
find_folder = "0.3.0"
|
find_folder = "0.3.0"
|
||||||
parking_lot = "0.9.0"
|
parking_lot = "0.9.0"
|
||||||
crossbeam = "=0.7.2"
|
crossbeam = "=0.7.2"
|
||||||
notify = "5.0.0-pre.1"
|
notify = "5.0.0-pre.2"
|
||||||
indexmap = "1.3.0"
|
indexmap = "1.3.0"
|
||||||
sum_type = "0.2.0"
|
sum_type = "0.2.0"
|
||||||
|
authc = { git = "https://gitlab.com/veloren/auth.git", rev = "65571ade0d954a0e0bd995fdb314854ff146ab97" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = "0.3"
|
criterion = "0.3"
|
||||||
|
@ -31,6 +31,7 @@ pub enum Species {
|
|||||||
Chicken = 1,
|
Chicken = 1,
|
||||||
Goose = 2,
|
Goose = 2,
|
||||||
Peacock = 3,
|
Peacock = 3,
|
||||||
|
Eagle = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data representing per-species generic data.
|
/// Data representing per-species generic data.
|
||||||
@ -42,6 +43,7 @@ pub struct AllSpecies<SpeciesMeta> {
|
|||||||
pub chicken: SpeciesMeta,
|
pub chicken: SpeciesMeta,
|
||||||
pub goose: SpeciesMeta,
|
pub goose: SpeciesMeta,
|
||||||
pub peacock: SpeciesMeta,
|
pub peacock: SpeciesMeta,
|
||||||
|
pub eagle: SpeciesMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, SpeciesMeta> core::ops::Index<&'a Species> for AllSpecies<SpeciesMeta> {
|
impl<'a, SpeciesMeta> core::ops::Index<&'a Species> for AllSpecies<SpeciesMeta> {
|
||||||
@ -54,15 +56,17 @@ impl<'a, SpeciesMeta> core::ops::Index<&'a Species> for AllSpecies<SpeciesMeta>
|
|||||||
Species::Chicken => &self.chicken,
|
Species::Chicken => &self.chicken,
|
||||||
Species::Goose => &self.goose,
|
Species::Goose => &self.goose,
|
||||||
Species::Peacock => &self.peacock,
|
Species::Peacock => &self.peacock,
|
||||||
|
Species::Eagle => &self.eagle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const ALL_SPECIES: [Species; 4] = [
|
pub const ALL_SPECIES: [Species; 5] = [
|
||||||
Species::Duck,
|
Species::Duck,
|
||||||
Species::Chicken,
|
Species::Chicken,
|
||||||
Species::Goose,
|
Species::Goose,
|
||||||
Species::Peacock,
|
Species::Peacock,
|
||||||
|
Species::Eagle,
|
||||||
];
|
];
|
||||||
|
|
||||||
impl<'a, SpeciesMeta: 'a> IntoIterator for &'a AllSpecies<SpeciesMeta> {
|
impl<'a, SpeciesMeta: 'a> IntoIterator for &'a AllSpecies<SpeciesMeta> {
|
||||||
|
@ -8,6 +8,9 @@ use specs::{Component, FlaggedStorage, HashMapStorage};
|
|||||||
use specs_idvs::IDVStorage;
|
use specs_idvs::IDVStorage;
|
||||||
use std::ops::Not;
|
use std::ops::Not;
|
||||||
|
|
||||||
|
// The limit on distance between the entity and a collectible (squared)
|
||||||
|
pub const MAX_PICKUP_RANGE_SQR: f32 = 64.0;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
pub struct Inventory {
|
pub struct Inventory {
|
||||||
pub slots: Vec<Option<Item>>,
|
pub slots: Vec<Option<Item>>,
|
||||||
@ -147,6 +150,7 @@ pub enum InventoryUpdateEvent {
|
|||||||
Swapped,
|
Swapped,
|
||||||
Dropped,
|
Dropped,
|
||||||
Collected,
|
Collected,
|
||||||
|
CollectFailed,
|
||||||
Possession,
|
Possession,
|
||||||
Debug,
|
Debug,
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ pub use energy::{Energy, EnergySource};
|
|||||||
pub use inputs::CanBuild;
|
pub use inputs::CanBuild;
|
||||||
pub use inventory::{
|
pub use inventory::{
|
||||||
item, Inventory, InventoryUpdate, InventoryUpdateEvent, Item, ItemKind, SwordKind, ToolData,
|
item, Inventory, InventoryUpdate, InventoryUpdateEvent, Item, ItemKind, SwordKind, ToolData,
|
||||||
ToolKind,
|
ToolKind, MAX_PICKUP_RANGE_SQR,
|
||||||
};
|
};
|
||||||
pub use last::Last;
|
pub use last::Last;
|
||||||
pub use location::{Waypoint, WaypointArea};
|
pub use location::{Waypoint, WaypointArea};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use authc::Uuid;
|
||||||
use specs::{Component, FlaggedStorage, NullStorage};
|
use specs::{Component, FlaggedStorage, NullStorage};
|
||||||
use specs_idvs::IDVStorage;
|
use specs_idvs::IDVStorage;
|
||||||
|
|
||||||
@ -7,20 +8,26 @@ const MAX_ALIAS_LEN: usize = 32;
|
|||||||
pub struct Player {
|
pub struct Player {
|
||||||
pub alias: String,
|
pub alias: String,
|
||||||
pub view_distance: Option<u32>,
|
pub view_distance: Option<u32>,
|
||||||
|
uuid: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
pub fn new(alias: String, view_distance: Option<u32>) -> Self {
|
pub fn new(alias: String, view_distance: Option<u32>, uuid: Uuid) -> Self {
|
||||||
Self {
|
Self {
|
||||||
alias,
|
alias,
|
||||||
view_distance,
|
view_distance,
|
||||||
|
uuid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_valid(&self) -> bool {
|
pub fn is_valid(&self) -> bool { Self::alias_is_valid(&self.alias) }
|
||||||
self.alias.chars().all(|c| c.is_alphanumeric() || c == '_')
|
|
||||||
&& self.alias.len() <= MAX_ALIAS_LEN
|
pub fn alias_is_valid(alias: &str) -> bool {
|
||||||
|
alias.chars().all(|c| c.is_alphanumeric() || c == '_') && alias.len() <= MAX_ALIAS_LEN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Not to be confused with uid
|
||||||
|
pub fn uuid(&self) -> Uuid { self.uuid }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Player {
|
impl Component for Player {
|
||||||
|
@ -138,7 +138,7 @@ impl Stats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Stats {
|
impl Stats {
|
||||||
pub fn new(name: String, body: Body, main: Option<comp::Item>) -> Self {
|
pub fn new(name: String, body: Body) -> Self {
|
||||||
let race = if let comp::Body::Humanoid(hbody) = body {
|
let race = if let comp::Body::Humanoid(hbody) = body {
|
||||||
Some(hbody.race)
|
Some(hbody.race)
|
||||||
} else {
|
} else {
|
||||||
|
@ -4,8 +4,8 @@ use vek::*;
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum ClientMsg {
|
pub enum ClientMsg {
|
||||||
Register {
|
Register {
|
||||||
player: comp::Player,
|
view_distance: Option<u32>,
|
||||||
password: String,
|
token_or_username: String,
|
||||||
},
|
},
|
||||||
Character {
|
Character {
|
||||||
name: String,
|
name: String,
|
||||||
@ -35,4 +35,5 @@ pub enum ClientMsg {
|
|||||||
key: Vec2<i32>,
|
key: Vec2<i32>,
|
||||||
},
|
},
|
||||||
Disconnect,
|
Disconnect,
|
||||||
|
Terminate,
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ pub mod server;
|
|||||||
pub use self::{
|
pub use self::{
|
||||||
client::ClientMsg,
|
client::ClientMsg,
|
||||||
ecs_packet::EcsCompPacket,
|
ecs_packet::EcsCompPacket,
|
||||||
server::{PlayerListUpdate, RequestStateError, ServerError, ServerInfo, ServerMsg},
|
server::{PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
|
@ -4,23 +4,17 @@ use crate::{
|
|||||||
terrain::{Block, TerrainChunk},
|
terrain::{Block, TerrainChunk},
|
||||||
ChatType,
|
ChatType,
|
||||||
};
|
};
|
||||||
|
use authc::AuthClientError;
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
||||||
pub enum RequestStateError {
|
|
||||||
Denied,
|
|
||||||
Already,
|
|
||||||
Impossible,
|
|
||||||
WrongMessage,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ServerInfo {
|
pub struct ServerInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub git_hash: String,
|
pub git_hash: String,
|
||||||
pub git_date: String,
|
pub git_date: String,
|
||||||
|
pub auth_provider: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@ -77,18 +71,31 @@ pub enum ServerMsg {
|
|||||||
chunk: Result<Box<TerrainChunk>, ()>,
|
chunk: Result<Box<TerrainChunk>, ()>,
|
||||||
},
|
},
|
||||||
TerrainBlockUpdates(HashMap<Vec3<i32>, Block>),
|
TerrainBlockUpdates(HashMap<Vec3<i32>, Block>),
|
||||||
Error(ServerError),
|
|
||||||
Disconnect,
|
Disconnect,
|
||||||
Shutdown,
|
Shutdown,
|
||||||
|
TooManyPlayers,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub enum ServerError {
|
pub enum RequestStateError {
|
||||||
TooManyPlayers,
|
RegisterDenied(RegisterError),
|
||||||
InvalidAuth,
|
Denied,
|
||||||
|
Already,
|
||||||
|
Impossible,
|
||||||
|
WrongMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum RegisterError {
|
||||||
|
AlreadyLoggedIn,
|
||||||
|
AuthError(String),
|
||||||
//TODO: InvalidAlias,
|
//TODO: InvalidAlias,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<AuthClientError> for RegisterError {
|
||||||
|
fn from(err: AuthClientError) -> Self { Self::AuthError(err.to_string()) }
|
||||||
|
}
|
||||||
|
|
||||||
impl ServerMsg {
|
impl ServerMsg {
|
||||||
pub fn chat(message: String) -> ServerMsg {
|
pub fn chat(message: String) -> ServerMsg {
|
||||||
ServerMsg::ChatMsg {
|
ServerMsg::ChatMsg {
|
||||||
|
@ -119,16 +119,16 @@ impl<S: PostMsg, R: PostMsg> PostBox<S, R> {
|
|||||||
|
|
||||||
pub fn send_message(&mut self, msg: S) { let _ = self.send_tx.send(msg); }
|
pub fn send_message(&mut self, msg: S) { let _ = self.send_tx.send(msg); }
|
||||||
|
|
||||||
pub fn next_message(&mut self) -> Option<R> {
|
pub fn next_message(&mut self) -> Result<R, Error> {
|
||||||
if self.error.is_some() {
|
if let Some(e) = self.error.clone() {
|
||||||
return None;
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.recv_rx.recv().ok()? {
|
match self.recv_rx.recv().map_err(|_| Error::ChannelFailure)? {
|
||||||
Ok(msg) => Some(msg),
|
Ok(msg) => Ok(msg),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.error = Some(e);
|
self.error = Some(e.clone());
|
||||||
None
|
Err(e)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -290,8 +290,35 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run RegionMap tick to update entity region occupancy
|
||||||
|
pub fn update_region_map(&self) {
|
||||||
|
self.ecs.write_resource::<RegionMap>().tick(
|
||||||
|
self.ecs.read_storage::<comp::Pos>(),
|
||||||
|
self.ecs.read_storage::<comp::Vel>(),
|
||||||
|
self.ecs.entities(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply terrain changes
|
||||||
|
pub fn apply_terrain_changes(&self) {
|
||||||
|
let mut terrain = self.ecs.write_resource::<TerrainGrid>();
|
||||||
|
let mut modified_blocks = std::mem::replace(
|
||||||
|
&mut self.ecs.write_resource::<BlockChange>().blocks,
|
||||||
|
Default::default(),
|
||||||
|
);
|
||||||
|
// Apply block modifications
|
||||||
|
// Only include in `TerrainChanges` if successful
|
||||||
|
modified_blocks.retain(|pos, block| terrain.set(*pos, *block).is_ok());
|
||||||
|
self.ecs.write_resource::<TerrainChanges>().modified_blocks = modified_blocks;
|
||||||
|
}
|
||||||
|
|
||||||
/// Execute a single tick, simulating the game state by the given duration.
|
/// Execute a single tick, simulating the game state by the given duration.
|
||||||
pub fn tick(&mut self, dt: Duration, add_foreign_systems: impl Fn(&mut DispatcherBuilder)) {
|
pub fn tick(
|
||||||
|
&mut self,
|
||||||
|
dt: Duration,
|
||||||
|
add_foreign_systems: impl Fn(&mut DispatcherBuilder),
|
||||||
|
update_terrain_and_regions: bool,
|
||||||
|
) {
|
||||||
// Change the time accordingly.
|
// Change the time accordingly.
|
||||||
self.ecs.write_resource::<TimeOfDay>().0 += dt.as_secs_f64() * DAY_CYCLE_FACTOR;
|
self.ecs.write_resource::<TimeOfDay>().0 += dt.as_secs_f64() * DAY_CYCLE_FACTOR;
|
||||||
self.ecs.write_resource::<Time>().0 += dt.as_secs_f64();
|
self.ecs.write_resource::<Time>().0 += dt.as_secs_f64();
|
||||||
@ -301,12 +328,9 @@ impl State {
|
|||||||
// important physics events.
|
// important physics events.
|
||||||
self.ecs.write_resource::<DeltaTime>().0 = dt.as_secs_f32().min(MAX_DELTA_TIME);
|
self.ecs.write_resource::<DeltaTime>().0 = dt.as_secs_f32().min(MAX_DELTA_TIME);
|
||||||
|
|
||||||
// Run RegionMap tick to update entity region occupancy
|
if update_terrain_and_regions {
|
||||||
self.ecs.write_resource::<RegionMap>().tick(
|
self.update_region_map();
|
||||||
self.ecs.read_storage::<comp::Pos>(),
|
}
|
||||||
self.ecs.read_storage::<comp::Vel>(),
|
|
||||||
self.ecs.entities(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Run systems to update the world.
|
// Run systems to update the world.
|
||||||
// Create and run a dispatcher for ecs systems.
|
// Create and run a dispatcher for ecs systems.
|
||||||
@ -319,16 +343,9 @@ impl State {
|
|||||||
|
|
||||||
self.ecs.maintain();
|
self.ecs.maintain();
|
||||||
|
|
||||||
// Apply terrain changes
|
if update_terrain_and_regions {
|
||||||
let mut terrain = self.ecs.write_resource::<TerrainGrid>();
|
self.apply_terrain_changes();
|
||||||
let mut modified_blocks = std::mem::replace(
|
}
|
||||||
&mut self.ecs.write_resource::<BlockChange>().blocks,
|
|
||||||
Default::default(),
|
|
||||||
);
|
|
||||||
// Apply block modifications
|
|
||||||
// Only include in `TerrainChanges` if successful
|
|
||||||
modified_blocks.retain(|pos, block| terrain.set(*pos, *block).is_ok());
|
|
||||||
self.ecs.write_resource::<TerrainChanges>().modified_blocks = modified_blocks;
|
|
||||||
|
|
||||||
// Process local events
|
// Process local events
|
||||||
let events = self.ecs.read_resource::<EventBus<LocalEvent>>().recv_all();
|
let events = self.ecs.read_resource::<EventBus<LocalEvent>>().recv_all();
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
pub const GIT_VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/githash"));
|
pub const GIT_VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/githash"));
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
pub static ref GIT_HASH: &'static str = include_str!(concat!(env!("OUT_DIR"), "/githash")).split("/").nth(0).expect("failed to retrieve git_hash!");
|
pub static ref GIT_HASH: &'static str = GIT_VERSION.split("/").nth(0).expect("failed to retrieve git_hash!");
|
||||||
pub static ref GIT_DATE: &'static str = include_str!(concat!(env!("OUT_DIR"), "/githash")).split("/").nth(1).expect("failed to retrieve git_date!");
|
pub static ref GIT_DATE: &'static str = GIT_VERSION.split("/").nth(1).expect("failed to retrieve git_date!");
|
||||||
}
|
}
|
||||||
|
|
||||||
use vek::{Mat3, Rgb, Rgba, Vec3};
|
use vek::{Mat3, Rgb, Rgba, Vec3};
|
||||||
|
@ -31,3 +31,4 @@ prometheus = "0.7"
|
|||||||
prometheus-static-metric = "0.2"
|
prometheus-static-metric = "0.2"
|
||||||
rouille = "3.0.0"
|
rouille = "3.0.0"
|
||||||
portpicker = { git = "https://github.com/wusyong/portpicker-rs", branch = "fix_ipv6" }
|
portpicker = { git = "https://github.com/wusyong/portpicker-rs", branch = "fix_ipv6" }
|
||||||
|
authc = { git = "https://gitlab.com/veloren/auth.git", rev = "65571ade0d954a0e0bd995fdb314854ff146ab97" }
|
||||||
|
@ -1,32 +1,88 @@
|
|||||||
|
use authc::{AuthClient, AuthToken, Uuid};
|
||||||
|
use common::msg::RegisterError;
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use log::{info, warn};
|
use log::error;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
fn derive_uuid(username: &str) -> Uuid {
|
||||||
|
let mut state: [u8; 16] = [
|
||||||
|
52, 17, 19, 239, 52, 17, 19, 239, 52, 17, 19, 239, 52, 17, 19, 239,
|
||||||
|
];
|
||||||
|
for mix_byte_1 in username.as_bytes() {
|
||||||
|
for i in 0..16 {
|
||||||
|
let mix_byte_step: u8 = mix_byte_1
|
||||||
|
.wrapping_pow(239)
|
||||||
|
.wrapping_mul((i as u8).wrapping_pow(43));
|
||||||
|
let mix_byte_2 = state[(i + mix_byte_step as usize) % 16];
|
||||||
|
let rot_step: u8 = mix_byte_1
|
||||||
|
.wrapping_pow(29)
|
||||||
|
.wrapping_mul((i as u8).wrapping_pow(163));
|
||||||
|
state[i] = (state[i] ^ mix_byte_1)
|
||||||
|
.wrapping_mul(mix_byte_2)
|
||||||
|
.rotate_left(rot_step as u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Uuid::from_slice(&state).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AuthProvider {
|
pub struct AuthProvider {
|
||||||
accounts: HashMap<String, String>,
|
accounts: HashMap<Uuid, String>,
|
||||||
|
auth_server: Option<AuthClient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthProvider {
|
impl AuthProvider {
|
||||||
pub fn new() -> Self {
|
pub fn new(auth_addr: Option<String>) -> Self {
|
||||||
|
let auth_server = match auth_addr {
|
||||||
|
Some(addr) => Some(AuthClient::new(addr)),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
AuthProvider {
|
AuthProvider {
|
||||||
accounts: HashMap::new(),
|
accounts: HashMap::new(),
|
||||||
|
auth_server,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn query(&mut self, username: String, password: String) -> bool {
|
pub fn logout(&mut self, uuid: Uuid) {
|
||||||
let pwd = password.clone();
|
if self.accounts.remove(&uuid).is_none() {
|
||||||
if self.accounts.entry(username.clone()).or_insert_with(|| {
|
error!("Attempted to logout user that is not logged in.");
|
||||||
info!("Registered new user '{}'", &username);
|
};
|
||||||
pwd
|
}
|
||||||
}) == &password
|
|
||||||
{
|
pub fn query(&mut self, username_or_token: String) -> Result<(String, Uuid), RegisterError> {
|
||||||
info!("User '{}' successfully authenticated", username);
|
// Based on whether auth server is provided or not we expect an username or
|
||||||
true
|
// token
|
||||||
|
match &self.auth_server {
|
||||||
|
// Token from auth server expected
|
||||||
|
Some(srv) => {
|
||||||
|
log::info!("Validating '{}' token.", &username_or_token);
|
||||||
|
// Parse token
|
||||||
|
let token = AuthToken::from_str(&username_or_token)
|
||||||
|
.map_err(|e| RegisterError::AuthError(e.to_string()))?;
|
||||||
|
// Validate token
|
||||||
|
let uuid = srv.validate(token)?;
|
||||||
|
// Check if already logged in
|
||||||
|
if self.accounts.contains_key(&uuid) {
|
||||||
|
return Err(RegisterError::AlreadyLoggedIn);
|
||||||
|
}
|
||||||
|
// Log in
|
||||||
|
let username = srv.uuid_to_username(uuid)?;
|
||||||
|
self.accounts.insert(uuid, username.clone());
|
||||||
|
Ok((username, uuid))
|
||||||
|
},
|
||||||
|
// Username is expected
|
||||||
|
None => {
|
||||||
|
// Assume username was provided
|
||||||
|
let username = username_or_token;
|
||||||
|
let uuid = derive_uuid(&username);
|
||||||
|
if !self.accounts.contains_key(&uuid) {
|
||||||
|
log::info!("New User '{}'", username);
|
||||||
|
self.accounts.insert(uuid, username.clone());
|
||||||
|
Ok((username, uuid))
|
||||||
} else {
|
} else {
|
||||||
warn!(
|
Err(RegisterError::AlreadyLoggedIn)
|
||||||
"User '{}' attempted to log in with invalid password '{}'!",
|
}
|
||||||
username, password
|
},
|
||||||
);
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -504,7 +504,7 @@ fn handle_spawn(server: &mut Server, entity: EcsEntity, args: String, action: &C
|
|||||||
.state
|
.state
|
||||||
.create_npc(
|
.create_npc(
|
||||||
pos,
|
pos,
|
||||||
comp::Stats::new(get_npc_name(id).into(), body, None),
|
comp::Stats::new(get_npc_name(id).into(), body),
|
||||||
body,
|
body,
|
||||||
)
|
)
|
||||||
.with(comp::Vel(vel))
|
.with(comp::Vel(vel))
|
||||||
@ -643,7 +643,7 @@ fn handle_object(server: &mut Server, entity: EcsEntity, args: String, _action:
|
|||||||
.read_storage::<comp::Ori>()
|
.read_storage::<comp::Ori>()
|
||||||
.get(entity)
|
.get(entity)
|
||||||
.copied();
|
.copied();
|
||||||
/*let builder = server
|
/*let builder = server.state
|
||||||
.create_object(pos, ori, obj_type)
|
.create_object(pos, ori, obj_type)
|
||||||
.with(ori);*/
|
.with(ori);*/
|
||||||
if let (Some(pos), Some(ori)) = (pos, ori) {
|
if let (Some(pos), Some(ori)) = (pos, ori) {
|
||||||
@ -705,6 +705,7 @@ fn handle_object(server: &mut Server, entity: EcsEntity, args: String, _action:
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
server
|
server
|
||||||
|
.state
|
||||||
.create_object(pos, obj_type)
|
.create_object(pos, obj_type)
|
||||||
.with(comp::Ori(
|
.with(comp::Ori(
|
||||||
// converts player orientation into a 90° rotation for the object by using the axis
|
// converts player orientation into a 90° rotation for the object by using the axis
|
||||||
|
@ -16,7 +16,7 @@ pub fn handle_create_character(
|
|||||||
let state = &mut server.state;
|
let state = &mut server.state;
|
||||||
let server_settings = &server.server_settings;
|
let server_settings = &server.server_settings;
|
||||||
|
|
||||||
Server::create_player_character(state, entity, name, body, main, server_settings);
|
state.create_player_character(entity, name, body, main, server_settings);
|
||||||
sys::subscription::initialize_region_subscription(state.ecs(), entity);
|
sys::subscription::initialize_region_subscription(state.ecs(), entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,8 +59,7 @@ pub fn handle_shoot(
|
|||||||
// TODO: Player height
|
// TODO: Player height
|
||||||
pos.z += 1.2;
|
pos.z += 1.2;
|
||||||
|
|
||||||
let mut builder =
|
let mut builder = state.create_projectile(Pos(pos), Vel(dir * 100.0), body, projectile);
|
||||||
Server::create_projectile(state, Pos(pos), Vel(dir * 100.0), body, projectile);
|
|
||||||
if let Some(light) = light {
|
if let Some(light) = light {
|
||||||
builder = builder.with(light)
|
builder = builder.with(light)
|
||||||
}
|
}
|
||||||
@ -73,6 +72,7 @@ pub fn handle_shoot(
|
|||||||
|
|
||||||
pub fn handle_create_waypoint(server: &mut Server, pos: Vec3<f32>) {
|
pub fn handle_create_waypoint(server: &mut Server, pos: Vec3<f32>) {
|
||||||
server
|
server
|
||||||
|
.state
|
||||||
.create_object(Pos(pos), comp::object::Body::CampfireLit)
|
.create_object(Pos(pos), comp::object::Body::CampfireLit)
|
||||||
.with(LightEmitter {
|
.with(LightEmitter {
|
||||||
offset: Vec3::unit_z() * 0.5,
|
offset: Vec3::unit_z() * 0.5,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::{Server, StateExt};
|
use crate::{Server, StateExt};
|
||||||
use common::{
|
use common::{
|
||||||
comp,
|
comp::{self, Pos, MAX_PICKUP_RANGE_SQR},
|
||||||
sync::WorldSyncExt,
|
sync::WorldSyncExt,
|
||||||
terrain::block::Block,
|
terrain::block::Block,
|
||||||
vol::{ReadVol, Vox},
|
vol::{ReadVol, Vox},
|
||||||
@ -16,7 +16,6 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
|
|
||||||
match manip {
|
match manip {
|
||||||
comp::InventoryManip::Pickup(uid) => {
|
comp::InventoryManip::Pickup(uid) => {
|
||||||
// TODO: enforce max pickup range
|
|
||||||
let item_entity = if let (Some((item, item_entity)), Some(inv)) = (
|
let item_entity = if let (Some((item, item_entity)), Some(inv)) = (
|
||||||
state
|
state
|
||||||
.ecs()
|
.ecs()
|
||||||
@ -33,7 +32,11 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
.write_storage::<comp::Inventory>()
|
.write_storage::<comp::Inventory>()
|
||||||
.get_mut(entity),
|
.get_mut(entity),
|
||||||
) {
|
) {
|
||||||
if inv.push(item).is_none() {
|
if within_pickup_range(
|
||||||
|
state.ecs().read_storage::<comp::Pos>().get(entity),
|
||||||
|
state.ecs().read_storage::<comp::Pos>().get(item_entity),
|
||||||
|
) && inv.push(item).is_none()
|
||||||
|
{
|
||||||
Some(item_entity)
|
Some(item_entity)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -56,20 +59,28 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
|
|
||||||
comp::InventoryManip::Collect(pos) => {
|
comp::InventoryManip::Collect(pos) => {
|
||||||
let block = state.terrain().get(pos).ok().copied();
|
let block = state.terrain().get(pos).ok().copied();
|
||||||
|
|
||||||
if let Some(block) = block {
|
if let Some(block) = block {
|
||||||
if block.is_collectible()
|
let has_inv_space = state
|
||||||
&& state
|
|
||||||
.ecs()
|
.ecs()
|
||||||
.read_storage::<comp::Inventory>()
|
.read_storage::<comp::Inventory>()
|
||||||
.get(entity)
|
.get(entity)
|
||||||
.map(|inv| !inv.is_full())
|
.map(|inv| !inv.is_full())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false);
|
||||||
&& state.try_set_block(pos, Block::empty()).is_some()
|
|
||||||
|
if !has_inv_space {
|
||||||
|
state.write_component(
|
||||||
|
entity,
|
||||||
|
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::CollectFailed),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if block.is_collectible() && state.try_set_block(pos, Block::empty()).is_some()
|
||||||
{
|
{
|
||||||
comp::Item::try_reclaim_from_block(block)
|
comp::Item::try_reclaim_from_block(block)
|
||||||
.map(|item| state.give_item(entity, item));
|
.map(|item| state.give_item(entity, item));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
comp::InventoryManip::Use(slot) => {
|
comp::InventoryManip::Use(slot) => {
|
||||||
@ -207,7 +218,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
+ Vec3::unit_z() * 10.0
|
+ Vec3::unit_z() * 10.0
|
||||||
+ Vec3::<f32>::zero().map(|_| rand::thread_rng().gen::<f32>() - 0.5) * 4.0;
|
+ Vec3::<f32>::zero().map(|_| rand::thread_rng().gen::<f32>() - 0.5) * 4.0;
|
||||||
|
|
||||||
server
|
state
|
||||||
.create_object(Default::default(), comp::object::Body::Pouch)
|
.create_object(Default::default(), comp::object::Body::Pouch)
|
||||||
.with(comp::Pos(pos.0 + Vec3::unit_z() * 0.25))
|
.with(comp::Pos(pos.0 + Vec3::unit_z() * 0.25))
|
||||||
.with(item)
|
.with(item)
|
||||||
@ -215,3 +226,39 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn within_pickup_range(player_position: Option<&Pos>, item_position: Option<&Pos>) -> bool {
|
||||||
|
match (player_position, item_position) {
|
||||||
|
(Some(ppos), Some(ipos)) => ppos.0.distance_squared(ipos.0) < MAX_PICKUP_RANGE_SQR,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use common::comp::Pos;
|
||||||
|
use vek::Vec3;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pickup_distance_within_range() {
|
||||||
|
let player_position = Pos(Vec3::zero());
|
||||||
|
let item_position = Pos(Vec3::one());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
within_pickup_range(Some(&player_position), Some(&item_position)),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pickup_distance_not_within_range() {
|
||||||
|
let player_position = Pos(Vec3::zero());
|
||||||
|
let item_position = Pos(Vec3::one() * 500.0);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
within_pickup_range(Some(&player_position), Some(&item_position)),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
use super::Event;
|
use super::Event;
|
||||||
use crate::{client::Client, Server, StateExt};
|
use crate::{auth_provider::AuthProvider, client::Client, state_ext::StateExt, Server};
|
||||||
use common::{
|
use common::{
|
||||||
comp,
|
comp,
|
||||||
|
comp::Player,
|
||||||
msg::{ClientState, PlayerListUpdate, ServerMsg},
|
msg::{ClientState, PlayerListUpdate, ServerMsg},
|
||||||
sync::{Uid, UidAllocator},
|
sync::{Uid, UidAllocator},
|
||||||
};
|
};
|
||||||
use log::error;
|
use log::error;
|
||||||
use specs::{saveload::MarkerAllocator, Builder, Entity as EcsEntity, WorldExt};
|
use specs::{saveload::MarkerAllocator, Builder, Entity as EcsEntity, Join, WorldExt};
|
||||||
|
|
||||||
pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) {
|
pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) {
|
||||||
let state = server.state_mut();
|
let state = server.state_mut();
|
||||||
@ -51,6 +52,22 @@ pub fn handle_client_disconnect(server: &mut Server, entity: EcsEntity) -> Event
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure to remove the player from the logged in list. (See AuthProvider)
|
||||||
|
// And send a disconnected message
|
||||||
|
{
|
||||||
|
let players = state.ecs().read_storage::<Player>();
|
||||||
|
let mut accounts = state.ecs().write_resource::<AuthProvider>();
|
||||||
|
let mut clients = state.ecs().write_storage::<Client>();
|
||||||
|
|
||||||
|
if let Some(player) = players.get(entity) {
|
||||||
|
accounts.logout(player.uuid());
|
||||||
|
|
||||||
|
let msg = ServerMsg::broadcast(format!("{} went offline.", &player.alias));
|
||||||
|
for client in (&mut clients).join().filter(|c| c.is_registered()) {
|
||||||
|
client.notify(msg.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Delete client entity
|
// Delete client entity
|
||||||
if let Err(err) = state.delete_entity_recorded(entity) {
|
if let Err(err) = state.delete_entity_recorded(entity) {
|
||||||
error!("Failed to delete disconnected client: {:?}", err);
|
error!("Failed to delete disconnected client: {:?}", err);
|
||||||
|
@ -10,6 +10,7 @@ pub mod events;
|
|||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod metrics;
|
pub mod metrics;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
pub mod state_ext;
|
||||||
pub mod sys;
|
pub mod sys;
|
||||||
#[cfg(not(feature = "worldgen"))] mod test_world;
|
#[cfg(not(feature = "worldgen"))] mod test_world;
|
||||||
|
|
||||||
@ -21,25 +22,22 @@ use crate::{
|
|||||||
chunk_generator::ChunkGenerator,
|
chunk_generator::ChunkGenerator,
|
||||||
client::{Client, RegionSubscription},
|
client::{Client, RegionSubscription},
|
||||||
cmd::CHAT_COMMANDS,
|
cmd::CHAT_COMMANDS,
|
||||||
|
state_ext::StateExt,
|
||||||
sys::sentinel::{DeletedEntities, TrackedComps},
|
sys::sentinel::{DeletedEntities, TrackedComps},
|
||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
assets, comp,
|
comp,
|
||||||
effect::Effect,
|
|
||||||
event::{EventBus, ServerEvent},
|
event::{EventBus, ServerEvent},
|
||||||
msg::{ClientMsg, ClientState, ServerError, ServerInfo, ServerMsg},
|
msg::{ClientMsg, ClientState, ServerInfo, ServerMsg},
|
||||||
net::PostOffice,
|
net::PostOffice,
|
||||||
state::{State, TimeOfDay},
|
state::{State, TimeOfDay},
|
||||||
sync::{Uid, WorldSyncExt},
|
sync::WorldSyncExt,
|
||||||
terrain::TerrainChunkSize,
|
terrain::TerrainChunkSize,
|
||||||
vol::{ReadVol, RectVolSize},
|
vol::{ReadVol, RectVolSize},
|
||||||
};
|
};
|
||||||
use log::{debug, error, warn};
|
use log::{debug, error};
|
||||||
use metrics::ServerMetrics;
|
use metrics::ServerMetrics;
|
||||||
use specs::{
|
use specs::{join::Join, Builder, Entity as EcsEntity, RunNow, SystemData, WorldExt};
|
||||||
join::Join, world::EntityBuilder as EcsEntityBuilder, Builder, Entity as EcsEntity, RunNow,
|
|
||||||
SystemData, WorldExt,
|
|
||||||
};
|
|
||||||
use std::{
|
use std::{
|
||||||
i32,
|
i32,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
@ -86,7 +84,9 @@ impl Server {
|
|||||||
let mut state = State::default();
|
let mut state = State::default();
|
||||||
state.ecs_mut().insert(EventBus::<ServerEvent>::default());
|
state.ecs_mut().insert(EventBus::<ServerEvent>::default());
|
||||||
// TODO: anything but this
|
// TODO: anything but this
|
||||||
state.ecs_mut().insert(AuthProvider::new());
|
state
|
||||||
|
.ecs_mut()
|
||||||
|
.insert(AuthProvider::new(settings.auth_server_address.clone()));
|
||||||
state.ecs_mut().insert(Tick(0));
|
state.ecs_mut().insert(Tick(0));
|
||||||
state.ecs_mut().insert(ChunkGenerator::new());
|
state.ecs_mut().insert(ChunkGenerator::new());
|
||||||
// System timers for performance monitoring
|
// System timers for performance monitoring
|
||||||
@ -96,6 +96,7 @@ impl Server {
|
|||||||
state.ecs_mut().insert(sys::SubscriptionTimer::default());
|
state.ecs_mut().insert(sys::SubscriptionTimer::default());
|
||||||
state.ecs_mut().insert(sys::TerrainSyncTimer::default());
|
state.ecs_mut().insert(sys::TerrainSyncTimer::default());
|
||||||
state.ecs_mut().insert(sys::TerrainTimer::default());
|
state.ecs_mut().insert(sys::TerrainTimer::default());
|
||||||
|
state.ecs_mut().insert(sys::WaypointTimer::default());
|
||||||
// Server-only components
|
// Server-only components
|
||||||
state.ecs_mut().register::<RegionSubscription>();
|
state.ecs_mut().register::<RegionSubscription>();
|
||||||
state.ecs_mut().register::<Client>();
|
state.ecs_mut().register::<Client>();
|
||||||
@ -196,6 +197,7 @@ impl Server {
|
|||||||
description: settings.server_description.clone(),
|
description: settings.server_description.clone(),
|
||||||
git_hash: common::util::GIT_HASH.to_string(),
|
git_hash: common::util::GIT_HASH.to_string(),
|
||||||
git_date: common::util::GIT_DATE.to_string(),
|
git_date: common::util::GIT_DATE.to_string(),
|
||||||
|
auth_provider: settings.auth_server_address.clone(),
|
||||||
},
|
},
|
||||||
metrics: ServerMetrics::new(settings.metrics_address)
|
metrics: ServerMetrics::new(settings.metrics_address)
|
||||||
.expect("Failed to initialize server metrics submodule."),
|
.expect("Failed to initialize server metrics submodule."),
|
||||||
@ -220,115 +222,6 @@ impl Server {
|
|||||||
/// Get a reference to the server's world.
|
/// Get a reference to the server's world.
|
||||||
pub fn world(&self) -> &World { &self.world }
|
pub fn world(&self) -> &World { &self.world }
|
||||||
|
|
||||||
/// Build a static object entity
|
|
||||||
pub fn create_object(
|
|
||||||
&mut self,
|
|
||||||
pos: comp::Pos,
|
|
||||||
object: comp::object::Body,
|
|
||||||
) -> EcsEntityBuilder {
|
|
||||||
self.state
|
|
||||||
.ecs_mut()
|
|
||||||
.create_entity_synced()
|
|
||||||
.with(pos)
|
|
||||||
.with(comp::Vel(Vec3::zero()))
|
|
||||||
.with(comp::Ori(Vec3::unit_y()))
|
|
||||||
.with(comp::Body::Object(object))
|
|
||||||
.with(comp::Mass(100.0))
|
|
||||||
.with(comp::Gravity(1.0))
|
|
||||||
//.with(comp::LightEmitter::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a projectile
|
|
||||||
pub fn create_projectile(
|
|
||||||
state: &mut State,
|
|
||||||
pos: comp::Pos,
|
|
||||||
vel: comp::Vel,
|
|
||||||
body: comp::Body,
|
|
||||||
projectile: comp::Projectile,
|
|
||||||
) -> EcsEntityBuilder {
|
|
||||||
state
|
|
||||||
.ecs_mut()
|
|
||||||
.create_entity_synced()
|
|
||||||
.with(pos)
|
|
||||||
.with(vel)
|
|
||||||
.with(comp::Ori(vel.0.normalized()))
|
|
||||||
.with(comp::Mass(0.0))
|
|
||||||
.with(body)
|
|
||||||
.with(projectile)
|
|
||||||
.with(comp::Sticky)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_player_character(
|
|
||||||
state: &mut State,
|
|
||||||
entity: EcsEntity,
|
|
||||||
name: String,
|
|
||||||
body: comp::Body,
|
|
||||||
main: Option<String>,
|
|
||||||
server_settings: &ServerSettings,
|
|
||||||
) {
|
|
||||||
// Give no item when an invalid specifier is given
|
|
||||||
let main = main.and_then(|specifier| assets::load_cloned(&specifier).ok());
|
|
||||||
|
|
||||||
let spawn_point = state.ecs().read_resource::<SpawnPoint>().0;
|
|
||||||
|
|
||||||
state.write_component(entity, body);
|
|
||||||
state.write_component(entity, comp::Stats::new(name, body, main.clone()));
|
|
||||||
state.write_component(entity, comp::Energy::new(1000));
|
|
||||||
state.write_component(entity, comp::Controller::default());
|
|
||||||
state.write_component(entity, comp::Pos(spawn_point));
|
|
||||||
state.write_component(entity, comp::Vel(Vec3::zero()));
|
|
||||||
state.write_component(entity, comp::Ori(Vec3::unit_y()));
|
|
||||||
state.write_component(entity, comp::Gravity(1.0));
|
|
||||||
state.write_component(entity, comp::CharacterState::default());
|
|
||||||
state.write_component(entity, comp::Alignment::Owned(entity));
|
|
||||||
state.write_component(entity, comp::Inventory::default());
|
|
||||||
state.write_component(
|
|
||||||
entity,
|
|
||||||
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::default()),
|
|
||||||
);
|
|
||||||
|
|
||||||
state.write_component(
|
|
||||||
entity,
|
|
||||||
if let Some(comp::ItemKind::Tool(tool)) = main.as_ref().map(|i| i.kind) {
|
|
||||||
let mut abilities = tool.get_abilities();
|
|
||||||
let mut ability_drain = abilities.drain(..);
|
|
||||||
comp::Loadout {
|
|
||||||
active_item: main.map(|item| comp::ItemConfig {
|
|
||||||
item,
|
|
||||||
primary_ability: ability_drain.next(),
|
|
||||||
secondary_ability: ability_drain.next(),
|
|
||||||
block_ability: Some(comp::CharacterAbility::BasicBlock),
|
|
||||||
dodge_ability: Some(comp::CharacterAbility::Roll),
|
|
||||||
}),
|
|
||||||
second_item: None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
comp::Loadout::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Make sure physics are accepted.
|
|
||||||
state.write_component(entity, comp::ForceUpdate);
|
|
||||||
|
|
||||||
// Give the Admin component to the player if their name exists in admin list
|
|
||||||
if server_settings.admins.contains(
|
|
||||||
&state
|
|
||||||
.ecs()
|
|
||||||
.read_storage::<comp::Player>()
|
|
||||||
.get(entity)
|
|
||||||
.expect("Failed to fetch entity.")
|
|
||||||
.alias,
|
|
||||||
) {
|
|
||||||
state.write_component(entity, comp::Admin);
|
|
||||||
}
|
|
||||||
// Tell the client its request was successful.
|
|
||||||
if let Some(client) = state.ecs().write_storage::<Client>().get_mut(entity) {
|
|
||||||
client.allow_state(ClientState::Character);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle events coming through via the event bus
|
|
||||||
|
|
||||||
/// Execute a single server tick, handle input and update the game state by
|
/// Execute a single server tick, handle input and update the game state by
|
||||||
/// the given duration.
|
/// the given duration.
|
||||||
pub fn tick(&mut self, _input: Input, dt: Duration) -> Result<Vec<Event>, Error> {
|
pub fn tick(&mut self, _input: Input, dt: Duration) -> Result<Vec<Event>, Error> {
|
||||||
@ -354,7 +247,6 @@ impl Server {
|
|||||||
// 8) Finish the tick, passing control of the main thread back
|
// 8) Finish the tick, passing control of the main thread back
|
||||||
// to the frontend
|
// to the frontend
|
||||||
|
|
||||||
let before_tick_1 = Instant::now();
|
|
||||||
// 1) Build up a list of events for this frame, to be passed to the frontend.
|
// 1) Build up a list of events for this frame, to be passed to the frontend.
|
||||||
let mut frontend_events = Vec::new();
|
let mut frontend_events = Vec::new();
|
||||||
|
|
||||||
@ -365,30 +257,49 @@ impl Server {
|
|||||||
|
|
||||||
// 2)
|
// 2)
|
||||||
|
|
||||||
|
let before_new_connections = Instant::now();
|
||||||
|
|
||||||
// 3) Handle inputs from clients
|
// 3) Handle inputs from clients
|
||||||
frontend_events.append(&mut self.handle_new_connections()?);
|
frontend_events.append(&mut self.handle_new_connections()?);
|
||||||
|
|
||||||
|
let before_message_system = Instant::now();
|
||||||
|
|
||||||
// Run message recieving sys before the systems in common for decreased latency
|
// Run message recieving sys before the systems in common for decreased latency
|
||||||
// (e.g. run before controller system)
|
// (e.g. run before controller system)
|
||||||
sys::message::Sys.run_now(&self.state.ecs());
|
sys::message::Sys.run_now(&self.state.ecs());
|
||||||
|
|
||||||
let before_tick_4 = Instant::now();
|
let before_state_tick = Instant::now();
|
||||||
|
|
||||||
// 4) Tick the server's LocalState.
|
// 4) Tick the server's LocalState.
|
||||||
self.state.tick(dt, sys::add_server_systems);
|
// 5) Fetch any generated `TerrainChunk`s and insert them into the terrain.
|
||||||
|
// in sys/terrain.rs
|
||||||
|
self.state.tick(dt, sys::add_server_systems, false);
|
||||||
|
|
||||||
let before_handle_events = Instant::now();
|
let before_handle_events = Instant::now();
|
||||||
|
|
||||||
// Handle game events
|
// Handle game events
|
||||||
frontend_events.append(&mut self.handle_events());
|
frontend_events.append(&mut self.handle_events());
|
||||||
|
|
||||||
|
let before_update_terrain_and_regions = Instant::now();
|
||||||
|
|
||||||
|
// Apply terrain changes and update the region map after processing server
|
||||||
|
// events so that changes made by server events will be immediately
|
||||||
|
// visble to client synchronization systems, minimizing the latency of
|
||||||
|
// `ServerEvent` mediated effects
|
||||||
|
self.state.update_region_map();
|
||||||
|
self.state.apply_terrain_changes();
|
||||||
|
|
||||||
|
let before_sync = Instant::now();
|
||||||
|
|
||||||
|
// 6) Synchronise clients with the new state of the world.
|
||||||
|
sys::run_sync_systems(self.state.ecs_mut());
|
||||||
|
|
||||||
|
let before_world_tick = Instant::now();
|
||||||
|
|
||||||
// Tick the world
|
// Tick the world
|
||||||
self.world.tick(dt);
|
self.world.tick(dt);
|
||||||
|
|
||||||
// 5) Fetch any generated `TerrainChunk`s and insert them into the terrain.
|
let before_entity_cleanup = Instant::now();
|
||||||
// in sys/terrain.rs
|
|
||||||
|
|
||||||
let before_tick_6 = Instant::now();
|
|
||||||
// 6) Synchronise clients with the new state of the world.
|
|
||||||
|
|
||||||
// Remove NPCs that are outside the view distances of all players
|
// Remove NPCs that are outside the view distances of all players
|
||||||
// This is done by removing NPCs in unloaded chunks
|
// This is done by removing NPCs in unloaded chunks
|
||||||
@ -410,8 +321,9 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let before_tick_7 = Instant::now();
|
let end_of_server_tick = Instant::now();
|
||||||
// 7) Update Metrics
|
// 7) Update Metrics
|
||||||
|
// Get system timing info
|
||||||
let entity_sync_nanos = self
|
let entity_sync_nanos = self
|
||||||
.state
|
.state
|
||||||
.ecs()
|
.ecs()
|
||||||
@ -430,31 +342,36 @@ impl Server {
|
|||||||
.read_resource::<sys::TerrainSyncTimer>()
|
.read_resource::<sys::TerrainSyncTimer>()
|
||||||
.nanos as i64;
|
.nanos as i64;
|
||||||
let terrain_nanos = self.state.ecs().read_resource::<sys::TerrainTimer>().nanos as i64;
|
let terrain_nanos = self.state.ecs().read_resource::<sys::TerrainTimer>().nanos as i64;
|
||||||
let total_sys_nanos = entity_sync_nanos
|
let waypoint_nanos = self.state.ecs().read_resource::<sys::WaypointTimer>().nanos as i64;
|
||||||
+ message_nanos
|
let total_sys_ran_in_dispatcher_nanos = terrain_nanos + waypoint_nanos;
|
||||||
+ sentinel_nanos
|
// Report timing info
|
||||||
+ subscription_nanos
|
|
||||||
+ terrain_sync_nanos
|
|
||||||
+ terrain_nanos;
|
|
||||||
self.metrics
|
self.metrics
|
||||||
.tick_time
|
.tick_time
|
||||||
.with_label_values(&["input"])
|
.with_label_values(&["new connections"])
|
||||||
.set((before_tick_4 - before_tick_1).as_nanos() as i64 - message_nanos);
|
.set((before_message_system - before_new_connections).as_nanos() as i64);
|
||||||
self.metrics
|
self.metrics
|
||||||
.tick_time
|
.tick_time
|
||||||
.with_label_values(&["state tick"])
|
.with_label_values(&["state tick"])
|
||||||
.set(
|
.set(
|
||||||
(before_handle_events - before_tick_4).as_nanos() as i64
|
(before_handle_events - before_state_tick).as_nanos() as i64
|
||||||
- (total_sys_nanos - message_nanos),
|
- total_sys_ran_in_dispatcher_nanos,
|
||||||
);
|
);
|
||||||
self.metrics
|
self.metrics
|
||||||
.tick_time
|
.tick_time
|
||||||
.with_label_values(&["handle server events"])
|
.with_label_values(&["handle server events"])
|
||||||
.set((before_tick_6 - before_handle_events).as_nanos() as i64);
|
.set((before_update_terrain_and_regions - before_handle_events).as_nanos() as i64);
|
||||||
self.metrics
|
self.metrics
|
||||||
.tick_time
|
.tick_time
|
||||||
.with_label_values(&["entity deletion"])
|
.with_label_values(&["update terrain and region map"])
|
||||||
.set((before_tick_7 - before_tick_6).as_nanos() as i64);
|
.set((before_sync - before_update_terrain_and_regions).as_nanos() as i64);
|
||||||
|
self.metrics
|
||||||
|
.tick_time
|
||||||
|
.with_label_values(&["world tick"])
|
||||||
|
.set((before_entity_cleanup - before_world_tick).as_nanos() as i64);
|
||||||
|
self.metrics
|
||||||
|
.tick_time
|
||||||
|
.with_label_values(&["entity cleanup"])
|
||||||
|
.set((end_of_server_tick - before_entity_cleanup).as_nanos() as i64);
|
||||||
self.metrics
|
self.metrics
|
||||||
.tick_time
|
.tick_time
|
||||||
.with_label_values(&["entity sync"])
|
.with_label_values(&["entity sync"])
|
||||||
@ -463,6 +380,10 @@ impl Server {
|
|||||||
.tick_time
|
.tick_time
|
||||||
.with_label_values(&["message"])
|
.with_label_values(&["message"])
|
||||||
.set(message_nanos);
|
.set(message_nanos);
|
||||||
|
self.metrics
|
||||||
|
.tick_time
|
||||||
|
.with_label_values(&["sentinel"])
|
||||||
|
.set(sentinel_nanos);
|
||||||
self.metrics
|
self.metrics
|
||||||
.tick_time
|
.tick_time
|
||||||
.with_label_values(&["subscription"])
|
.with_label_values(&["subscription"])
|
||||||
@ -475,6 +396,11 @@ impl Server {
|
|||||||
.tick_time
|
.tick_time
|
||||||
.with_label_values(&["terrain"])
|
.with_label_values(&["terrain"])
|
||||||
.set(terrain_nanos);
|
.set(terrain_nanos);
|
||||||
|
self.metrics
|
||||||
|
.tick_time
|
||||||
|
.with_label_values(&["waypoint"])
|
||||||
|
.set(waypoint_nanos);
|
||||||
|
// Report other info
|
||||||
self.metrics
|
self.metrics
|
||||||
.player_online
|
.player_online
|
||||||
.set(self.state.ecs().read_storage::<Client>().join().count() as i64);
|
.set(self.state.ecs().read_storage::<Client>().join().count() as i64);
|
||||||
@ -494,7 +420,7 @@ impl Server {
|
|||||||
self.metrics
|
self.metrics
|
||||||
.tick_time
|
.tick_time
|
||||||
.with_label_values(&["metrics"])
|
.with_label_values(&["metrics"])
|
||||||
.set(before_tick_7.elapsed().as_nanos() as i64);
|
.set(end_of_server_tick.elapsed().as_nanos() as i64);
|
||||||
|
|
||||||
// 8) Finish the tick, pass control back to the frontend.
|
// 8) Finish the tick, pass control back to the frontend.
|
||||||
|
|
||||||
@ -523,7 +449,7 @@ impl Server {
|
|||||||
<= self.state.ecs().read_storage::<Client>().join().count()
|
<= self.state.ecs().read_storage::<Client>().join().count()
|
||||||
{
|
{
|
||||||
// Note: in this case the client is dropped
|
// Note: in this case the client is dropped
|
||||||
client.notify(ServerMsg::Error(ServerError::TooManyPlayers));
|
client.notify(ServerMsg::TooManyPlayers);
|
||||||
} else {
|
} else {
|
||||||
let entity = self
|
let entity = self
|
||||||
.state
|
.state
|
||||||
@ -606,119 +532,3 @@ impl Server {
|
|||||||
impl Drop for Server {
|
impl Drop for Server {
|
||||||
fn drop(&mut self) { self.state.notify_registered_clients(ServerMsg::Shutdown); }
|
fn drop(&mut self) { self.state.notify_registered_clients(ServerMsg::Shutdown); }
|
||||||
}
|
}
|
||||||
|
|
||||||
trait StateExt {
|
|
||||||
fn give_item(&mut self, entity: EcsEntity, item: comp::Item) -> bool;
|
|
||||||
fn apply_effect(&mut self, entity: EcsEntity, effect: Effect);
|
|
||||||
fn notify_registered_clients(&self, msg: ServerMsg);
|
|
||||||
fn create_npc(
|
|
||||||
&mut self,
|
|
||||||
pos: comp::Pos,
|
|
||||||
stats: comp::Stats,
|
|
||||||
body: comp::Body,
|
|
||||||
) -> EcsEntityBuilder;
|
|
||||||
fn delete_entity_recorded(
|
|
||||||
&mut self,
|
|
||||||
entity: EcsEntity,
|
|
||||||
) -> Result<(), specs::error::WrongGeneration>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StateExt for State {
|
|
||||||
fn give_item(&mut self, entity: EcsEntity, item: comp::Item) -> bool {
|
|
||||||
let success = self
|
|
||||||
.ecs()
|
|
||||||
.write_storage::<comp::Inventory>()
|
|
||||||
.get_mut(entity)
|
|
||||||
.map(|inv| inv.push(item).is_none())
|
|
||||||
.unwrap_or(false);
|
|
||||||
if success {
|
|
||||||
self.write_component(
|
|
||||||
entity,
|
|
||||||
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Collected),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
success
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_effect(&mut self, entity: EcsEntity, effect: Effect) {
|
|
||||||
match effect {
|
|
||||||
Effect::Health(change) => {
|
|
||||||
self.ecs()
|
|
||||||
.write_storage::<comp::Stats>()
|
|
||||||
.get_mut(entity)
|
|
||||||
.map(|stats| stats.health.change_by(change));
|
|
||||||
},
|
|
||||||
Effect::Xp(xp) => {
|
|
||||||
self.ecs()
|
|
||||||
.write_storage::<comp::Stats>()
|
|
||||||
.get_mut(entity)
|
|
||||||
.map(|stats| stats.exp.change_by(xp));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a non-player character.
|
|
||||||
fn create_npc(
|
|
||||||
&mut self,
|
|
||||||
pos: comp::Pos,
|
|
||||||
stats: comp::Stats,
|
|
||||||
body: comp::Body,
|
|
||||||
) -> EcsEntityBuilder {
|
|
||||||
self.ecs_mut()
|
|
||||||
.create_entity_synced()
|
|
||||||
.with(pos)
|
|
||||||
.with(comp::Vel(Vec3::zero()))
|
|
||||||
.with(comp::Ori(Vec3::unit_y()))
|
|
||||||
.with(comp::Controller::default())
|
|
||||||
.with(body)
|
|
||||||
.with(stats)
|
|
||||||
.with(comp::Alignment::Npc)
|
|
||||||
.with(comp::Energy::new(500))
|
|
||||||
.with(comp::Gravity(1.0))
|
|
||||||
.with(comp::CharacterState::default())
|
|
||||||
.with(comp::Loadout::default()) // TODO Give the poor npc something to do
|
|
||||||
}
|
|
||||||
|
|
||||||
fn notify_registered_clients(&self, msg: ServerMsg) {
|
|
||||||
for client in (&mut self.ecs().write_storage::<Client>())
|
|
||||||
.join()
|
|
||||||
.filter(|c| c.is_registered())
|
|
||||||
{
|
|
||||||
client.notify(msg.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_entity_recorded(
|
|
||||||
&mut self,
|
|
||||||
entity: EcsEntity,
|
|
||||||
) -> Result<(), specs::error::WrongGeneration> {
|
|
||||||
let (maybe_uid, maybe_pos) = (
|
|
||||||
self.ecs().read_storage::<Uid>().get(entity).copied(),
|
|
||||||
self.ecs().read_storage::<comp::Pos>().get(entity).copied(),
|
|
||||||
);
|
|
||||||
let res = self.ecs_mut().delete_entity(entity);
|
|
||||||
if res.is_ok() {
|
|
||||||
if let (Some(uid), Some(pos)) = (maybe_uid, maybe_pos) {
|
|
||||||
if let Some(region_key) = self
|
|
||||||
.ecs()
|
|
||||||
.read_resource::<common::region::RegionMap>()
|
|
||||||
.find_region(entity, pos.0)
|
|
||||||
{
|
|
||||||
self.ecs()
|
|
||||||
.write_resource::<DeletedEntities>()
|
|
||||||
.record_deleted_entity(uid, region_key);
|
|
||||||
} else {
|
|
||||||
// Don't panic if the entity wasn't found in a region maybe it was just created
|
|
||||||
// and then deleted before the region manager had a chance to assign it a
|
|
||||||
// region
|
|
||||||
warn!(
|
|
||||||
"Failed to find region containing entity during entity deletion, assuming \
|
|
||||||
it wasn't sent to any clients and so deletion doesn't need to be \
|
|
||||||
recorded for sync purposes"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -10,12 +10,12 @@ const DEFAULT_WORLD_SEED: u32 = 5284;
|
|||||||
pub struct ServerSettings {
|
pub struct ServerSettings {
|
||||||
pub gameserver_address: SocketAddr,
|
pub gameserver_address: SocketAddr,
|
||||||
pub metrics_address: SocketAddr,
|
pub metrics_address: SocketAddr,
|
||||||
|
pub auth_server_address: Option<String>,
|
||||||
pub max_players: usize,
|
pub max_players: usize,
|
||||||
pub world_seed: u32,
|
pub world_seed: u32,
|
||||||
//pub pvp_enabled: bool,
|
//pub pvp_enabled: bool,
|
||||||
pub server_name: String,
|
pub server_name: String,
|
||||||
pub server_description: String,
|
pub server_description: String,
|
||||||
//pub login_server: whatever
|
|
||||||
pub start_time: f64,
|
pub start_time: f64,
|
||||||
pub admins: Vec<String>,
|
pub admins: Vec<String>,
|
||||||
/// When set to None, loads the default map file (if available); otherwise,
|
/// When set to None, loads the default map file (if available); otherwise,
|
||||||
@ -28,6 +28,7 @@ impl Default for ServerSettings {
|
|||||||
Self {
|
Self {
|
||||||
gameserver_address: SocketAddr::from(([0; 4], 14004)),
|
gameserver_address: SocketAddr::from(([0; 4], 14004)),
|
||||||
metrics_address: SocketAddr::from(([0; 4], 14005)),
|
metrics_address: SocketAddr::from(([0; 4], 14005)),
|
||||||
|
auth_server_address: Some("https://auth.veloren.net".into()),
|
||||||
world_seed: DEFAULT_WORLD_SEED,
|
world_seed: DEFAULT_WORLD_SEED,
|
||||||
server_name: "Veloren Alpha".to_owned(),
|
server_name: "Veloren Alpha".to_owned(),
|
||||||
server_description: "This is the best Veloren server.".to_owned(),
|
server_description: "This is the best Veloren server.".to_owned(),
|
||||||
@ -107,6 +108,7 @@ impl ServerSettings {
|
|||||||
[127, 0, 0, 1],
|
[127, 0, 0, 1],
|
||||||
pick_unused_port().expect("Failed to find unused port!"),
|
pick_unused_port().expect("Failed to find unused port!"),
|
||||||
)),
|
)),
|
||||||
|
auth_server_address: None,
|
||||||
// If loading the default map file, make sure the seed is also default.
|
// If loading the default map file, make sure the seed is also default.
|
||||||
world_seed: if load.map_file.is_some() {
|
world_seed: if load.map_file.is_some() {
|
||||||
load.world_seed
|
load.world_seed
|
||||||
|
244
server/src/state_ext.rs
Normal file
244
server/src/state_ext.rs
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
use crate::{client::Client, settings::ServerSettings, sys::sentinel::DeletedEntities, SpawnPoint};
|
||||||
|
use common::{
|
||||||
|
assets, comp,
|
||||||
|
effect::Effect,
|
||||||
|
msg::{ClientState, ServerMsg},
|
||||||
|
state::State,
|
||||||
|
sync::{Uid, WorldSyncExt},
|
||||||
|
};
|
||||||
|
use log::warn;
|
||||||
|
use specs::{Builder, Entity as EcsEntity, EntityBuilder as EcsEntityBuilder, Join, WorldExt};
|
||||||
|
use vek::*;
|
||||||
|
|
||||||
|
pub trait StateExt {
|
||||||
|
fn give_item(&mut self, entity: EcsEntity, item: comp::Item) -> bool;
|
||||||
|
fn apply_effect(&mut self, entity: EcsEntity, effect: Effect);
|
||||||
|
fn create_npc(
|
||||||
|
&mut self,
|
||||||
|
pos: comp::Pos,
|
||||||
|
stats: comp::Stats,
|
||||||
|
body: comp::Body,
|
||||||
|
) -> EcsEntityBuilder;
|
||||||
|
fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder;
|
||||||
|
fn create_projectile(
|
||||||
|
&mut self,
|
||||||
|
pos: comp::Pos,
|
||||||
|
vel: comp::Vel,
|
||||||
|
body: comp::Body,
|
||||||
|
projectile: comp::Projectile,
|
||||||
|
) -> EcsEntityBuilder;
|
||||||
|
fn create_player_character(
|
||||||
|
&mut self,
|
||||||
|
entity: EcsEntity,
|
||||||
|
name: String,
|
||||||
|
body: comp::Body,
|
||||||
|
main: Option<String>,
|
||||||
|
server_settings: &ServerSettings,
|
||||||
|
);
|
||||||
|
fn notify_registered_clients(&self, msg: ServerMsg);
|
||||||
|
fn delete_entity_recorded(
|
||||||
|
&mut self,
|
||||||
|
entity: EcsEntity,
|
||||||
|
) -> Result<(), specs::error::WrongGeneration>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StateExt for State {
|
||||||
|
fn give_item(&mut self, entity: EcsEntity, item: comp::Item) -> bool {
|
||||||
|
let success = self
|
||||||
|
.ecs()
|
||||||
|
.write_storage::<comp::Inventory>()
|
||||||
|
.get_mut(entity)
|
||||||
|
.map(|inv| inv.push(item).is_none())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if success {
|
||||||
|
self.write_component(
|
||||||
|
entity,
|
||||||
|
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Collected),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
success
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_effect(&mut self, entity: EcsEntity, effect: Effect) {
|
||||||
|
match effect {
|
||||||
|
Effect::Health(change) => {
|
||||||
|
self.ecs()
|
||||||
|
.write_storage::<comp::Stats>()
|
||||||
|
.get_mut(entity)
|
||||||
|
.map(|stats| stats.health.change_by(change));
|
||||||
|
},
|
||||||
|
Effect::Xp(xp) => {
|
||||||
|
self.ecs()
|
||||||
|
.write_storage::<comp::Stats>()
|
||||||
|
.get_mut(entity)
|
||||||
|
.map(|stats| stats.exp.change_by(xp));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a non-player character.
|
||||||
|
fn create_npc(
|
||||||
|
&mut self,
|
||||||
|
pos: comp::Pos,
|
||||||
|
stats: comp::Stats,
|
||||||
|
body: comp::Body,
|
||||||
|
) -> EcsEntityBuilder {
|
||||||
|
self.ecs_mut()
|
||||||
|
.create_entity_synced()
|
||||||
|
.with(pos)
|
||||||
|
.with(comp::Vel(Vec3::zero()))
|
||||||
|
.with(comp::Ori(Vec3::unit_y()))
|
||||||
|
.with(comp::Controller::default())
|
||||||
|
.with(body)
|
||||||
|
.with(stats)
|
||||||
|
.with(comp::Alignment::Npc)
|
||||||
|
.with(comp::Energy::new(500))
|
||||||
|
.with(comp::Gravity(1.0))
|
||||||
|
.with(comp::CharacterState::default())
|
||||||
|
.with(comp::Loadout::default()) // TODO Give the poor npc something to do
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a static object entity
|
||||||
|
fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder {
|
||||||
|
self.ecs_mut()
|
||||||
|
.create_entity_synced()
|
||||||
|
.with(pos)
|
||||||
|
.with(comp::Vel(Vec3::zero()))
|
||||||
|
.with(comp::Ori(Vec3::unit_y()))
|
||||||
|
.with(comp::Body::Object(object))
|
||||||
|
.with(comp::Mass(100.0))
|
||||||
|
.with(comp::Gravity(1.0))
|
||||||
|
//.with(comp::LightEmitter::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a projectile
|
||||||
|
fn create_projectile(
|
||||||
|
&mut self,
|
||||||
|
pos: comp::Pos,
|
||||||
|
vel: comp::Vel,
|
||||||
|
body: comp::Body,
|
||||||
|
projectile: comp::Projectile,
|
||||||
|
) -> EcsEntityBuilder {
|
||||||
|
self.ecs_mut()
|
||||||
|
.create_entity_synced()
|
||||||
|
.with(pos)
|
||||||
|
.with(vel)
|
||||||
|
.with(comp::Ori(vel.0.normalized()))
|
||||||
|
.with(comp::Mass(0.0))
|
||||||
|
.with(body)
|
||||||
|
.with(projectile)
|
||||||
|
.with(comp::Sticky)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_player_character(
|
||||||
|
&mut self,
|
||||||
|
entity: EcsEntity,
|
||||||
|
name: String,
|
||||||
|
body: comp::Body,
|
||||||
|
main: Option<String>,
|
||||||
|
server_settings: &ServerSettings,
|
||||||
|
) {
|
||||||
|
// Give no item when an invalid specifier is given
|
||||||
|
let main = main.and_then(|specifier| assets::load_cloned::<comp::Item>(&specifier).ok());
|
||||||
|
|
||||||
|
let spawn_point = self.ecs().read_resource::<SpawnPoint>().0;
|
||||||
|
|
||||||
|
self.write_component(entity, body);
|
||||||
|
self.write_component(entity, comp::Stats::new(name, body));
|
||||||
|
self.write_component(entity, comp::Energy::new(1000));
|
||||||
|
self.write_component(entity, comp::Controller::default());
|
||||||
|
self.write_component(entity, comp::Pos(spawn_point));
|
||||||
|
self.write_component(entity, comp::Vel(Vec3::zero()));
|
||||||
|
self.write_component(entity, comp::Ori(Vec3::unit_y()));
|
||||||
|
self.write_component(entity, comp::Gravity(1.0));
|
||||||
|
self.write_component(entity, comp::CharacterState::default());
|
||||||
|
self.write_component(entity, comp::Alignment::Owned(entity));
|
||||||
|
self.write_component(entity, comp::Inventory::default());
|
||||||
|
self.write_component(
|
||||||
|
entity,
|
||||||
|
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::default()),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.write_component(
|
||||||
|
entity,
|
||||||
|
if let Some(comp::ItemKind::Tool(tool)) = main.as_ref().map(|i| i.kind) {
|
||||||
|
let mut abilities = tool.get_abilities();
|
||||||
|
let mut ability_drain = abilities.drain(..);
|
||||||
|
comp::Loadout {
|
||||||
|
active_item: main.map(|item| comp::ItemConfig {
|
||||||
|
item,
|
||||||
|
primary_ability: ability_drain.next(),
|
||||||
|
secondary_ability: ability_drain.next(),
|
||||||
|
block_ability: Some(comp::CharacterAbility::BasicBlock),
|
||||||
|
dodge_ability: Some(comp::CharacterAbility::Roll),
|
||||||
|
}),
|
||||||
|
second_item: None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
comp::Loadout::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make sure physics are accepted.
|
||||||
|
self.write_component(entity, comp::ForceUpdate);
|
||||||
|
|
||||||
|
// Give the Admin component to the player if their name exists in admin list
|
||||||
|
if server_settings.admins.contains(
|
||||||
|
&self
|
||||||
|
.ecs()
|
||||||
|
.read_storage::<comp::Player>()
|
||||||
|
.get(entity)
|
||||||
|
.expect("Failed to fetch entity.")
|
||||||
|
.alias,
|
||||||
|
) {
|
||||||
|
self.write_component(entity, comp::Admin);
|
||||||
|
}
|
||||||
|
// Tell the client its request was successful.
|
||||||
|
if let Some(client) = self.ecs().write_storage::<Client>().get_mut(entity) {
|
||||||
|
client.allow_state(ClientState::Character);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_registered_clients(&self, msg: ServerMsg) {
|
||||||
|
for client in (&mut self.ecs().write_storage::<Client>())
|
||||||
|
.join()
|
||||||
|
.filter(|c| c.is_registered())
|
||||||
|
{
|
||||||
|
client.notify(msg.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_entity_recorded(
|
||||||
|
&mut self,
|
||||||
|
entity: EcsEntity,
|
||||||
|
) -> Result<(), specs::error::WrongGeneration> {
|
||||||
|
let (maybe_uid, maybe_pos) = (
|
||||||
|
self.ecs().read_storage::<Uid>().get(entity).copied(),
|
||||||
|
self.ecs().read_storage::<comp::Pos>().get(entity).copied(),
|
||||||
|
);
|
||||||
|
let res = self.ecs_mut().delete_entity(entity);
|
||||||
|
if res.is_ok() {
|
||||||
|
if let (Some(uid), Some(pos)) = (maybe_uid, maybe_pos) {
|
||||||
|
if let Some(region_key) = self
|
||||||
|
.ecs()
|
||||||
|
.read_resource::<common::region::RegionMap>()
|
||||||
|
.find_region(entity, pos.0)
|
||||||
|
{
|
||||||
|
self.ecs()
|
||||||
|
.write_resource::<DeletedEntities>()
|
||||||
|
.record_deleted_entity(uid, region_key);
|
||||||
|
} else {
|
||||||
|
// Don't panic if the entity wasn't found in a region maybe it was just created
|
||||||
|
// and then deleted before the region manager had a chance to assign it a
|
||||||
|
// region
|
||||||
|
warn!(
|
||||||
|
"Failed to find region containing entity during entity deletion, assuming \
|
||||||
|
it wasn't sent to any clients and so deletion doesn't need to be \
|
||||||
|
recorded for sync purposes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
use super::SysTimer;
|
use super::SysTimer;
|
||||||
use crate::{auth_provider::AuthProvider, client::Client, CLIENT_TIMEOUT};
|
use crate::{auth_provider::AuthProvider, client::Client, CLIENT_TIMEOUT};
|
||||||
use common::{
|
use common::{
|
||||||
comp::{Admin, Body, CanBuild, Controller, ForceUpdate, Ori, Player, Pos, Stats, Vel},
|
comp::{Admin, CanBuild, Controller, ForceUpdate, Ori, Player, Pos, Stats, Vel},
|
||||||
event::{EventBus, ServerEvent},
|
event::{EventBus, ServerEvent},
|
||||||
msg::{
|
msg::{
|
||||||
validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, PlayerListUpdate,
|
validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, PlayerListUpdate,
|
||||||
@ -27,7 +27,6 @@ impl<'a> System<'a> for Sys {
|
|||||||
ReadExpect<'a, TerrainGrid>,
|
ReadExpect<'a, TerrainGrid>,
|
||||||
Write<'a, SysTimer<Self>>,
|
Write<'a, SysTimer<Self>>,
|
||||||
ReadStorage<'a, Uid>,
|
ReadStorage<'a, Uid>,
|
||||||
ReadStorage<'a, Body>,
|
|
||||||
ReadStorage<'a, CanBuild>,
|
ReadStorage<'a, CanBuild>,
|
||||||
ReadStorage<'a, Admin>,
|
ReadStorage<'a, Admin>,
|
||||||
ReadStorage<'a, ForceUpdate>,
|
ReadStorage<'a, ForceUpdate>,
|
||||||
@ -51,7 +50,6 @@ impl<'a> System<'a> for Sys {
|
|||||||
terrain,
|
terrain,
|
||||||
mut timer,
|
mut timer,
|
||||||
uids,
|
uids,
|
||||||
bodies,
|
|
||||||
can_build,
|
can_build,
|
||||||
admins,
|
admins,
|
||||||
force_updates,
|
force_updates,
|
||||||
@ -81,7 +79,6 @@ impl<'a> System<'a> for Sys {
|
|||||||
let mut new_players = Vec::new();
|
let mut new_players = Vec::new();
|
||||||
|
|
||||||
for (entity, client) in (&entities, &mut clients).join() {
|
for (entity, client) in (&entities, &mut clients).join() {
|
||||||
let mut disconnect = false;
|
|
||||||
let new_msgs = client.postbox.new_messages();
|
let new_msgs = client.postbox.new_messages();
|
||||||
|
|
||||||
// Update client ping.
|
// Update client ping.
|
||||||
@ -91,7 +88,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
|| client.postbox.error().is_some()
|
|| client.postbox.error().is_some()
|
||||||
// Postbox error
|
// Postbox error
|
||||||
{
|
{
|
||||||
disconnect = true;
|
server_emitter.emit(ServerEvent::ClientDisconnect(entity));
|
||||||
} else if time - client.last_ping > CLIENT_TIMEOUT * 0.5 {
|
} else if time - client.last_ping > CLIENT_TIMEOUT * 0.5 {
|
||||||
// Try pinging the client if the timeout is nearing.
|
// Try pinging the client if the timeout is nearing.
|
||||||
client.postbox.send_message(ServerMsg::Ping);
|
client.postbox.send_message(ServerMsg::Ping);
|
||||||
@ -122,12 +119,27 @@ impl<'a> System<'a> for Sys {
|
|||||||
},
|
},
|
||||||
ClientState::Pending => {},
|
ClientState::Pending => {},
|
||||||
},
|
},
|
||||||
// Valid player
|
// Request registered state (login)
|
||||||
ClientMsg::Register { player, password } if player.is_valid() => {
|
ClientMsg::Register {
|
||||||
if !accounts.query(player.alias.clone(), password) {
|
view_distance,
|
||||||
client.error_state(RequestStateError::Denied);
|
token_or_username,
|
||||||
|
} => {
|
||||||
|
let (username, uuid) = match accounts.query(token_or_username.clone()) {
|
||||||
|
Err(err) => {
|
||||||
|
client.error_state(RequestStateError::RegisterDenied(err));
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Ok((username, uuid)) => (username, uuid),
|
||||||
|
};
|
||||||
|
|
||||||
|
let player = Player::new(username, view_distance, uuid);
|
||||||
|
|
||||||
|
if !player.is_valid() {
|
||||||
|
// Invalid player
|
||||||
|
client.error_state(RequestStateError::Impossible);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
match client.client_state {
|
match client.client_state {
|
||||||
ClientState::Connected => {
|
ClientState::Connected => {
|
||||||
// Add Player component to this client
|
// Add Player component to this client
|
||||||
@ -148,8 +160,6 @@ impl<'a> System<'a> for Sys {
|
|||||||
}
|
}
|
||||||
//client.allow_state(ClientState::Registered);
|
//client.allow_state(ClientState::Registered);
|
||||||
},
|
},
|
||||||
// Invalid player
|
|
||||||
ClientMsg::Register { .. } => client.error_state(RequestStateError::Impossible),
|
|
||||||
ClientMsg::SetViewDistance(view_distance) => match client.client_state {
|
ClientMsg::SetViewDistance(view_distance) => match client.client_state {
|
||||||
ClientState::Character { .. } => {
|
ClientState::Character { .. } => {
|
||||||
players
|
players
|
||||||
@ -272,25 +282,13 @@ impl<'a> System<'a> for Sys {
|
|||||||
ClientMsg::Ping => client.postbox.send_message(ServerMsg::Pong),
|
ClientMsg::Ping => client.postbox.send_message(ServerMsg::Pong),
|
||||||
ClientMsg::Pong => {},
|
ClientMsg::Pong => {},
|
||||||
ClientMsg::Disconnect => {
|
ClientMsg::Disconnect => {
|
||||||
disconnect = true;
|
client.postbox.send_message(ServerMsg::Disconnect);
|
||||||
|
},
|
||||||
|
ClientMsg::Terminate => {
|
||||||
|
server_emitter.emit(ServerEvent::ClientDisconnect(entity));
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if disconnect {
|
|
||||||
if let (Some(player), Some(_)) = (
|
|
||||||
players.get(entity),
|
|
||||||
// It only shows a message if you had a body (not in char selection)
|
|
||||||
bodies.get(entity),
|
|
||||||
) {
|
|
||||||
new_chat_msgs.push((
|
|
||||||
None,
|
|
||||||
ServerMsg::broadcast(format!("{} went offline.", &player.alias)),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
server_emitter.emit(ServerEvent::ClientDisconnect(entity));
|
|
||||||
client.postbox.send_message(ServerMsg::Disconnect);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle new players.
|
// Handle new players.
|
||||||
|
@ -15,30 +15,35 @@ pub type SentinelTimer = SysTimer<sentinel::Sys>;
|
|||||||
pub type SubscriptionTimer = SysTimer<subscription::Sys>;
|
pub type SubscriptionTimer = SysTimer<subscription::Sys>;
|
||||||
pub type TerrainTimer = SysTimer<terrain::Sys>;
|
pub type TerrainTimer = SysTimer<terrain::Sys>;
|
||||||
pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>;
|
pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>;
|
||||||
|
pub type WaypointTimer = SysTimer<waypoint::Sys>;
|
||||||
|
|
||||||
// System names
|
// System names
|
||||||
const ENTITY_SYNC_SYS: &str = "server_entity_sync_sys";
|
// Note: commented names may be useful in the future
|
||||||
const SENTINEL_SYS: &str = "sentinel_sys";
|
//const ENTITY_SYNC_SYS: &str = "server_entity_sync_sys";
|
||||||
const SUBSCRIPTION_SYS: &str = "server_subscription_sys";
|
//const SENTINEL_SYS: &str = "sentinel_sys";
|
||||||
const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys";
|
//const SUBSCRIPTION_SYS: &str = "server_subscription_sys";
|
||||||
|
//const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys";
|
||||||
const TERRAIN_SYS: &str = "server_terrain_sys";
|
const TERRAIN_SYS: &str = "server_terrain_sys";
|
||||||
const WAYPOINT_SYS: &str = "waypoint_sys";
|
const WAYPOINT_SYS: &str = "waypoint_sys";
|
||||||
|
|
||||||
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
|
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
|
||||||
// TODO: makes some of these dependent on systems in common like the phys system
|
dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]);
|
||||||
dispatch_builder.add(sentinel::Sys, SENTINEL_SYS, &[common::sys::PHYS_SYS]);
|
|
||||||
dispatch_builder.add(subscription::Sys, SUBSCRIPTION_SYS, &[
|
|
||||||
common::sys::PHYS_SYS,
|
|
||||||
]);
|
|
||||||
dispatch_builder.add(entity_sync::Sys, ENTITY_SYNC_SYS, &[
|
|
||||||
SUBSCRIPTION_SYS,
|
|
||||||
SENTINEL_SYS,
|
|
||||||
]);
|
|
||||||
dispatch_builder.add(terrain_sync::Sys, TERRAIN_SYS, &[]);
|
|
||||||
dispatch_builder.add(terrain::Sys, TERRAIN_SYNC_SYS, &[TERRAIN_SYS]);
|
|
||||||
dispatch_builder.add(waypoint::Sys, WAYPOINT_SYS, &[]);
|
dispatch_builder.add(waypoint::Sys, WAYPOINT_SYS, &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn run_sync_systems(ecs: &mut specs::World) {
|
||||||
|
use specs::RunNow;
|
||||||
|
|
||||||
|
// Setup for entity sync
|
||||||
|
// If I'm not mistaken, these two could be ran in parallel
|
||||||
|
sentinel::Sys.run_now(ecs);
|
||||||
|
subscription::Sys.run_now(ecs);
|
||||||
|
|
||||||
|
// Sync
|
||||||
|
terrain_sync::Sys.run_now(ecs);
|
||||||
|
entity_sync::Sys.run_now(ecs);
|
||||||
|
}
|
||||||
|
|
||||||
/// Used to keep track of how much time each system takes
|
/// Used to keep track of how much time each system takes
|
||||||
pub struct SysTimer<S> {
|
pub struct SysTimer<S> {
|
||||||
pub nanos: u64,
|
pub nanos: u64,
|
||||||
|
@ -180,7 +180,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
.choose(&mut rand::thread_rng())
|
.choose(&mut rand::thread_rng())
|
||||||
.expect("SPAWN_NPCS is nonempty")(
|
.expect("SPAWN_NPCS is nonempty")(
|
||||||
);
|
);
|
||||||
let mut stats = comp::Stats::new(name, body, main.clone());
|
let mut stats = comp::Stats::new(name, body);
|
||||||
let mut loadout = comp::Loadout {
|
let mut loadout = comp::Loadout {
|
||||||
active_item: main.map(|item| comp::ItemConfig {
|
active_item: main.map(|item| comp::ItemConfig {
|
||||||
item,
|
item,
|
||||||
@ -209,7 +209,6 @@ impl<'a> System<'a> for Sys {
|
|||||||
get_npc_name(&NPC_NAMES.humanoid, body_new.race)
|
get_npc_name(&NPC_NAMES.humanoid, body_new.race)
|
||||||
),
|
),
|
||||||
body,
|
body,
|
||||||
Some(assets::load_expect_cloned("common.items.weapons.hammer_1")),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
loadout = comp::Loadout {
|
loadout = comp::Loadout {
|
||||||
|
@ -8,12 +8,8 @@ use common::{
|
|||||||
};
|
};
|
||||||
use specs::{Join, Read, ReadExpect, ReadStorage, System, Write, WriteStorage};
|
use specs::{Join, Read, ReadExpect, ReadStorage, System, Write, WriteStorage};
|
||||||
|
|
||||||
/// This system will handle loading generated chunks and unloading
|
/// This systems sends new chunks to clients as well as changes to existing
|
||||||
/// uneeded chunks.
|
/// chunks
|
||||||
/// 1. Inserts newly generated chunks into the TerrainGrid
|
|
||||||
/// 2. Sends new chunks to neaby clients
|
|
||||||
/// 3. Handles the chunk's supplement (e.g. npcs)
|
|
||||||
/// 4. Removes chunks outside the range of players
|
|
||||||
pub struct Sys;
|
pub struct Sys;
|
||||||
impl<'a> System<'a> for Sys {
|
impl<'a> System<'a> for Sys {
|
||||||
type SystemData = (
|
type SystemData = (
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
use super::SysTimer;
|
||||||
use common::comp::{Player, Pos, Waypoint, WaypointArea};
|
use common::comp::{Player, Pos, Waypoint, WaypointArea};
|
||||||
use specs::{Entities, Join, ReadStorage, System, WriteStorage};
|
use specs::{Entities, Join, ReadStorage, System, Write, WriteStorage};
|
||||||
|
|
||||||
/// This system updates player waypoints
|
/// This system updates player waypoints
|
||||||
/// TODO: Make this faster by only considering local waypoints
|
/// TODO: Make this faster by only considering local waypoints
|
||||||
@ -11,12 +12,15 @@ impl<'a> System<'a> for Sys {
|
|||||||
ReadStorage<'a, Player>,
|
ReadStorage<'a, Player>,
|
||||||
ReadStorage<'a, WaypointArea>,
|
ReadStorage<'a, WaypointArea>,
|
||||||
WriteStorage<'a, Waypoint>,
|
WriteStorage<'a, Waypoint>,
|
||||||
|
Write<'a, SysTimer<Self>>,
|
||||||
);
|
);
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
&mut self,
|
&mut self,
|
||||||
(entities, positions, players, waypoint_areas, mut waypoints): Self::SystemData,
|
(entities, positions, players, waypoint_areas, mut waypoints, mut timer): Self::SystemData,
|
||||||
) {
|
) {
|
||||||
|
timer.start();
|
||||||
|
|
||||||
for (entity, player_pos, _) in (&entities, &positions, &players).join() {
|
for (entity, player_pos, _) in (&entities, &positions, &players).join() {
|
||||||
for (waypoint_pos, waypoint_area) in (&positions, &waypoint_areas).join() {
|
for (waypoint_pos, waypoint_area) in (&positions, &waypoint_areas).join() {
|
||||||
if player_pos.0.distance_squared(waypoint_pos.0) < waypoint_area.radius().powf(2.0)
|
if player_pos.0.distance_squared(waypoint_pos.0) < waypoint_area.radius().powf(2.0)
|
||||||
@ -25,5 +29,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timer.end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,8 +24,8 @@ gfx_device_gl = { version = "0.16.2", optional = true }
|
|||||||
gfx_window_glutin = "0.31.0"
|
gfx_window_glutin = "0.31.0"
|
||||||
glutin = "0.21.1"
|
glutin = "0.21.1"
|
||||||
winit = { version = "0.19.4", features = ["serde"] }
|
winit = { version = "0.19.4", features = ["serde"] }
|
||||||
conrod_core = { git = "https://gitlab.com/veloren/conrod.git" }
|
conrod_core = { git = "https://gitlab.com/veloren/conrod.git", branch = "hide_text" }
|
||||||
conrod_winit = { git = "https://gitlab.com/veloren/conrod.git" }
|
conrod_winit = { git = "https://gitlab.com/veloren/conrod.git", branch = "hide_text" }
|
||||||
euc = "0.3.0"
|
euc = "0.3.0"
|
||||||
|
|
||||||
# ECS
|
# ECS
|
||||||
@ -35,6 +35,9 @@ specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git" }
|
|||||||
# Mathematics
|
# Mathematics
|
||||||
vek = { version = "0.9.9", features = ["serde"] }
|
vek = { version = "0.9.9", features = ["serde"] }
|
||||||
|
|
||||||
|
# Controller
|
||||||
|
gilrs = { version = "0.7", features = ["serde"] }
|
||||||
|
|
||||||
# Singleplayer
|
# Singleplayer
|
||||||
server = { package = "veloren-server", path = "../server", optional = true }
|
server = { package = "veloren-server", path = "../server", optional = true }
|
||||||
|
|
||||||
@ -60,10 +63,10 @@ cpal = "0.10"
|
|||||||
crossbeam = "=0.7.2"
|
crossbeam = "=0.7.2"
|
||||||
hashbrown = { version = "0.6.2", features = ["rayon", "serde", "nightly"] }
|
hashbrown = { version = "0.6.2", features = ["rayon", "serde", "nightly"] }
|
||||||
chrono = "0.4.9"
|
chrono = "0.4.9"
|
||||||
rust-argon2 = "0.5"
|
|
||||||
bincode = "1.2"
|
bincode = "1.2"
|
||||||
deunicode = "1.0"
|
deunicode = "1.0"
|
||||||
uvth = "3.1.1"
|
uvth = "3.1.1"
|
||||||
|
authc = { git = "https://gitlab.com/veloren/auth.git", rev = "65571ade0d954a0e0bd995fdb314854ff146ab97" }
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
dispatch = "0.1.4"
|
dispatch = "0.1.4"
|
||||||
|
@ -103,30 +103,35 @@ impl<'a> From<&'a comp::bird_medium::Body> for SkeletonAttr {
|
|||||||
(Chicken, _) => (4.0, 3.0),
|
(Chicken, _) => (4.0, 3.0),
|
||||||
(Goose, _) => (5.0, 5.0),
|
(Goose, _) => (5.0, 5.0),
|
||||||
(Peacock, _) => (4.0, 7.0),
|
(Peacock, _) => (4.0, 7.0),
|
||||||
|
(Eagle, _) => (3.5, 5.0),
|
||||||
},
|
},
|
||||||
chest: match (body.species, body.body_type) {
|
chest: match (body.species, body.body_type) {
|
||||||
(Duck, _) => (0.0, 5.0),
|
(Duck, _) => (0.0, 5.0),
|
||||||
(Chicken, _) => (0.0, 5.0),
|
(Chicken, _) => (0.0, 5.0),
|
||||||
(Goose, _) => (0.0, 8.0),
|
(Goose, _) => (0.0, 8.0),
|
||||||
(Peacock, _) => (0.0, 10.0),
|
(Peacock, _) => (0.0, 10.0),
|
||||||
|
(Eagle, _) => (0.0, 8.0),
|
||||||
},
|
},
|
||||||
tail: match (body.species, body.body_type) {
|
tail: match (body.species, body.body_type) {
|
||||||
(Duck, _) => (-3.0, 1.5),
|
(Duck, _) => (-3.0, 1.5),
|
||||||
(Chicken, _) => (-3.0, 1.5),
|
(Chicken, _) => (-3.0, 1.5),
|
||||||
(Goose, _) => (-5.0, 3.0),
|
(Goose, _) => (-5.0, 3.0),
|
||||||
(Peacock, _) => (-5.5, 2.0),
|
(Peacock, _) => (-5.5, 2.0),
|
||||||
|
(Eagle, _) => (-8.0, -4.0),
|
||||||
},
|
},
|
||||||
wing: match (body.species, body.body_type) {
|
wing: match (body.species, body.body_type) {
|
||||||
(Duck, _) => (2.75, 0.0, 6.0),
|
(Duck, _) => (2.75, 0.0, 6.0),
|
||||||
(Chicken, _) => (2.75, 0.0, 6.0),
|
(Chicken, _) => (2.75, 0.0, 6.0),
|
||||||
(Goose, _) => (3.75, -1.0, 9.0),
|
(Goose, _) => (3.75, -1.0, 9.0),
|
||||||
(Peacock, _) => (3.0, 0.0, 9.0),
|
(Peacock, _) => (3.0, 0.0, 9.0),
|
||||||
|
(Eagle, _) => (3.0, -8.0, 5.0),
|
||||||
},
|
},
|
||||||
foot: match (body.species, body.body_type) {
|
foot: match (body.species, body.body_type) {
|
||||||
(Duck, _) => (2.0, -1.5, 4.0),
|
(Duck, _) => (2.0, -1.5, 4.0),
|
||||||
(Chicken, _) => (2.0, -1.5, 4.0),
|
(Chicken, _) => (2.0, -1.5, 4.0),
|
||||||
(Goose, _) => (2.0, -1.5, 7.0),
|
(Goose, _) => (2.0, -1.5, 7.0),
|
||||||
(Peacock, _) => (2.0, -2.5, 8.0),
|
(Peacock, _) => (2.0, -2.5, 8.0),
|
||||||
|
(Eagle, _) => (2.0, -2.0, 8.0),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
329
voxygen/src/controller.rs
Normal file
329
voxygen/src/controller.rs
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
//! Module containing controller-specific abstractions allowing complex
|
||||||
|
//! keybindings
|
||||||
|
|
||||||
|
use crate::window::{GameInput, MenuInput};
|
||||||
|
use gilrs::{ev::Code as GilCode, Axis as GilAxis, Button as GilButton};
|
||||||
|
use hashbrown::HashMap;
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Contains all controller related settings and keymaps
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ControllerSettings {
|
||||||
|
pub game_button_map: HashMap<Button, Vec<GameInput>>,
|
||||||
|
pub menu_button_map: HashMap<Button, Vec<MenuInput>>,
|
||||||
|
pub game_analog_button_map: HashMap<AnalogButton, Vec<AnalogButtonGameAction>>,
|
||||||
|
pub menu_analog_button_map: HashMap<AnalogButton, Vec<AnalogButtonMenuAction>>,
|
||||||
|
pub game_axis_map: HashMap<Axis, Vec<AxisGameAction>>,
|
||||||
|
pub menu_axis_map: HashMap<Axis, Vec<AxisMenuAction>>,
|
||||||
|
pub pan_sensitivity: u32,
|
||||||
|
pub axis_deadzones: HashMap<Axis, f32>,
|
||||||
|
pub button_deadzones: HashMap<AnalogButton, f32>,
|
||||||
|
pub mouse_emulation_sensitivity: u32,
|
||||||
|
pub inverted_axes: Vec<Axis>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ControllerSettings {
|
||||||
|
pub fn apply_axis_deadzone(&self, k: &Axis, input: f32) -> f32 {
|
||||||
|
let threshold = *self.axis_deadzones.get(k).unwrap_or(&0.2);
|
||||||
|
|
||||||
|
// This could be one comparison per handled event faster if threshold was
|
||||||
|
// guaranteed to fall into <0, 1) range
|
||||||
|
let input_abs = input.abs();
|
||||||
|
if input_abs <= threshold || threshold >= 1.0 {
|
||||||
|
0.0
|
||||||
|
} else if threshold <= 0.0 {
|
||||||
|
input
|
||||||
|
} else {
|
||||||
|
(input_abs - threshold) / (1.0 - threshold) * input.signum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_button_deadzone(&self, k: &AnalogButton, input: f32) -> f32 {
|
||||||
|
let threshold = *self.button_deadzones.get(k).unwrap_or(&0.2);
|
||||||
|
|
||||||
|
// This could be one comparison per handled event faster if threshold was
|
||||||
|
// guaranteed to fall into <0, 1) range
|
||||||
|
if input <= threshold || threshold >= 1.0 {
|
||||||
|
0.0
|
||||||
|
} else if threshold <= 0.0 {
|
||||||
|
input
|
||||||
|
} else {
|
||||||
|
(input - threshold) / (1.0 - threshold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&crate::settings::GamepadSettings> for ControllerSettings {
|
||||||
|
fn from(settings: &crate::settings::GamepadSettings) -> Self {
|
||||||
|
Self {
|
||||||
|
game_button_map: {
|
||||||
|
let mut map: HashMap<_, Vec<_>> = HashMap::new();
|
||||||
|
map.entry(settings.game_buttons.primary)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Primary);
|
||||||
|
map.entry(settings.game_buttons.secondary)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Secondary);
|
||||||
|
map.entry(settings.game_buttons.toggle_cursor)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::ToggleCursor);
|
||||||
|
map.entry(settings.game_buttons.escape)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Escape);
|
||||||
|
map.entry(settings.game_buttons.enter)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Enter);
|
||||||
|
map.entry(settings.game_buttons.command)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Command);
|
||||||
|
map.entry(settings.game_buttons.move_forward)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::MoveForward);
|
||||||
|
map.entry(settings.game_buttons.move_left)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::MoveLeft);
|
||||||
|
map.entry(settings.game_buttons.move_back)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::MoveBack);
|
||||||
|
map.entry(settings.game_buttons.move_right)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::MoveRight);
|
||||||
|
map.entry(settings.game_buttons.jump)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Jump);
|
||||||
|
map.entry(settings.game_buttons.sit)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Sit);
|
||||||
|
map.entry(settings.game_buttons.glide)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Glide);
|
||||||
|
map.entry(settings.game_buttons.climb)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Climb);
|
||||||
|
map.entry(settings.game_buttons.climb_down)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::ClimbDown);
|
||||||
|
map.entry(settings.game_buttons.wall_leap)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::WallLeap);
|
||||||
|
map.entry(settings.game_buttons.mount)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Mount);
|
||||||
|
map.entry(settings.game_buttons.map)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Map);
|
||||||
|
map.entry(settings.game_buttons.bag)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Bag);
|
||||||
|
map.entry(settings.game_buttons.quest_log)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::QuestLog);
|
||||||
|
map.entry(settings.game_buttons.character_window)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::CharacterWindow);
|
||||||
|
map.entry(settings.game_buttons.social)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Social);
|
||||||
|
map.entry(settings.game_buttons.spellbook)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Spellbook);
|
||||||
|
map.entry(settings.game_buttons.settings)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Settings);
|
||||||
|
map.entry(settings.game_buttons.help)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Help);
|
||||||
|
map.entry(settings.game_buttons.toggle_interface)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::ToggleInterface);
|
||||||
|
map.entry(settings.game_buttons.toggle_debug)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::ToggleDebug);
|
||||||
|
map.entry(settings.game_buttons.fullscreen)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Fullscreen);
|
||||||
|
map.entry(settings.game_buttons.screenshot)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Screenshot);
|
||||||
|
map.entry(settings.game_buttons.toggle_ingame_ui)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::ToggleIngameUi);
|
||||||
|
map.entry(settings.game_buttons.roll)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Roll);
|
||||||
|
map.entry(settings.game_buttons.respawn)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Respawn);
|
||||||
|
map.entry(settings.game_buttons.interact)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Interact);
|
||||||
|
map.entry(settings.game_buttons.toggle_wield)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::ToggleWield);
|
||||||
|
map.entry(settings.game_buttons.charge)
|
||||||
|
.or_default()
|
||||||
|
.push(GameInput::Charge);
|
||||||
|
map
|
||||||
|
},
|
||||||
|
menu_button_map: {
|
||||||
|
let mut map: HashMap<_, Vec<_>> = HashMap::new();
|
||||||
|
map.entry(settings.menu_buttons.up)
|
||||||
|
.or_default()
|
||||||
|
.push(MenuInput::Up);
|
||||||
|
map.entry(settings.menu_buttons.down)
|
||||||
|
.or_default()
|
||||||
|
.push(MenuInput::Down);
|
||||||
|
map.entry(settings.menu_buttons.left)
|
||||||
|
.or_default()
|
||||||
|
.push(MenuInput::Left);
|
||||||
|
map.entry(settings.menu_buttons.right)
|
||||||
|
.or_default()
|
||||||
|
.push(MenuInput::Right);
|
||||||
|
map.entry(settings.menu_buttons.scroll_up)
|
||||||
|
.or_default()
|
||||||
|
.push(MenuInput::ScrollUp);
|
||||||
|
map.entry(settings.menu_buttons.scroll_down)
|
||||||
|
.or_default()
|
||||||
|
.push(MenuInput::ScrollDown);
|
||||||
|
map.entry(settings.menu_buttons.scroll_left)
|
||||||
|
.or_default()
|
||||||
|
.push(MenuInput::ScrollLeft);
|
||||||
|
map.entry(settings.menu_buttons.scroll_right)
|
||||||
|
.or_default()
|
||||||
|
.push(MenuInput::ScrollRight);
|
||||||
|
map.entry(settings.menu_buttons.home)
|
||||||
|
.or_default()
|
||||||
|
.push(MenuInput::Home);
|
||||||
|
map.entry(settings.menu_buttons.end)
|
||||||
|
.or_default()
|
||||||
|
.push(MenuInput::End);
|
||||||
|
map.entry(settings.menu_buttons.apply)
|
||||||
|
.or_default()
|
||||||
|
.push(MenuInput::Apply);
|
||||||
|
map.entry(settings.menu_buttons.back)
|
||||||
|
.or_default()
|
||||||
|
.push(MenuInput::Back);
|
||||||
|
map.entry(settings.menu_buttons.exit)
|
||||||
|
.or_default()
|
||||||
|
.push(MenuInput::Exit);
|
||||||
|
map
|
||||||
|
},
|
||||||
|
game_analog_button_map: HashMap::new(),
|
||||||
|
menu_analog_button_map: HashMap::new(),
|
||||||
|
game_axis_map: {
|
||||||
|
let mut map: HashMap<_, Vec<_>> = HashMap::new();
|
||||||
|
map.entry(settings.game_axis.movement_x)
|
||||||
|
.or_default()
|
||||||
|
.push(AxisGameAction::MovementX);
|
||||||
|
map.entry(settings.game_axis.movement_y)
|
||||||
|
.or_default()
|
||||||
|
.push(AxisGameAction::MovementY);
|
||||||
|
map.entry(settings.game_axis.camera_x)
|
||||||
|
.or_default()
|
||||||
|
.push(AxisGameAction::CameraX);
|
||||||
|
map.entry(settings.game_axis.camera_y)
|
||||||
|
.or_default()
|
||||||
|
.push(AxisGameAction::CameraY);
|
||||||
|
map
|
||||||
|
},
|
||||||
|
menu_axis_map: {
|
||||||
|
let mut map: HashMap<_, Vec<_>> = HashMap::new();
|
||||||
|
map.entry(settings.menu_axis.move_x)
|
||||||
|
.or_default()
|
||||||
|
.push(AxisMenuAction::MoveX);
|
||||||
|
map.entry(settings.menu_axis.move_y)
|
||||||
|
.or_default()
|
||||||
|
.push(AxisMenuAction::MoveY);
|
||||||
|
map.entry(settings.menu_axis.scroll_x)
|
||||||
|
.or_default()
|
||||||
|
.push(AxisMenuAction::ScrollX);
|
||||||
|
map.entry(settings.menu_axis.scroll_y)
|
||||||
|
.or_default()
|
||||||
|
.push(AxisMenuAction::ScrollY);
|
||||||
|
map
|
||||||
|
},
|
||||||
|
pan_sensitivity: settings.pan_sensitivity,
|
||||||
|
axis_deadzones: settings.axis_deadzones.clone(),
|
||||||
|
button_deadzones: settings.button_deadzones.clone(),
|
||||||
|
mouse_emulation_sensitivity: settings.mouse_emulation_sensitivity,
|
||||||
|
inverted_axes: settings.inverted_axes.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All the menu actions you can bind to an Axis
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||||
|
pub enum AxisMenuAction {
|
||||||
|
MoveX,
|
||||||
|
MoveY,
|
||||||
|
ScrollX,
|
||||||
|
ScrollY,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All the game actions you can bind to an Axis
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||||
|
pub enum AxisGameAction {
|
||||||
|
MovementX,
|
||||||
|
MovementY,
|
||||||
|
CameraX,
|
||||||
|
CameraY,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All the menu actions you can bind to an analog button
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||||
|
pub enum AnalogButtonMenuAction {}
|
||||||
|
|
||||||
|
/// All the game actions you can bind to an analog button
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||||
|
pub enum AnalogButtonGameAction {}
|
||||||
|
|
||||||
|
/// Button::Simple(GilButton::Unknown) is invalid and equal to mapping an action
|
||||||
|
/// to nothing
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||||
|
pub enum Button {
|
||||||
|
Simple(GilButton),
|
||||||
|
EventCode(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AnalogButton::Simple(GilButton::Unknown) is invalid and equal to mapping an
|
||||||
|
/// action to nothing
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||||
|
pub enum AnalogButton {
|
||||||
|
Simple(GilButton),
|
||||||
|
EventCode(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Axis::Simple(GilAxis::Unknown) is invalid and equal to mapping an action to
|
||||||
|
/// nothing
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||||
|
pub enum Axis {
|
||||||
|
Simple(GilAxis),
|
||||||
|
EventCode(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(GilAxis, GilCode)> for Axis {
|
||||||
|
fn from((axis, code): (GilAxis, GilCode)) -> Self {
|
||||||
|
match axis {
|
||||||
|
GilAxis::Unknown => Self::EventCode(code.into_u32()),
|
||||||
|
_ => Self::Simple(axis),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(GilButton, GilCode)> for Button {
|
||||||
|
fn from((button, code): (GilButton, GilCode)) -> Self {
|
||||||
|
match button {
|
||||||
|
GilButton::Unknown => Self::EventCode(code.into_u32()),
|
||||||
|
_ => Self::Simple(button),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(GilButton, GilCode)> for AnalogButton {
|
||||||
|
fn from((button, code): (GilButton, GilCode)) -> Self {
|
||||||
|
match button {
|
||||||
|
GilButton::Unknown => Self::EventCode(code.into_u32()),
|
||||||
|
_ => Self::Simple(button),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ pub struct KeyState {
|
|||||||
pub left: bool,
|
pub left: bool,
|
||||||
pub up: bool,
|
pub up: bool,
|
||||||
pub down: bool,
|
pub down: bool,
|
||||||
|
pub analog_matrix: Vec2<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyState {
|
impl KeyState {
|
||||||
@ -14,16 +15,21 @@ impl KeyState {
|
|||||||
left: false,
|
left: false,
|
||||||
up: false,
|
up: false,
|
||||||
down: false,
|
down: false,
|
||||||
|
analog_matrix: Vec2::zero(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dir_vec(&self) -> Vec2<f32> {
|
pub fn dir_vec(&self) -> Vec2<f32> {
|
||||||
let dir = Vec2::<f32>::new(
|
let dir = if self.analog_matrix == Vec2::zero() {
|
||||||
|
Vec2::<f32>::new(
|
||||||
if self.right { 1.0 } else { 0.0 } + if self.left { -1.0 } else { 0.0 },
|
if self.right { 1.0 } else { 0.0 } + if self.left { -1.0 } else { 0.0 },
|
||||||
if self.up { 1.0 } else { 0.0 } + if self.down { -1.0 } else { 0.0 },
|
if self.up { 1.0 } else { 0.0 } + if self.down { -1.0 } else { 0.0 },
|
||||||
);
|
)
|
||||||
|
} else {
|
||||||
|
self.analog_matrix
|
||||||
|
};
|
||||||
|
|
||||||
if dir.magnitude_squared() == 0.0 {
|
if dir.magnitude_squared() <= 1.0 {
|
||||||
dir
|
dir
|
||||||
} else {
|
} else {
|
||||||
dir.normalized()
|
dir.normalized()
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod anim;
|
pub mod anim;
|
||||||
pub mod audio;
|
pub mod audio;
|
||||||
|
pub mod controller;
|
||||||
mod ecs;
|
mod ecs;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod hud;
|
pub mod hud;
|
||||||
|
@ -78,7 +78,7 @@ impl PlayState for CharSelectionState {
|
|||||||
char_data.body,
|
char_data.body,
|
||||||
char_data.tool,
|
char_data.tool,
|
||||||
);
|
);
|
||||||
return PlayStateResult::Push(Box::new(SessionState::new(
|
return PlayStateResult::Switch(Box::new(SessionState::new(
|
||||||
global_state,
|
global_state,
|
||||||
self.client.clone(),
|
self.client.clone(),
|
||||||
)));
|
)));
|
||||||
@ -138,7 +138,7 @@ impl PlayState for CharSelectionState {
|
|||||||
) {
|
) {
|
||||||
global_state.info_message =
|
global_state.info_message =
|
||||||
Some(localized_strings.get("common.connection_lost").to_owned());
|
Some(localized_strings.get("common.connection_lost").to_owned());
|
||||||
error!("[session] Failed to tick the scene: {:?}", err);
|
error!("[char_selection] Failed to tick the scene: {:?}", err);
|
||||||
|
|
||||||
return PlayStateResult::Pop;
|
return PlayStateResult::Pop;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use client::{error::Error as ClientError, Client};
|
use client::{error::Error as ClientError, Client};
|
||||||
use common::{comp, net::PostError};
|
use common::net::PostError;
|
||||||
use crossbeam::channel::{unbounded, Receiver, TryRecvError};
|
use crossbeam::channel::{unbounded, Receiver, Sender, TryRecvError};
|
||||||
use std::{
|
use std::{
|
||||||
net::ToSocketAddrs,
|
net::ToSocketAddrs,
|
||||||
sync::{
|
sync::{
|
||||||
@ -15,32 +15,39 @@ use std::{
|
|||||||
pub enum Error {
|
pub enum Error {
|
||||||
// Error parsing input string or error resolving host name.
|
// Error parsing input string or error resolving host name.
|
||||||
BadAddress(std::io::Error),
|
BadAddress(std::io::Error),
|
||||||
// Parsing/host name resolution successful but could not connect.
|
// Parsing/host name resolution successful but there was an error within the client.
|
||||||
#[allow(dead_code)]
|
ClientError(ClientError),
|
||||||
ConnectionFailed(ClientError),
|
|
||||||
// Parsing yielded an empty iterator (specifically to_socket_addrs()).
|
// Parsing yielded an empty iterator (specifically to_socket_addrs()).
|
||||||
NoAddress,
|
NoAddress,
|
||||||
InvalidAuth,
|
|
||||||
ClientCrashed,
|
ClientCrashed,
|
||||||
ServerIsFull,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum Msg {
|
||||||
|
IsAuthTrusted(String),
|
||||||
|
Done(Result<Client, Error>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AuthTrust(String, bool);
|
||||||
|
|
||||||
// Used to asynchronously parse the server address, resolve host names,
|
// Used to asynchronously parse the server address, resolve host names,
|
||||||
// and create the client (which involves establishing a connection to the
|
// and create the client (which involves establishing a connection to the
|
||||||
// server).
|
// server).
|
||||||
pub struct ClientInit {
|
pub struct ClientInit {
|
||||||
rx: Receiver<Result<Client, Error>>,
|
rx: Receiver<Msg>,
|
||||||
|
trust_tx: Sender<AuthTrust>,
|
||||||
cancel: Arc<AtomicBool>,
|
cancel: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
impl ClientInit {
|
impl ClientInit {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
connection_args: (String, u16, bool),
|
connection_args: (String, u16, bool),
|
||||||
player: comp::Player,
|
username: String,
|
||||||
|
view_distance: Option<u32>,
|
||||||
password: String,
|
password: String,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (server_address, default_port, prefer_ipv6) = connection_args;
|
let (server_address, default_port, prefer_ipv6) = connection_args;
|
||||||
|
|
||||||
let (tx, rx) = unbounded();
|
let (tx, rx) = unbounded();
|
||||||
|
let (trust_tx, trust_rx) = unbounded();
|
||||||
let cancel = Arc::new(AtomicBool::new(false));
|
let cancel = Arc::new(AtomicBool::new(false));
|
||||||
let cancel2 = Arc::clone(&cancel);
|
let cancel2 = Arc::clone(&cancel);
|
||||||
|
|
||||||
@ -66,40 +73,39 @@ impl ClientInit {
|
|||||||
for socket_addr in
|
for socket_addr in
|
||||||
first_addrs.clone().into_iter().chain(second_addrs.clone())
|
first_addrs.clone().into_iter().chain(second_addrs.clone())
|
||||||
{
|
{
|
||||||
match Client::new(socket_addr, player.view_distance) {
|
match Client::new(socket_addr, view_distance) {
|
||||||
Ok(mut client) => {
|
Ok(mut client) => {
|
||||||
if let Err(ClientError::InvalidAuth) =
|
if let Err(err) =
|
||||||
client.register(player.clone(), password.clone())
|
client.register(username, password, |auth_server| {
|
||||||
|
let _ = tx
|
||||||
|
.send(Msg::IsAuthTrusted(auth_server.to_string()));
|
||||||
|
trust_rx
|
||||||
|
.recv()
|
||||||
|
.map(|AuthTrust(server, trust)| {
|
||||||
|
trust && &server == auth_server
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
{
|
{
|
||||||
last_err = Some(Error::InvalidAuth);
|
last_err = Some(Error::ClientError(err));
|
||||||
break;
|
break 'tries;
|
||||||
}
|
}
|
||||||
//client.register(player, password);
|
let _ = tx.send(Msg::Done(Ok(client)));
|
||||||
let _ = tx.send(Ok(client));
|
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
match err {
|
match err {
|
||||||
ClientError::Network(PostError::Bincode(_)) => {
|
ClientError::Network(PostError::Bincode(_)) => {
|
||||||
last_err = Some(Error::ConnectionFailed(err));
|
last_err = Some(Error::ClientError(err));
|
||||||
break 'tries;
|
break 'tries;
|
||||||
},
|
},
|
||||||
// Assume the connection failed and try again soon
|
// Assume the connection failed and try again soon
|
||||||
ClientError::Network(_) => {},
|
ClientError::Network(_) => {},
|
||||||
ClientError::TooManyPlayers => {
|
// Non-connection error, stop attempts
|
||||||
last_err = Some(Error::ServerIsFull);
|
err => {
|
||||||
|
last_err = Some(Error::ClientError(err));
|
||||||
break 'tries;
|
break 'tries;
|
||||||
},
|
},
|
||||||
ClientError::InvalidAuth => {
|
|
||||||
last_err = Some(Error::InvalidAuth);
|
|
||||||
break 'tries;
|
|
||||||
},
|
|
||||||
// TODO: Handle errors?
|
|
||||||
_ => panic!(
|
|
||||||
"Unexpected non-network error when creating client: \
|
|
||||||
{:?}",
|
|
||||||
err
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -107,29 +113,38 @@ impl ClientInit {
|
|||||||
thread::sleep(Duration::from_secs(5));
|
thread::sleep(Duration::from_secs(5));
|
||||||
}
|
}
|
||||||
// Parsing/host name resolution successful but no connection succeeded.
|
// Parsing/host name resolution successful but no connection succeeded.
|
||||||
let _ = tx.send(Err(last_err.unwrap_or(Error::NoAddress)));
|
let _ = tx.send(Msg::Done(Err(last_err.unwrap_or(Error::NoAddress))));
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Error parsing input string or error resolving host name.
|
// Error parsing input string or error resolving host name.
|
||||||
let _ = tx.send(Err(Error::BadAddress(err)));
|
let _ = tx.send(Msg::Done(Err(Error::BadAddress(err))));
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ClientInit { rx, cancel }
|
ClientInit {
|
||||||
|
rx,
|
||||||
|
trust_tx,
|
||||||
|
cancel,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Poll if the thread is complete.
|
/// Poll if the thread is complete.
|
||||||
/// Returns None if the thread is still running, otherwise returns the
|
/// Returns None if the thread is still running, otherwise returns the
|
||||||
/// Result of client creation.
|
/// Result of client creation.
|
||||||
pub fn poll(&self) -> Option<Result<Client, Error>> {
|
pub fn poll(&self) -> Option<Msg> {
|
||||||
match self.rx.try_recv() {
|
match self.rx.try_recv() {
|
||||||
Ok(result) => Some(result),
|
Ok(msg) => Some(msg),
|
||||||
Err(TryRecvError::Empty) => None,
|
Err(TryRecvError::Empty) => None,
|
||||||
Err(TryRecvError::Disconnected) => Some(Err(Error::ClientCrashed)),
|
Err(TryRecvError::Disconnected) => Some(Msg::Done(Err(Error::ClientCrashed))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Report trust status of auth server
|
||||||
|
pub fn auth_trust(&self, auth_server: String, trusted: bool) {
|
||||||
|
let _ = self.trust_tx.send(AuthTrust(auth_server, trusted));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cancel(&mut self) { self.cancel.store(true, Ordering::Relaxed); }
|
pub fn cancel(&mut self) { self.cancel.store(true, Ordering::Relaxed); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,15 +3,11 @@ mod client_init;
|
|||||||
|
|
||||||
use super::char_selection::CharSelectionState;
|
use super::char_selection::CharSelectionState;
|
||||||
use crate::{
|
use crate::{
|
||||||
i18n::{i18n_asset_key, VoxygenLocalization},
|
singleplayer::Singleplayer, window::Event, Direction, GlobalState, PlayState, PlayStateResult,
|
||||||
singleplayer::Singleplayer,
|
|
||||||
window::Event,
|
|
||||||
Direction, GlobalState, PlayState, PlayStateResult,
|
|
||||||
};
|
};
|
||||||
use argon2::{self, Config};
|
use client_init::{ClientInit, Error as InitError, Msg as InitMsg};
|
||||||
use client_init::{ClientInit, Error as InitError};
|
|
||||||
use common::{assets::load_expect, clock::Clock, comp};
|
use common::{assets::load_expect, clock::Clock, comp};
|
||||||
use log::warn;
|
use log::{error, warn};
|
||||||
#[cfg(feature = "singleplayer")]
|
#[cfg(feature = "singleplayer")]
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use ui::{Event as MainMenuEvent, MainMenuUi};
|
use ui::{Event as MainMenuEvent, MainMenuUi};
|
||||||
@ -47,6 +43,10 @@ impl PlayState for MainMenuState {
|
|||||||
// Reset singleplayer server if it was running already
|
// Reset singleplayer server if it was running already
|
||||||
global_state.singleplayer = None;
|
global_state.singleplayer = None;
|
||||||
|
|
||||||
|
let localized_strings = load_expect::<crate::i18n::VoxygenLocalization>(
|
||||||
|
&crate::i18n::i18n_asset_key(&global_state.settings.language.selected_language),
|
||||||
|
);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Handle window events.
|
// Handle window events.
|
||||||
for event in global_state.window.fetch_events(&mut global_state.settings) {
|
for event in global_state.window.fetch_events(&mut global_state.settings) {
|
||||||
@ -65,7 +65,7 @@ impl PlayState for MainMenuState {
|
|||||||
|
|
||||||
// Poll client creation.
|
// Poll client creation.
|
||||||
match client_init.as_ref().and_then(|init| init.poll()) {
|
match client_init.as_ref().and_then(|init| init.poll()) {
|
||||||
Some(Ok(mut client)) => {
|
Some(InitMsg::Done(Ok(mut client))) => {
|
||||||
self.main_menu_ui.connected();
|
self.main_menu_ui.connected();
|
||||||
// Register voxygen components / resources
|
// Register voxygen components / resources
|
||||||
crate::ecs::init(client.state_mut().ecs_mut());
|
crate::ecs::init(client.state_mut().ecs_mut());
|
||||||
@ -74,18 +74,82 @@ impl PlayState for MainMenuState {
|
|||||||
std::rc::Rc::new(std::cell::RefCell::new(client)),
|
std::rc::Rc::new(std::cell::RefCell::new(client)),
|
||||||
)));
|
)));
|
||||||
},
|
},
|
||||||
Some(Err(err)) => {
|
Some(InitMsg::Done(Err(err))) => {
|
||||||
client_init = None;
|
client_init = None;
|
||||||
global_state.info_message = Some(
|
global_state.info_message = Some({
|
||||||
match err {
|
let err = match err {
|
||||||
InitError::BadAddress(_) | InitError::NoAddress => "Server not found",
|
InitError::BadAddress(_) | InitError::NoAddress => {
|
||||||
InitError::InvalidAuth => "Invalid credentials",
|
localized_strings.get("main.login.server_not_found").into()
|
||||||
InitError::ServerIsFull => "Server is full",
|
},
|
||||||
InitError::ConnectionFailed(_) => "Connection failed",
|
InitError::ClientError(err) => match err {
|
||||||
InitError::ClientCrashed => "Client crashed",
|
client::Error::AuthErr(e) => format!(
|
||||||
|
"{}: {}",
|
||||||
|
localized_strings.get("main.login.authentication_error"),
|
||||||
|
e
|
||||||
|
),
|
||||||
|
client::Error::TooManyPlayers => {
|
||||||
|
localized_strings.get("main.login.server_full").into()
|
||||||
|
},
|
||||||
|
client::Error::AuthServerNotTrusted => localized_strings
|
||||||
|
.get("main.login.untrusted_auth_server")
|
||||||
|
.into(),
|
||||||
|
client::Error::ServerWentMad => localized_strings
|
||||||
|
.get("main.login.outdated_client_or_server")
|
||||||
|
.into(),
|
||||||
|
client::Error::ServerTimeout => {
|
||||||
|
localized_strings.get("main.login.timeout").into()
|
||||||
|
},
|
||||||
|
client::Error::ServerShutdown => {
|
||||||
|
localized_strings.get("main.login.server_shut_down").into()
|
||||||
|
},
|
||||||
|
client::Error::AlreadyLoggedIn => {
|
||||||
|
localized_strings.get("main.login.already_logged_in").into()
|
||||||
|
},
|
||||||
|
client::Error::Network(e) => format!(
|
||||||
|
"{}: {:?}",
|
||||||
|
localized_strings.get("main.login.network_error"),
|
||||||
|
e
|
||||||
|
),
|
||||||
|
client::Error::Other(e) => {
|
||||||
|
format!("{}: {}", localized_strings.get("common.error"), e)
|
||||||
|
},
|
||||||
|
client::Error::AuthClientError(e) => match e {
|
||||||
|
client::AuthClientError::JsonError(e) => format!(
|
||||||
|
"{}: {}",
|
||||||
|
localized_strings.get("common.fatal_error"),
|
||||||
|
e
|
||||||
|
),
|
||||||
|
client::AuthClientError::RequestError(_) => format!(
|
||||||
|
"{}: {}",
|
||||||
|
localized_strings.get("main.login.failed_sending_request"),
|
||||||
|
e
|
||||||
|
),
|
||||||
|
client::AuthClientError::ServerError(_, e) => format!("{}", e),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InitError::ClientCrashed => {
|
||||||
|
localized_strings.get("main.login.client_crashed").into()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Log error for possible additional use later or incase that the error
|
||||||
|
// displayed is cut of.
|
||||||
|
error!("{}", err);
|
||||||
|
err
|
||||||
|
});
|
||||||
|
},
|
||||||
|
Some(InitMsg::IsAuthTrusted(auth_server)) => {
|
||||||
|
if global_state
|
||||||
|
.settings
|
||||||
|
.networking
|
||||||
|
.trusted_auth_servers
|
||||||
|
.contains(&auth_server)
|
||||||
|
{
|
||||||
|
// Can't fail since we just polled it, it must be Some
|
||||||
|
client_init.as_ref().unwrap().auth_trust(auth_server, true);
|
||||||
|
} else {
|
||||||
|
// Show warning that auth server is not trusted and prompt for approval
|
||||||
|
self.main_menu_ui.auth_trust_prompt(auth_server);
|
||||||
}
|
}
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
None => {},
|
None => {},
|
||||||
}
|
}
|
||||||
@ -141,15 +205,24 @@ impl PlayState for MainMenuState {
|
|||||||
MainMenuEvent::DisclaimerClosed => {
|
MainMenuEvent::DisclaimerClosed => {
|
||||||
global_state.settings.show_disclaimer = false
|
global_state.settings.show_disclaimer = false
|
||||||
},
|
},
|
||||||
|
MainMenuEvent::AuthServerTrust(auth_server, trust) => {
|
||||||
|
if trust {
|
||||||
|
global_state
|
||||||
|
.settings
|
||||||
|
.networking
|
||||||
|
.trusted_auth_servers
|
||||||
|
.insert(auth_server.clone());
|
||||||
|
global_state.settings.save_to_file_warn();
|
||||||
|
}
|
||||||
|
client_init
|
||||||
|
.as_ref()
|
||||||
|
.map(|init| init.auth_trust(auth_server, trust));
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let localized_strings = load_expect::<VoxygenLocalization>(&i18n_asset_key(
|
|
||||||
&global_state.settings.language.selected_language,
|
|
||||||
));
|
|
||||||
|
|
||||||
if let Some(info) = global_state.info_message.take() {
|
if let Some(info) = global_state.info_message.take() {
|
||||||
self.main_menu_ui
|
self.main_menu_ui.show_info(info);
|
||||||
.show_info(info, localized_strings.get("common.okay").to_owned());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw the UI to the screen.
|
// Draw the UI to the screen.
|
||||||
@ -190,25 +263,17 @@ fn attempt_login(
|
|||||||
warn!("Failed to save settings: {:?}", err);
|
warn!("Failed to save settings: {:?}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
let player = comp::Player::new(
|
if comp::Player::alias_is_valid(&username) {
|
||||||
username.clone(),
|
|
||||||
Some(global_state.settings.graphics.view_distance),
|
|
||||||
);
|
|
||||||
|
|
||||||
if player.is_valid() {
|
|
||||||
// Don't try to connect if there is already a connection in progress.
|
// Don't try to connect if there is already a connection in progress.
|
||||||
if client_init.is_none() {
|
if client_init.is_none() {
|
||||||
*client_init = Some(ClientInit::new(
|
*client_init = Some(ClientInit::new(
|
||||||
(server_address, server_port, false),
|
(server_address, server_port, false),
|
||||||
player,
|
username,
|
||||||
{
|
Some(global_state.settings.graphics.view_distance),
|
||||||
let salt = b"staticsalt_zTuGkGvybZIjZbNUDtw15";
|
{ password },
|
||||||
let config = Config::default();
|
|
||||||
argon2::hash_encoded(password.as_bytes(), salt, &config).unwrap()
|
|
||||||
},
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
global_state.info_message = Some("Invalid username or password".to_string());
|
global_state.info_message = Some("Invalid username".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,9 @@ widget_ids! {
|
|||||||
// Info Window
|
// Info Window
|
||||||
info_frame,
|
info_frame,
|
||||||
info_text,
|
info_text,
|
||||||
info_bottom
|
info_bottom,
|
||||||
|
// Auth Trust Prompt
|
||||||
|
button_add_auth_trust,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +92,7 @@ image_ids! {
|
|||||||
button_hover: "voxygen.element.buttons.button_hover",
|
button_hover: "voxygen.element.buttons.button_hover",
|
||||||
button_press: "voxygen.element.buttons.button_press",
|
button_press: "voxygen.element.buttons.button_press",
|
||||||
input_bg_top: "voxygen.element.misc_bg.textbox_top",
|
input_bg_top: "voxygen.element.misc_bg.textbox_top",
|
||||||
//input_bg_mid: "voxygen.element.misc_bg.textbox_mid", <-- For password input
|
input_bg_mid: "voxygen.element.misc_bg.textbox_mid",
|
||||||
input_bg_bot: "voxygen.element.misc_bg.textbox_bot",
|
input_bg_bot: "voxygen.element.misc_bg.textbox_bot",
|
||||||
|
|
||||||
|
|
||||||
@ -122,16 +124,17 @@ pub enum Event {
|
|||||||
Quit,
|
Quit,
|
||||||
Settings,
|
Settings,
|
||||||
DisclaimerClosed,
|
DisclaimerClosed,
|
||||||
|
AuthServerTrust(String, bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum PopupType {
|
pub enum PopupType {
|
||||||
Error,
|
Error,
|
||||||
ConnectionInfo,
|
ConnectionInfo,
|
||||||
|
AuthTrustPrompt(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PopupData {
|
pub struct PopupData {
|
||||||
msg: String,
|
msg: String,
|
||||||
button_text: String,
|
|
||||||
popup_type: PopupType,
|
popup_type: PopupType,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,27 +264,51 @@ impl MainMenuUi {
|
|||||||
.font_id(self.fonts.cyri.conrod_id)
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
.font_size(self.fonts.cyri.scale(14))
|
.font_size(self.fonts.cyri.scale(14))
|
||||||
.set(self.ids.version, ui_widgets);
|
.set(self.ids.version, ui_widgets);
|
||||||
// Popup (Error/Info)
|
|
||||||
if let Some(popup_data) = &self.popup {
|
// Popup (Error/Info/AuthTrustPrompt)
|
||||||
let text = Text::new(&popup_data.msg)
|
let mut change_popup = None;
|
||||||
.rgba(1.0, 1.0, 1.0, if self.connect { fade_msg } else { 1.0 })
|
if let Some(PopupData { msg, popup_type }) = &self.popup {
|
||||||
|
let text = Text::new(msg)
|
||||||
|
.rgba(
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
if let PopupType::ConnectionInfo = popup_type {
|
||||||
|
fade_msg
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
},
|
||||||
|
)
|
||||||
.font_id(self.fonts.cyri.conrod_id);
|
.font_id(self.fonts.cyri.conrod_id);
|
||||||
Rectangle::fill_with([65.0 * 6.0, 140.0], color::TRANSPARENT)
|
let (frame_w, frame_h) = if let PopupType::AuthTrustPrompt(_) = popup_type {
|
||||||
|
(65.0 * 8.0, 370.0)
|
||||||
|
} else {
|
||||||
|
(65.0 * 6.0, 140.0)
|
||||||
|
};
|
||||||
|
let error_bg = Rectangle::fill_with([frame_w, frame_h], color::TRANSPARENT)
|
||||||
.rgba(0.1, 0.1, 0.1, if self.connect { 0.0 } else { 1.0 })
|
.rgba(0.1, 0.1, 0.1, if self.connect { 0.0 } else { 1.0 })
|
||||||
.parent(ui_widgets.window)
|
.parent(ui_widgets.window);
|
||||||
.up_from(self.ids.banner_top, 15.0)
|
if let PopupType::AuthTrustPrompt(_) = popup_type {
|
||||||
|
error_bg.middle_of(ui_widgets.window)
|
||||||
|
} else {
|
||||||
|
error_bg.up_from(self.ids.banner_top, 15.0)
|
||||||
|
}
|
||||||
.set(self.ids.login_error_bg, ui_widgets);
|
.set(self.ids.login_error_bg, ui_widgets);
|
||||||
Image::new(self.imgs.info_frame)
|
Image::new(self.imgs.info_frame)
|
||||||
.w_h(65.0 * 6.0, 140.0)
|
.w_h(frame_w, frame_h)
|
||||||
.color(Some(Color::Rgba(
|
.color(Some(Color::Rgba(
|
||||||
1.0,
|
1.0,
|
||||||
1.0,
|
1.0,
|
||||||
1.0,
|
1.0,
|
||||||
if self.connect { 0.0 } else { 1.0 },
|
if let PopupType::ConnectionInfo = popup_type {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
},
|
||||||
)))
|
)))
|
||||||
.middle_of(self.ids.login_error_bg)
|
.middle_of(self.ids.login_error_bg)
|
||||||
.set(self.ids.error_frame, ui_widgets);
|
.set(self.ids.error_frame, ui_widgets);
|
||||||
if self.connect {
|
if let PopupType::ConnectionInfo = popup_type {
|
||||||
text.mid_top_with_margin_on(self.ids.error_frame, 10.0)
|
text.mid_top_with_margin_on(self.ids.error_frame, 10.0)
|
||||||
.font_id(self.fonts.alkhemi.conrod_id)
|
.font_id(self.fonts.alkhemi.conrod_id)
|
||||||
.bottom_left_with_margins_on(ui_widgets.window, 60.0, 60.0)
|
.bottom_left_with_margins_on(ui_widgets.window, 60.0, 60.0)
|
||||||
@ -289,6 +316,7 @@ impl MainMenuUi {
|
|||||||
.set(self.ids.login_error, ui_widgets);
|
.set(self.ids.login_error, ui_widgets);
|
||||||
} else {
|
} else {
|
||||||
text.mid_top_with_margin_on(self.ids.error_frame, 10.0)
|
text.mid_top_with_margin_on(self.ids.error_frame, 10.0)
|
||||||
|
.w(frame_w - 10.0 * 2.0)
|
||||||
.font_id(self.fonts.cyri.conrod_id)
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
.font_size(self.fonts.cyri.scale(25))
|
.font_size(self.fonts.cyri.scale(25))
|
||||||
.set(self.ids.login_error, ui_widgets);
|
.set(self.ids.login_error, ui_widgets);
|
||||||
@ -296,7 +324,7 @@ impl MainMenuUi {
|
|||||||
if Button::image(self.imgs.button)
|
if Button::image(self.imgs.button)
|
||||||
.w_h(100.0, 30.0)
|
.w_h(100.0, 30.0)
|
||||||
.mid_bottom_with_margin_on(
|
.mid_bottom_with_margin_on(
|
||||||
if self.connect {
|
if let PopupType::ConnectionInfo = popup_type {
|
||||||
ui_widgets.window
|
ui_widgets.window
|
||||||
} else {
|
} else {
|
||||||
self.ids.login_error_bg
|
self.ids.login_error_bg
|
||||||
@ -306,22 +334,55 @@ impl MainMenuUi {
|
|||||||
.hover_image(self.imgs.button_hover)
|
.hover_image(self.imgs.button_hover)
|
||||||
.press_image(self.imgs.button_press)
|
.press_image(self.imgs.button_press)
|
||||||
.label_y(Relative::Scalar(2.0))
|
.label_y(Relative::Scalar(2.0))
|
||||||
.label(&popup_data.button_text)
|
.label(match popup_type {
|
||||||
|
PopupType::Error => self.voxygen_i18n.get("common.okay"),
|
||||||
|
PopupType::ConnectionInfo => self.voxygen_i18n.get("common.cancel"),
|
||||||
|
PopupType::AuthTrustPrompt(_) => self.voxygen_i18n.get("common.cancel"),
|
||||||
|
})
|
||||||
.label_font_id(self.fonts.cyri.conrod_id)
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
.label_font_size(self.fonts.cyri.scale(15))
|
.label_font_size(self.fonts.cyri.scale(15))
|
||||||
.label_color(TEXT_COLOR)
|
.label_color(TEXT_COLOR)
|
||||||
.set(self.ids.button_ok, ui_widgets)
|
.set(self.ids.button_ok, ui_widgets)
|
||||||
.was_clicked()
|
.was_clicked()
|
||||||
{
|
{
|
||||||
match popup_data.popup_type {
|
match &popup_type {
|
||||||
|
PopupType::Error => (),
|
||||||
PopupType::ConnectionInfo => {
|
PopupType::ConnectionInfo => {
|
||||||
events.push(Event::CancelLoginAttempt);
|
events.push(Event::CancelLoginAttempt);
|
||||||
},
|
},
|
||||||
_ => (),
|
PopupType::AuthTrustPrompt(auth_server) => {
|
||||||
};
|
events.push(Event::AuthServerTrust(auth_server.clone(), false));
|
||||||
self.popup = None;
|
},
|
||||||
};
|
};
|
||||||
|
change_popup = Some(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let PopupType::AuthTrustPrompt(auth_server) = popup_type {
|
||||||
|
if Button::image(self.imgs.button)
|
||||||
|
.w_h(100.0, 30.0)
|
||||||
|
.right_from(self.ids.button_ok, 10.0)
|
||||||
|
.hover_image(self.imgs.button_hover)
|
||||||
|
.press_image(self.imgs.button_press)
|
||||||
|
.label_y(Relative::Scalar(2.0))
|
||||||
|
.label("Add") // TODO: localize
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.label_font_size(self.fonts.cyri.scale(15))
|
||||||
|
.label_color(TEXT_COLOR)
|
||||||
|
.set(self.ids.button_add_auth_trust, ui_widgets)
|
||||||
|
.was_clicked()
|
||||||
|
{
|
||||||
|
events.push(Event::AuthServerTrust(auth_server.clone(), true));
|
||||||
|
change_popup = Some(Some(PopupData {
|
||||||
|
msg: self.voxygen_i18n.get("main.connecting").into(),
|
||||||
|
popup_type: PopupType::ConnectionInfo,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(p) = change_popup {
|
||||||
|
self.popup = p;
|
||||||
|
}
|
||||||
|
|
||||||
if !self.connect {
|
if !self.connect {
|
||||||
Image::new(self.imgs.banner)
|
Image::new(self.imgs.banner)
|
||||||
.w_h(65.0 * 6.0, 100.0 * 6.0)
|
.w_h(65.0 * 6.0, 100.0 * 6.0)
|
||||||
@ -387,7 +448,6 @@ impl MainMenuUi {
|
|||||||
self.connecting = Some(std::time::Instant::now());
|
self.connecting = Some(std::time::Instant::now());
|
||||||
self.popup = Some(PopupData {
|
self.popup = Some(PopupData {
|
||||||
msg: [self.voxygen_i18n.get("main.connecting"), "..."].concat(),
|
msg: [self.voxygen_i18n.get("main.connecting"), "..."].concat(),
|
||||||
button_text: self.voxygen_i18n.get("common.cancel").to_owned(),
|
|
||||||
popup_type: PopupType::ConnectionInfo,
|
popup_type: PopupType::ConnectionInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -399,7 +459,7 @@ impl MainMenuUi {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Info Window
|
// Info Window
|
||||||
Rectangle::fill_with([550.0, 200.0], color::BLACK)
|
Rectangle::fill_with([550.0, 400.0], color::BLACK)
|
||||||
.top_left_with_margins_on(ui_widgets.window, 40.0, 40.0)
|
.top_left_with_margins_on(ui_widgets.window, 40.0, 40.0)
|
||||||
.color(Color::Rgba(0.0, 0.0, 0.0, 0.95))
|
.color(Color::Rgba(0.0, 0.0, 0.0, 0.95))
|
||||||
.set(self.ids.info_frame, ui_widgets);
|
.set(self.ids.info_frame, ui_widgets);
|
||||||
@ -425,7 +485,6 @@ impl MainMenuUi {
|
|||||||
self.connecting = Some(std::time::Instant::now());
|
self.connecting = Some(std::time::Instant::now());
|
||||||
self.popup = Some(PopupData {
|
self.popup = Some(PopupData {
|
||||||
msg: [self.voxygen_i18n.get("main.creating_world"), "..."].concat(),
|
msg: [self.voxygen_i18n.get("main.creating_world"), "..."].concat(),
|
||||||
button_text: self.voxygen_i18n.get("common.cancel").to_owned(),
|
|
||||||
popup_type: PopupType::ConnectionInfo,
|
popup_type: PopupType::ConnectionInfo,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -461,14 +520,12 @@ impl MainMenuUi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Password
|
// Password
|
||||||
// TODO: REACTIVATE THIS WHEN A PROPER ACCOUNT SYSTEM IS IN PLACE
|
Rectangle::fill_with([320.0, 50.0], color::rgba(0.0, 0.0, 0.0, 0.97))
|
||||||
/*Rectangle::fill_with([320.0, 50.0], color::rgba(0.0, 0.0, 0.0, 0.97))
|
|
||||||
.down_from(self.ids.usrnm_bg, 30.0)
|
.down_from(self.ids.usrnm_bg, 30.0)
|
||||||
.set(self.ids.passwd_bg, ui_widgets);
|
.set(self.ids.passwd_bg, ui_widgets);
|
||||||
Image::new(self.imgs.input_bg_mid)
|
Image::new(self.imgs.input_bg_mid)
|
||||||
.w_h(337.0, 67.0)
|
.w_h(337.0, 67.0)
|
||||||
.middle_of(self.ids.passwd_bg)
|
.middle_of(self.ids.passwd_bg)
|
||||||
.color(Some(INACTIVE))
|
|
||||||
.set(self.ids.password_bg, ui_widgets);
|
.set(self.ids.password_bg, ui_widgets);
|
||||||
for event in TextBox::new(&self.password)
|
for event in TextBox::new(&self.password)
|
||||||
.w_h(290.0, 30.0)
|
.w_h(290.0, 30.0)
|
||||||
@ -479,18 +536,20 @@ impl MainMenuUi {
|
|||||||
// transparent background
|
// transparent background
|
||||||
.color(TRANSPARENT)
|
.color(TRANSPARENT)
|
||||||
.border_color(TRANSPARENT)
|
.border_color(TRANSPARENT)
|
||||||
|
.hide_text("*")
|
||||||
.set(self.ids.password_field, ui_widgets)
|
.set(self.ids.password_field, ui_widgets)
|
||||||
{
|
{
|
||||||
match event {
|
match event {
|
||||||
TextBoxEvent::Update(password) => {
|
TextBoxEvent::Update(password) => {
|
||||||
// Note: TextBox limits the input string length to what fits in it
|
// Note: TextBox limits the input string length to what fits in it
|
||||||
self.password = password;
|
self.password = password;
|
||||||
}
|
},
|
||||||
TextBoxEvent::Enter => {
|
TextBoxEvent::Enter => {
|
||||||
login!();
|
login!();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}*/
|
|
||||||
if self.show_servers {
|
if self.show_servers {
|
||||||
Image::new(self.imgs.info_frame)
|
Image::new(self.imgs.info_frame)
|
||||||
.mid_top_with_margin_on(self.ids.username_bg, -320.0)
|
.mid_top_with_margin_on(self.ids.username_bg, -320.0)
|
||||||
@ -556,7 +615,7 @@ impl MainMenuUi {
|
|||||||
}
|
}
|
||||||
// Server address
|
// Server address
|
||||||
Rectangle::fill_with([320.0, 50.0], color::rgba(0.0, 0.0, 0.0, 0.97))
|
Rectangle::fill_with([320.0, 50.0], color::rgba(0.0, 0.0, 0.0, 0.97))
|
||||||
.down_from(self.ids.usrnm_bg, 30.0)
|
.down_from(self.ids.passwd_bg, 30.0)
|
||||||
.set(self.ids.srvr_bg, ui_widgets);
|
.set(self.ids.srvr_bg, ui_widgets);
|
||||||
Image::new(self.imgs.input_bg_bot)
|
Image::new(self.imgs.input_bg_bot)
|
||||||
.w_h(337.0, 67.0)
|
.w_h(337.0, 67.0)
|
||||||
@ -582,6 +641,7 @@ impl MainMenuUi {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login button
|
// Login button
|
||||||
if Button::image(self.imgs.button)
|
if Button::image(self.imgs.button)
|
||||||
.hover_image(self.imgs.button_hover)
|
.hover_image(self.imgs.button_hover)
|
||||||
@ -684,10 +744,22 @@ impl MainMenuUi {
|
|||||||
events
|
events
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_info(&mut self, msg: String, button_text: String) {
|
pub fn auth_trust_prompt(&mut self, auth_server: String) {
|
||||||
|
self.popup = Some(PopupData {
|
||||||
|
msg: format!(
|
||||||
|
"Warning: The server you are trying to connect to has provided this \
|
||||||
|
authentication server address:\n\n{}\n\nbut it is not in your list of trusted \
|
||||||
|
authentication servers.\n\nMake sure that you trust this site and owner to not \
|
||||||
|
try and bruteforce your password!",
|
||||||
|
&auth_server
|
||||||
|
),
|
||||||
|
popup_type: PopupType::AuthTrustPrompt(auth_server),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_info(&mut self, msg: String) {
|
||||||
self.popup = Some(PopupData {
|
self.popup = Some(PopupData {
|
||||||
msg,
|
msg,
|
||||||
button_text,
|
|
||||||
popup_type: PopupType::Error,
|
popup_type: PopupType::Error,
|
||||||
});
|
});
|
||||||
self.connecting = None;
|
self.connecting = None;
|
||||||
|
@ -15,7 +15,7 @@ use crate::{
|
|||||||
create_pp_mesh, create_skybox_mesh, Consts, Globals, Light, Model, PostProcessLocals,
|
create_pp_mesh, create_skybox_mesh, Consts, Globals, Light, Model, PostProcessLocals,
|
||||||
PostProcessPipeline, Renderer, Shadow, SkyboxLocals, SkyboxPipeline,
|
PostProcessPipeline, Renderer, Shadow, SkyboxLocals, SkyboxPipeline,
|
||||||
},
|
},
|
||||||
window::Event,
|
window::{AnalogGameInput, Event},
|
||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
comp,
|
comp,
|
||||||
@ -50,6 +50,7 @@ pub struct Scene {
|
|||||||
lights: Consts<Light>,
|
lights: Consts<Light>,
|
||||||
shadows: Consts<Shadow>,
|
shadows: Consts<Shadow>,
|
||||||
camera: Camera,
|
camera: Camera,
|
||||||
|
camera_input_state: Vec2<f32>,
|
||||||
|
|
||||||
skybox: Skybox,
|
skybox: Skybox,
|
||||||
postprocess: PostProcess,
|
postprocess: PostProcess,
|
||||||
@ -85,6 +86,7 @@ impl Scene {
|
|||||||
.create_consts(&[Shadow::default(); MAX_SHADOW_COUNT])
|
.create_consts(&[Shadow::default(); MAX_SHADOW_COUNT])
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
camera: Camera::new(resolution.x / resolution.y, CameraMode::ThirdPerson),
|
camera: Camera::new(resolution.x / resolution.y, CameraMode::ThirdPerson),
|
||||||
|
camera_input_state: Vec2::zero(),
|
||||||
|
|
||||||
skybox: Skybox {
|
skybox: Skybox {
|
||||||
model: renderer.create_model(&create_skybox_mesh()).unwrap(),
|
model: renderer.create_model(&create_skybox_mesh()).unwrap(),
|
||||||
@ -146,6 +148,17 @@ impl Scene {
|
|||||||
.zoom_switch(delta * (0.05 + self.camera.get_distance() * 0.01));
|
.zoom_switch(delta * (0.05 + self.camera.get_distance() * 0.01));
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
|
Event::AnalogGameInput(input) => match input {
|
||||||
|
AnalogGameInput::CameraX(d) => {
|
||||||
|
self.camera_input_state.x = d;
|
||||||
|
true
|
||||||
|
},
|
||||||
|
AnalogGameInput::CameraY(d) => {
|
||||||
|
self.camera_input_state.y = d;
|
||||||
|
true
|
||||||
|
},
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
// All other events are unhandled
|
// All other events are unhandled
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
@ -185,6 +198,12 @@ impl Scene {
|
|||||||
_ => 1_f32,
|
_ => 1_f32,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add the analog input to camera
|
||||||
|
self.camera
|
||||||
|
.rotate_by(Vec3::from([self.camera_input_state.x, 0.0, 0.0]));
|
||||||
|
self.camera
|
||||||
|
.rotate_by(Vec3::from([0.0, self.camera_input_state.y, 0.0]));
|
||||||
|
|
||||||
// Alter camera position to match player.
|
// Alter camera position to match player.
|
||||||
let tilt = self.camera.get_orientation().y;
|
let tilt = self.camera.get_orientation().y;
|
||||||
let dist = self.camera.get_distance();
|
let dist = self.camera.get_distance();
|
||||||
|
@ -3,9 +3,10 @@ use crate::{
|
|||||||
hud::{DebugInfo, Event as HudEvent, Hud},
|
hud::{DebugInfo, Event as HudEvent, Hud},
|
||||||
i18n::{i18n_asset_key, VoxygenLocalization},
|
i18n::{i18n_asset_key, VoxygenLocalization},
|
||||||
key_state::KeyState,
|
key_state::KeyState,
|
||||||
|
menu::char_selection::CharSelectionState,
|
||||||
render::Renderer,
|
render::Renderer,
|
||||||
scene::{camera, Scene, SceneData},
|
scene::{camera, Scene, SceneData},
|
||||||
window::{Event, GameInput},
|
window::{AnalogGameInput, Event, GameInput},
|
||||||
Direction, Error, GlobalState, PlayState, PlayStateResult,
|
Direction, Error, GlobalState, PlayState, PlayStateResult,
|
||||||
};
|
};
|
||||||
use client::{self, Client, Event::Chat};
|
use client::{self, Client, Event::Chat};
|
||||||
@ -13,7 +14,7 @@ use common::{
|
|||||||
assets::{load_watched, watch},
|
assets::{load_watched, watch},
|
||||||
clock::Clock,
|
clock::Clock,
|
||||||
comp,
|
comp,
|
||||||
comp::{Pos, Vel},
|
comp::{Pos, Vel, MAX_PICKUP_RANGE_SQR},
|
||||||
msg::ClientState,
|
msg::ClientState,
|
||||||
terrain::{Block, BlockKind},
|
terrain::{Block, BlockKind},
|
||||||
vol::ReadVol,
|
vol::ReadVol,
|
||||||
@ -24,6 +25,14 @@ use specs::{Join, WorldExt};
|
|||||||
use std::{cell::RefCell, rc::Rc, time::Duration};
|
use std::{cell::RefCell, rc::Rc, time::Duration};
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
|
/// The action to perform after a tick
|
||||||
|
enum TickAction {
|
||||||
|
// Continue executing
|
||||||
|
Continue,
|
||||||
|
// Disconnected (i.e. go to main menu)
|
||||||
|
Disconnect,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SessionState {
|
pub struct SessionState {
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
client: Rc<RefCell<Client>>,
|
client: Rc<RefCell<Client>>,
|
||||||
@ -65,7 +74,7 @@ impl SessionState {
|
|||||||
|
|
||||||
impl SessionState {
|
impl SessionState {
|
||||||
/// Tick the session (and the client attached to it).
|
/// Tick the session (and the client attached to it).
|
||||||
fn tick(&mut self, dt: Duration) -> Result<(), Error> {
|
fn tick(&mut self, dt: Duration) -> Result<TickAction, Error> {
|
||||||
self.inputs.tick(dt);
|
self.inputs.tick(dt);
|
||||||
for event in self.client.borrow_mut().tick(
|
for event in self.client.borrow_mut().tick(
|
||||||
self.inputs.clone(),
|
self.inputs.clone(),
|
||||||
@ -79,7 +88,7 @@ impl SessionState {
|
|||||||
} => {
|
} => {
|
||||||
self.hud.new_message(event);
|
self.hud.new_message(event);
|
||||||
},
|
},
|
||||||
client::Event::Disconnect => {}, // TODO
|
client::Event::Disconnect => return Ok(TickAction::Disconnect),
|
||||||
client::Event::DisconnectionNotification(time) => {
|
client::Event::DisconnectionNotification(time) => {
|
||||||
let message = match time {
|
let message = match time {
|
||||||
0 => String::from("Goodbye!"),
|
0 => String::from("Goodbye!"),
|
||||||
@ -94,7 +103,7 @@ impl SessionState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(TickAction::Continue)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clean up the session (and the client attached to it) after a tick.
|
/// Clean up the session (and the client attached to it) after a tick.
|
||||||
@ -155,27 +164,52 @@ impl PlayState for SessionState {
|
|||||||
let camera::Dependents {
|
let camera::Dependents {
|
||||||
view_mat, cam_pos, ..
|
view_mat, cam_pos, ..
|
||||||
} = self.scene.camera().dependents();
|
} = self.scene.camera().dependents();
|
||||||
|
|
||||||
|
// Choose a spot above the player's head for item distance checks
|
||||||
|
let player_pos = match self
|
||||||
|
.client
|
||||||
|
.borrow()
|
||||||
|
.state()
|
||||||
|
.read_storage::<comp::Pos>()
|
||||||
|
.get(self.client.borrow().entity())
|
||||||
|
{
|
||||||
|
Some(pos) => pos.0 + (Vec3::unit_z() * 2.0),
|
||||||
|
_ => cam_pos, // Should never happen, but a safe fallback
|
||||||
|
};
|
||||||
|
|
||||||
let cam_dir: Vec3<f32> = Vec3::from(view_mat.inverted() * -Vec4::unit_z());
|
let cam_dir: Vec3<f32> = Vec3::from(view_mat.inverted() * -Vec4::unit_z());
|
||||||
|
|
||||||
// Check to see whether we're aiming at anything
|
// Check to see whether we're aiming at anything
|
||||||
let (build_pos, select_pos) = {
|
let (build_pos, select_pos) = {
|
||||||
let client = self.client.borrow();
|
let client = self.client.borrow();
|
||||||
let terrain = client.state().terrain();
|
let terrain = client.state().terrain();
|
||||||
let ray = terrain
|
|
||||||
|
let cam_ray = terrain
|
||||||
.ray(cam_pos, cam_pos + cam_dir * 100.0)
|
.ray(cam_pos, cam_pos + cam_dir * 100.0)
|
||||||
.until(|block| block.is_tangible())
|
.until(|block| block.is_tangible())
|
||||||
.cast();
|
.cast();
|
||||||
let dist = ray.0;
|
|
||||||
if let Ok(Some(_)) = ray.1 {
|
let cam_dist = cam_ray.0;
|
||||||
// Hit something!
|
|
||||||
|
if let Ok(Some(_)) = cam_ray.1 {
|
||||||
|
// The ray hit something, is it within pickup range?
|
||||||
|
let select_pos = if player_pos.distance_squared(cam_pos + cam_dir * cam_dist)
|
||||||
|
<= MAX_PICKUP_RANGE_SQR
|
||||||
|
{
|
||||||
|
Some((cam_pos + cam_dir * cam_dist).map(|e| e.floor() as i32))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
(
|
(
|
||||||
Some((cam_pos + cam_dir * (dist - 0.01)).map(|e| e.floor() as i32)),
|
Some((cam_pos + cam_dir * (cam_dist - 0.01)).map(|e| e.floor() as i32)),
|
||||||
Some((cam_pos + cam_dir * dist).map(|e| e.floor() as i32)),
|
select_pos,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(None, None)
|
(None, None)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only highlight collectables
|
// Only highlight collectables
|
||||||
self.scene.set_select_pos(select_pos.filter(|sp| {
|
self.scene.set_select_pos(select_pos.filter(|sp| {
|
||||||
self.client
|
self.client
|
||||||
@ -234,6 +268,7 @@ impl PlayState for SessionState {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.inputs.secondary.set_state(state);
|
self.inputs.secondary.set_state(state);
|
||||||
|
|
||||||
if let Some(select_pos) = select_pos {
|
if let Some(select_pos) = select_pos {
|
||||||
client.collect_block(select_pos);
|
client.collect_block(select_pos);
|
||||||
}
|
}
|
||||||
@ -354,6 +389,17 @@ impl PlayState for SessionState {
|
|||||||
Event::InputUpdate(GameInput::Charge, state) => {
|
Event::InputUpdate(GameInput::Charge, state) => {
|
||||||
self.inputs.charge.set_state(state);
|
self.inputs.charge.set_state(state);
|
||||||
},
|
},
|
||||||
|
Event::AnalogGameInput(input) => match input {
|
||||||
|
AnalogGameInput::MovementX(v) => {
|
||||||
|
self.key_state.analog_matrix.x = v;
|
||||||
|
},
|
||||||
|
AnalogGameInput::MovementY(v) => {
|
||||||
|
self.key_state.analog_matrix.y = v;
|
||||||
|
},
|
||||||
|
other => {
|
||||||
|
self.scene.handle_input_event(Event::AnalogGameInput(other));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// Pass all other events to the scene
|
// Pass all other events to the scene
|
||||||
event => {
|
event => {
|
||||||
@ -379,12 +425,16 @@ impl PlayState for SessionState {
|
|||||||
|| !global_state.singleplayer.as_ref().unwrap().is_paused()
|
|| !global_state.singleplayer.as_ref().unwrap().is_paused()
|
||||||
{
|
{
|
||||||
// Perform an in-game tick.
|
// Perform an in-game tick.
|
||||||
if let Err(err) = self.tick(clock.get_avg_delta()) {
|
match self.tick(clock.get_avg_delta()) {
|
||||||
|
Ok(TickAction::Continue) => {}, // Do nothing
|
||||||
|
Ok(TickAction::Disconnect) => return PlayStateResult::Pop, // Go to main menu
|
||||||
|
Err(err) => {
|
||||||
global_state.info_message =
|
global_state.info_message =
|
||||||
Some(localized_strings.get("common.connection_lost").to_owned());
|
Some(localized_strings.get("common.connection_lost").to_owned());
|
||||||
error!("[session] Failed to tick the scene: {:?}", err);
|
error!("[session] Failed to tick the scene: {:?}", err);
|
||||||
|
|
||||||
return PlayStateResult::Pop;
|
return PlayStateResult::Pop;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -657,6 +707,13 @@ impl PlayState for SessionState {
|
|||||||
current_client_state = self.client.borrow().get_client_state();
|
current_client_state = self.client.borrow().get_client_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let ClientState::Registered = current_client_state {
|
||||||
|
return PlayStateResult::Switch(Box::new(CharSelectionState::new(
|
||||||
|
global_state,
|
||||||
|
self.client.clone(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
PlayStateResult::Pop
|
PlayStateResult::Pop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use directories::{ProjectDirs, UserDirs};
|
use directories::{ProjectDirs, UserDirs};
|
||||||
use glutin::{MouseButton, VirtualKeyCode};
|
use glutin::{MouseButton, VirtualKeyCode};
|
||||||
|
use hashbrown::{HashMap, HashSet};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use std::{fs, io::prelude::*, path::PathBuf};
|
use std::{fs, io::prelude::*, path::PathBuf};
|
||||||
@ -104,6 +105,225 @@ impl Default for ControlSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct GamepadSettings {
|
||||||
|
pub game_buttons: con_settings::GameButtons,
|
||||||
|
pub menu_buttons: con_settings::MenuButtons,
|
||||||
|
pub game_axis: con_settings::GameAxis,
|
||||||
|
pub menu_axis: con_settings::MenuAxis,
|
||||||
|
pub game_analog_buttons: con_settings::GameAnalogButton,
|
||||||
|
pub menu_analog_buttons: con_settings::MenuAnalogButton,
|
||||||
|
pub pan_sensitivity: u32,
|
||||||
|
pub pan_invert_y: bool,
|
||||||
|
pub axis_deadzones: HashMap<crate::controller::Axis, f32>,
|
||||||
|
pub button_deadzones: HashMap<crate::controller::AnalogButton, f32>,
|
||||||
|
pub mouse_emulation_sensitivity: u32,
|
||||||
|
pub inverted_axes: Vec<crate::controller::Axis>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GamepadSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
game_buttons: con_settings::GameButtons::default(),
|
||||||
|
menu_buttons: con_settings::MenuButtons::default(),
|
||||||
|
game_axis: con_settings::GameAxis::default(),
|
||||||
|
menu_axis: con_settings::MenuAxis::default(),
|
||||||
|
game_analog_buttons: con_settings::GameAnalogButton::default(),
|
||||||
|
menu_analog_buttons: con_settings::MenuAnalogButton::default(),
|
||||||
|
pan_sensitivity: 10,
|
||||||
|
pan_invert_y: false,
|
||||||
|
axis_deadzones: HashMap::new(),
|
||||||
|
button_deadzones: HashMap::new(),
|
||||||
|
mouse_emulation_sensitivity: 12,
|
||||||
|
inverted_axes: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod con_settings {
|
||||||
|
use crate::controller::*;
|
||||||
|
use gilrs::{Axis as GilAxis, Button as GilButton};
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct GameButtons {
|
||||||
|
pub primary: Button,
|
||||||
|
pub secondary: Button,
|
||||||
|
pub toggle_cursor: Button,
|
||||||
|
pub escape: Button,
|
||||||
|
pub enter: Button,
|
||||||
|
pub command: Button,
|
||||||
|
pub move_forward: Button,
|
||||||
|
pub move_left: Button,
|
||||||
|
pub move_back: Button,
|
||||||
|
pub move_right: Button,
|
||||||
|
pub jump: Button,
|
||||||
|
pub sit: Button,
|
||||||
|
pub glide: Button,
|
||||||
|
pub climb: Button,
|
||||||
|
pub climb_down: Button,
|
||||||
|
pub wall_leap: Button,
|
||||||
|
pub mount: Button,
|
||||||
|
pub map: Button,
|
||||||
|
pub bag: Button,
|
||||||
|
pub quest_log: Button,
|
||||||
|
pub character_window: Button,
|
||||||
|
pub social: Button,
|
||||||
|
pub spellbook: Button,
|
||||||
|
pub settings: Button,
|
||||||
|
pub help: Button,
|
||||||
|
pub toggle_interface: Button,
|
||||||
|
pub toggle_debug: Button,
|
||||||
|
pub fullscreen: Button,
|
||||||
|
pub screenshot: Button,
|
||||||
|
pub toggle_ingame_ui: Button,
|
||||||
|
pub roll: Button,
|
||||||
|
pub respawn: Button,
|
||||||
|
pub interact: Button,
|
||||||
|
pub toggle_wield: Button,
|
||||||
|
pub charge: Button,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct MenuButtons {
|
||||||
|
pub up: Button,
|
||||||
|
pub down: Button,
|
||||||
|
pub left: Button,
|
||||||
|
pub right: Button,
|
||||||
|
pub scroll_up: Button,
|
||||||
|
pub scroll_down: Button,
|
||||||
|
pub scroll_left: Button,
|
||||||
|
pub scroll_right: Button,
|
||||||
|
pub home: Button,
|
||||||
|
pub end: Button,
|
||||||
|
pub apply: Button,
|
||||||
|
pub back: Button,
|
||||||
|
pub exit: Button,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct GameAxis {
|
||||||
|
pub movement_x: Axis,
|
||||||
|
pub movement_y: Axis,
|
||||||
|
pub camera_x: Axis,
|
||||||
|
pub camera_y: Axis,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct MenuAxis {
|
||||||
|
pub move_x: Axis,
|
||||||
|
pub move_y: Axis,
|
||||||
|
pub scroll_x: Axis,
|
||||||
|
pub scroll_y: Axis,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct GameAnalogButton {}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct MenuAnalogButton {}
|
||||||
|
|
||||||
|
impl Default for GameButtons {
|
||||||
|
fn default() -> Self {
|
||||||
|
// binding to unknown = getting skipped from processing
|
||||||
|
Self {
|
||||||
|
primary: Button::Simple(GilButton::RightTrigger2),
|
||||||
|
secondary: Button::Simple(GilButton::LeftTrigger2),
|
||||||
|
toggle_cursor: Button::Simple(GilButton::Select),
|
||||||
|
escape: Button::Simple(GilButton::Mode),
|
||||||
|
enter: Button::Simple(GilButton::Unknown),
|
||||||
|
command: Button::Simple(GilButton::Unknown),
|
||||||
|
move_forward: Button::Simple(GilButton::Unknown),
|
||||||
|
move_left: Button::Simple(GilButton::Unknown),
|
||||||
|
move_back: Button::Simple(GilButton::Unknown),
|
||||||
|
move_right: Button::Simple(GilButton::Unknown),
|
||||||
|
jump: Button::Simple(GilButton::South),
|
||||||
|
sit: Button::Simple(GilButton::West),
|
||||||
|
glide: Button::Simple(GilButton::LeftTrigger),
|
||||||
|
climb: Button::Simple(GilButton::South),
|
||||||
|
climb_down: Button::Simple(GilButton::Unknown),
|
||||||
|
wall_leap: Button::Simple(GilButton::Unknown),
|
||||||
|
mount: Button::Simple(GilButton::North),
|
||||||
|
map: Button::Simple(GilButton::DPadRight),
|
||||||
|
bag: Button::Simple(GilButton::DPadDown),
|
||||||
|
quest_log: Button::Simple(GilButton::Unknown),
|
||||||
|
character_window: Button::Simple(GilButton::Unknown),
|
||||||
|
social: Button::Simple(GilButton::Unknown),
|
||||||
|
spellbook: Button::Simple(GilButton::Unknown),
|
||||||
|
settings: Button::Simple(GilButton::Unknown),
|
||||||
|
help: Button::Simple(GilButton::Unknown),
|
||||||
|
toggle_interface: Button::Simple(GilButton::Unknown),
|
||||||
|
toggle_debug: Button::Simple(GilButton::Unknown),
|
||||||
|
fullscreen: Button::Simple(GilButton::Unknown),
|
||||||
|
screenshot: Button::Simple(GilButton::DPadUp),
|
||||||
|
toggle_ingame_ui: Button::Simple(GilButton::Unknown),
|
||||||
|
roll: Button::Simple(GilButton::RightTrigger),
|
||||||
|
respawn: Button::Simple(GilButton::RightTrigger2),
|
||||||
|
interact: Button::Simple(GilButton::LeftTrigger2),
|
||||||
|
toggle_wield: Button::Simple(GilButton::DPadLeft),
|
||||||
|
charge: Button::Simple(GilButton::Unknown),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MenuButtons {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
up: Button::Simple(GilButton::Unknown),
|
||||||
|
down: Button::Simple(GilButton::Unknown),
|
||||||
|
left: Button::Simple(GilButton::Unknown),
|
||||||
|
right: Button::Simple(GilButton::Unknown),
|
||||||
|
scroll_up: Button::Simple(GilButton::Unknown),
|
||||||
|
scroll_down: Button::Simple(GilButton::Unknown),
|
||||||
|
scroll_left: Button::Simple(GilButton::Unknown),
|
||||||
|
scroll_right: Button::Simple(GilButton::Unknown),
|
||||||
|
home: Button::Simple(GilButton::DPadUp),
|
||||||
|
end: Button::Simple(GilButton::DPadDown),
|
||||||
|
apply: Button::Simple(GilButton::South),
|
||||||
|
back: Button::Simple(GilButton::East),
|
||||||
|
exit: Button::Simple(GilButton::Mode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GameAxis {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
movement_x: Axis::Simple(GilAxis::LeftStickX),
|
||||||
|
movement_y: Axis::Simple(GilAxis::LeftStickY),
|
||||||
|
camera_x: Axis::Simple(GilAxis::RightStickX),
|
||||||
|
camera_y: Axis::Simple(GilAxis::RightStickY),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MenuAxis {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
move_x: Axis::Simple(GilAxis::RightStickX),
|
||||||
|
move_y: Axis::Simple(GilAxis::RightStickY),
|
||||||
|
scroll_x: Axis::Simple(GilAxis::LeftStickX),
|
||||||
|
scroll_y: Axis::Simple(GilAxis::LeftStickY),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GameAnalogButton {
|
||||||
|
fn default() -> Self { Self {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MenuAnalogButton {
|
||||||
|
fn default() -> Self { Self {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `GameplaySettings` contains sensitivity and gameplay options.
|
/// `GameplaySettings` contains sensitivity and gameplay options.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@ -157,6 +377,7 @@ pub struct NetworkingSettings {
|
|||||||
pub password: String,
|
pub password: String,
|
||||||
pub servers: Vec<String>,
|
pub servers: Vec<String>,
|
||||||
pub default_server: usize,
|
pub default_server: usize,
|
||||||
|
pub trusted_auth_servers: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for NetworkingSettings {
|
impl Default for NetworkingSettings {
|
||||||
@ -166,6 +387,10 @@ impl Default for NetworkingSettings {
|
|||||||
password: String::default(),
|
password: String::default(),
|
||||||
servers: vec!["server.veloren.net".to_string()],
|
servers: vec!["server.veloren.net".to_string()],
|
||||||
default_server: 0,
|
default_server: 0,
|
||||||
|
trusted_auth_servers: ["https://auth.veloren.net"]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -292,6 +517,7 @@ pub struct Settings {
|
|||||||
pub logon_commands: Vec<String>,
|
pub logon_commands: Vec<String>,
|
||||||
pub language: LanguageSettings,
|
pub language: LanguageSettings,
|
||||||
pub screenshots_path: PathBuf,
|
pub screenshots_path: PathBuf,
|
||||||
|
pub controller: GamepadSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
@ -323,6 +549,7 @@ impl Default for Settings {
|
|||||||
logon_commands: Vec::new(),
|
logon_commands: Vec::new(),
|
||||||
language: LanguageSettings::default(),
|
language: LanguageSettings::default(),
|
||||||
screenshots_path,
|
screenshots_path,
|
||||||
|
controller: GamepadSettings::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -634,8 +634,8 @@ impl Ui {
|
|||||||
mesh.push_quad(create_ui_quad(
|
mesh.push_quad(create_ui_quad(
|
||||||
gl_aabr(rect),
|
gl_aabr(rect),
|
||||||
Aabr {
|
Aabr {
|
||||||
min: Vec2::new(0.0, 0.0),
|
min: Vec2::zero(),
|
||||||
max: Vec2::new(0.0, 0.0),
|
max: Vec2::zero(),
|
||||||
},
|
},
|
||||||
color,
|
color,
|
||||||
UiMode::Geometry,
|
UiMode::Geometry,
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
controller::*,
|
||||||
render::{Renderer, WinColorFmt, WinDepthFmt},
|
render::{Renderer, WinColorFmt, WinDepthFmt},
|
||||||
settings::Settings,
|
settings::Settings,
|
||||||
ui, Error,
|
ui, Error,
|
||||||
};
|
};
|
||||||
|
use gilrs::{EventType, Gilrs};
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
/// Represents a key that the game recognises after keyboard mapping.
|
/// Represents a key that the game recognises after input mapping.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||||
pub enum GameInput {
|
pub enum GameInput {
|
||||||
Primary,
|
Primary,
|
||||||
@ -49,6 +51,40 @@ pub enum GameInput {
|
|||||||
Charge,
|
Charge,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a key that the game menus recognise after input mapping
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||||
|
pub enum MenuInput {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
ScrollUp,
|
||||||
|
ScrollDown,
|
||||||
|
ScrollLeft,
|
||||||
|
ScrollRight,
|
||||||
|
Home,
|
||||||
|
End,
|
||||||
|
Apply,
|
||||||
|
Back,
|
||||||
|
Exit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub enum AnalogMenuInput {
|
||||||
|
MoveX(f32),
|
||||||
|
MoveY(f32),
|
||||||
|
ScrollX(f32),
|
||||||
|
ScrollY(f32),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub enum AnalogGameInput {
|
||||||
|
MovementX(f32),
|
||||||
|
MovementY(f32),
|
||||||
|
CameraX(f32),
|
||||||
|
CameraY(f32),
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents an incoming event from the window.
|
/// Represents an incoming event from the window.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
@ -76,6 +112,13 @@ pub enum Event {
|
|||||||
SettingsChanged,
|
SettingsChanged,
|
||||||
/// The window is (un)focused
|
/// The window is (un)focused
|
||||||
Focused(bool),
|
Focused(bool),
|
||||||
|
/// A key that the game recognises for menu navigation has been pressed or
|
||||||
|
/// released
|
||||||
|
MenuInput(MenuInput, bool),
|
||||||
|
/// Update of the analog inputs recognized by the menus
|
||||||
|
AnalogMenuInput(AnalogMenuInput),
|
||||||
|
/// Update of the analog inputs recognized by the game
|
||||||
|
AnalogGameInput(AnalogGameInput),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type MouseButton = winit::MouseButton;
|
pub type MouseButton = winit::MouseButton;
|
||||||
@ -277,6 +320,10 @@ pub struct Window {
|
|||||||
keypress_map: HashMap<GameInput, glutin::ElementState>,
|
keypress_map: HashMap<GameInput, glutin::ElementState>,
|
||||||
supplement_events: Vec<Event>,
|
supplement_events: Vec<Event>,
|
||||||
focused: bool,
|
focused: bool,
|
||||||
|
gilrs: Option<Gilrs>,
|
||||||
|
controller_settings: ControllerSettings,
|
||||||
|
cursor_position: winit::dpi::LogicalPosition,
|
||||||
|
mouse_emulation_vec: Vec2<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
@ -414,6 +461,30 @@ impl Window {
|
|||||||
|
|
||||||
let keypress_map = HashMap::new();
|
let keypress_map = HashMap::new();
|
||||||
|
|
||||||
|
let gilrs = match Gilrs::new() {
|
||||||
|
Ok(gilrs) => Some(gilrs),
|
||||||
|
Err(gilrs::Error::NotImplemented(_dummy)) => {
|
||||||
|
warn!("Controller input is unsupported on this platform.");
|
||||||
|
None
|
||||||
|
},
|
||||||
|
Err(gilrs::Error::InvalidAxisToBtn) => {
|
||||||
|
error!(
|
||||||
|
"Invalid AxisToBtn controller mapping. Falling back to no controller support."
|
||||||
|
);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
Err(gilrs::Error::Other(err)) => {
|
||||||
|
error!(
|
||||||
|
"Platform-specific error when creating a Gilrs instance: `{}`. Falling back \
|
||||||
|
to no controller support.",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let controller_settings = ControllerSettings::from(&settings.controller);
|
||||||
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
events_loop,
|
events_loop,
|
||||||
renderer: Renderer::new(
|
renderer: Renderer::new(
|
||||||
@ -437,6 +508,10 @@ impl Window {
|
|||||||
keypress_map,
|
keypress_map,
|
||||||
supplement_events: vec![],
|
supplement_events: vec![],
|
||||||
focused: true,
|
focused: true,
|
||||||
|
gilrs,
|
||||||
|
controller_settings,
|
||||||
|
cursor_position: winit::dpi::LogicalPosition::new(0.0, 0.0),
|
||||||
|
mouse_emulation_vec: Vec2::zero(),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.fullscreen(settings.graphics.fullscreen);
|
this.fullscreen(settings.graphics.fullscreen);
|
||||||
@ -477,6 +552,7 @@ impl Window {
|
|||||||
};
|
};
|
||||||
let mut toggle_fullscreen = false;
|
let mut toggle_fullscreen = false;
|
||||||
let mut take_screenshot = false;
|
let mut take_screenshot = false;
|
||||||
|
let mut cursor_position = None;
|
||||||
|
|
||||||
self.events_loop.poll_events(|event| {
|
self.events_loop.poll_events(|event| {
|
||||||
// Get events for ui.
|
// Get events for ui.
|
||||||
@ -556,6 +632,9 @@ impl Window {
|
|||||||
*focused = state;
|
*focused = state;
|
||||||
events.push(Event::Focused(state));
|
events.push(Event::Focused(state));
|
||||||
},
|
},
|
||||||
|
glutin::WindowEvent::CursorMoved { position, .. } => {
|
||||||
|
cursor_position = Some(position);
|
||||||
|
},
|
||||||
_ => {},
|
_ => {},
|
||||||
},
|
},
|
||||||
glutin::Event::DeviceEvent { event, .. } => match event {
|
glutin::Event::DeviceEvent { event, .. } => match event {
|
||||||
@ -593,6 +672,10 @@ impl Window {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Some(pos) = cursor_position {
|
||||||
|
self.cursor_position = pos;
|
||||||
|
}
|
||||||
|
|
||||||
if take_screenshot {
|
if take_screenshot {
|
||||||
self.take_screenshot(&settings);
|
self.take_screenshot(&settings);
|
||||||
}
|
}
|
||||||
@ -601,9 +684,217 @@ impl Window {
|
|||||||
self.toggle_fullscreen(settings);
|
self.toggle_fullscreen(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(gilrs) = &mut self.gilrs {
|
||||||
|
while let Some(event) = gilrs.next_event() {
|
||||||
|
fn handle_buttons(
|
||||||
|
settings: &ControllerSettings,
|
||||||
|
events: &mut Vec<Event>,
|
||||||
|
button: &Button,
|
||||||
|
is_pressed: bool,
|
||||||
|
) {
|
||||||
|
if let Some(evs) = settings.game_button_map.get(button) {
|
||||||
|
for ev in evs {
|
||||||
|
events.push(Event::InputUpdate(*ev, is_pressed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(evs) = settings.menu_button_map.get(button) {
|
||||||
|
for ev in evs {
|
||||||
|
events.push(Event::MenuInput(*ev, is_pressed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match event.event {
|
||||||
|
EventType::ButtonPressed(button, code)
|
||||||
|
| EventType::ButtonRepeated(button, code) => {
|
||||||
|
handle_buttons(
|
||||||
|
&self.controller_settings,
|
||||||
|
&mut events,
|
||||||
|
&Button::from((button, code)),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
EventType::ButtonReleased(button, code) => {
|
||||||
|
handle_buttons(
|
||||||
|
&self.controller_settings,
|
||||||
|
&mut events,
|
||||||
|
&Button::from((button, code)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
EventType::ButtonChanged(button, _value, code) => {
|
||||||
|
if let Some(actions) = self
|
||||||
|
.controller_settings
|
||||||
|
.game_analog_button_map
|
||||||
|
.get(&AnalogButton::from((button, code)))
|
||||||
|
{
|
||||||
|
for action in actions {
|
||||||
|
match *action {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(actions) = self
|
||||||
|
.controller_settings
|
||||||
|
.menu_analog_button_map
|
||||||
|
.get(&AnalogButton::from((button, code)))
|
||||||
|
{
|
||||||
|
for action in actions {
|
||||||
|
match *action {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
EventType::AxisChanged(axis, value, code) => {
|
||||||
|
let value = match self
|
||||||
|
.controller_settings
|
||||||
|
.inverted_axes
|
||||||
|
.contains(&Axis::from((axis, code)))
|
||||||
|
{
|
||||||
|
true => value * -1.0,
|
||||||
|
false => value,
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = self
|
||||||
|
.controller_settings
|
||||||
|
.apply_axis_deadzone(&Axis::from((axis, code)), value);
|
||||||
|
|
||||||
|
if self.cursor_grabbed {
|
||||||
|
if let Some(actions) = self
|
||||||
|
.controller_settings
|
||||||
|
.game_axis_map
|
||||||
|
.get(&Axis::from((axis, code)))
|
||||||
|
{
|
||||||
|
for action in actions {
|
||||||
|
match *action {
|
||||||
|
AxisGameAction::MovementX => {
|
||||||
|
events.push(Event::AnalogGameInput(
|
||||||
|
AnalogGameInput::MovementX(value),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
AxisGameAction::MovementY => {
|
||||||
|
events.push(Event::AnalogGameInput(
|
||||||
|
AnalogGameInput::MovementY(value),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
AxisGameAction::CameraX => {
|
||||||
|
events.push(Event::AnalogGameInput(
|
||||||
|
AnalogGameInput::CameraX(
|
||||||
|
value
|
||||||
|
* self.controller_settings.pan_sensitivity
|
||||||
|
as f32
|
||||||
|
/ 100.0,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
AxisGameAction::CameraY => {
|
||||||
|
events.push(Event::AnalogGameInput(
|
||||||
|
AnalogGameInput::CameraY(
|
||||||
|
value
|
||||||
|
* self.controller_settings.pan_sensitivity
|
||||||
|
as f32
|
||||||
|
/ 100.0,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(actions) = self
|
||||||
|
.controller_settings
|
||||||
|
.menu_axis_map
|
||||||
|
.get(&Axis::from((axis, code)))
|
||||||
|
{
|
||||||
|
// TODO: possibly add sensitivity settings when this is used
|
||||||
|
for action in actions {
|
||||||
|
match *action {
|
||||||
|
AxisMenuAction::MoveX => {
|
||||||
|
events.push(Event::AnalogMenuInput(
|
||||||
|
AnalogMenuInput::MoveX(value),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
AxisMenuAction::MoveY => {
|
||||||
|
events.push(Event::AnalogMenuInput(
|
||||||
|
AnalogMenuInput::MoveY(value),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
AxisMenuAction::ScrollX => {
|
||||||
|
events.push(Event::AnalogMenuInput(
|
||||||
|
AnalogMenuInput::ScrollX(value),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
AxisMenuAction::ScrollY => {
|
||||||
|
events.push(Event::AnalogMenuInput(
|
||||||
|
AnalogMenuInput::ScrollY(value),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
EventType::Connected => {},
|
||||||
|
EventType::Disconnected => {},
|
||||||
|
EventType::Dropped => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse emulation for the menus, to be removed when a proper menu navigation
|
||||||
|
// system is available
|
||||||
|
if !self.cursor_grabbed {
|
||||||
|
events = events
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|event| match event {
|
||||||
|
Event::AnalogMenuInput(input) => match input {
|
||||||
|
AnalogMenuInput::MoveX(d) => {
|
||||||
|
self.mouse_emulation_vec.x = d;
|
||||||
|
None
|
||||||
|
},
|
||||||
|
AnalogMenuInput::MoveY(d) => {
|
||||||
|
// This just has to be inverted for some reason
|
||||||
|
self.mouse_emulation_vec.y = d * -1.0;
|
||||||
|
None
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
let event = Event::AnalogMenuInput(input);
|
||||||
|
Some(event)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Event::MenuInput(input, state) => match input {
|
||||||
|
MenuInput::Apply => Some(match state {
|
||||||
|
true => Event::Ui(ui::Event(conrod_core::event::Input::Press(
|
||||||
|
conrod_core::input::Button::Mouse(
|
||||||
|
conrod_core::input::state::mouse::Button::Left,
|
||||||
|
),
|
||||||
|
))),
|
||||||
|
false => Event::Ui(ui::Event(conrod_core::event::Input::Release(
|
||||||
|
conrod_core::input::Button::Mouse(
|
||||||
|
conrod_core::input::state::mouse::Button::Left,
|
||||||
|
),
|
||||||
|
))),
|
||||||
|
}),
|
||||||
|
_ => Some(event),
|
||||||
|
},
|
||||||
|
_ => Some(event),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let sensitivity = self.controller_settings.mouse_emulation_sensitivity;
|
||||||
|
if self.mouse_emulation_vec != Vec2::zero() {
|
||||||
|
self.offset_cursor(self.mouse_emulation_vec * sensitivity as f32)
|
||||||
|
.unwrap_or(());
|
||||||
|
}
|
||||||
|
}
|
||||||
events
|
events
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Moves cursor by an offset
|
||||||
|
pub fn offset_cursor(&self, d: Vec2<f32>) -> Result<(), String> {
|
||||||
|
self.window
|
||||||
|
.window()
|
||||||
|
.set_cursor_position(winit::dpi::LogicalPosition::new(
|
||||||
|
d.x as f64 + self.cursor_position.x,
|
||||||
|
d.y as f64 + self.cursor_position.y,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn swap_buffers(&self) -> Result<(), Error> {
|
pub fn swap_buffers(&self) -> Result<(), Error> {
|
||||||
self.window
|
self.window
|
||||||
.swap_buffers()
|
.swap_buffers()
|
||||||
|
@ -28,5 +28,5 @@ serde_derive = "1.0.102"
|
|||||||
ron = "0.5.1"
|
ron = "0.5.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
minifb = { git = "https://github.com/emoon/rust_minifb.git" }
|
|
||||||
pretty_env_logger = "0.3.0"
|
pretty_env_logger = "0.3.0"
|
||||||
|
minifb = "0.14.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user