mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'shandley/persistence-characters' into 'master'
Character Persistence See merge request veloren/veloren!916
This commit is contained in:
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
**/target
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -33,6 +33,10 @@ maps
|
|||||||
screenshots
|
screenshots
|
||||||
todo.txt
|
todo.txt
|
||||||
|
|
||||||
|
# Game data
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite-journal
|
||||||
|
|
||||||
# direnv
|
# direnv
|
||||||
/.envrc
|
/.envrc
|
||||||
*.bat
|
*.bat
|
||||||
|
@ -69,6 +69,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Villagers tools and clothing
|
- Villagers tools and clothing
|
||||||
- Cultists clothing
|
- Cultists clothing
|
||||||
- You can start the game by pressing "enter" from the character selection menu
|
- You can start the game by pressing "enter" from the character selection menu
|
||||||
|
- Added server-side character saving
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
74
Cargo.lock
generated
74
Cargo.lock
generated
@ -1100,6 +1100,38 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "307dde1a517939465bc4042b47377284a56cee6160f8066f1f5035eb7b25a3fc"
|
checksum = "307dde1a517939465bc4042b47377284a56cee6160f8066f1f5035eb7b25a3fc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diesel"
|
||||||
|
version = "1.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33d7ca63eb2efea87a7f56a283acc49e2ce4b2bd54adf7465dc1d81fef13d8fc"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder 1.3.4",
|
||||||
|
"diesel_derives",
|
||||||
|
"libsqlite3-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diesel_derives"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2 1.0.9",
|
||||||
|
"quote 1.0.3",
|
||||||
|
"syn 1.0.16",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diesel_migrations"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf3cde8413353dc7f5d72fa8ce0b99a560a359d2c5ef1e5817ca731cd9008f4c"
|
||||||
|
dependencies = [
|
||||||
|
"migrations_internals",
|
||||||
|
"migrations_macros",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "directories"
|
name = "directories"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
@ -1166,6 +1198,12 @@ dependencies = [
|
|||||||
"nom 4.2.3",
|
"nom 4.2.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dotenv"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "downcast-rs"
|
name = "downcast-rs"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@ -2401,6 +2439,17 @@ dependencies = [
|
|||||||
"winapi 0.3.8",
|
"winapi 0.3.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsqlite3-sys"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3711dfd91a1081d2458ad2d06ea30a8755256e74038be2ad927d94e1c955ca8"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libssh2-sys"
|
name = "libssh2-sys"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@ -2550,6 +2599,27 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "migrations_internals"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b4fc84e4af020b837029e017966f86a1c2d5e83e64b589963d5047525995860"
|
||||||
|
dependencies = [
|
||||||
|
"diesel",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "migrations_macros"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c"
|
||||||
|
dependencies = [
|
||||||
|
"migrations_internals",
|
||||||
|
"proc-macro2 1.0.9",
|
||||||
|
"quote 1.0.3",
|
||||||
|
"syn 1.0.16",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
@ -4952,8 +5022,12 @@ dependencies = [
|
|||||||
"authc",
|
"authc",
|
||||||
"chrono",
|
"chrono",
|
||||||
"crossbeam",
|
"crossbeam",
|
||||||
|
"diesel",
|
||||||
|
"diesel_migrations",
|
||||||
|
"dotenv",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"libsqlite3-sys",
|
||||||
"log 0.4.8",
|
"log 0.4.8",
|
||||||
"portpicker",
|
"portpicker",
|
||||||
"prometheus",
|
"prometheus",
|
||||||
|
@ -337,11 +337,14 @@ Enjoy your stay in the World of Veloren."#,
|
|||||||
|
|
||||||
|
|
||||||
/// Start chracter selection section
|
/// Start chracter selection section
|
||||||
|
"char_selection.loading_characters": "Loading characters...",
|
||||||
"char_selection.delete_permanently": "Permanently delete this Character?",
|
"char_selection.delete_permanently": "Permanently delete this Character?",
|
||||||
|
"char_selection.deleting_character": "Deleting Character...",
|
||||||
"char_selection.change_server": "Change Server",
|
"char_selection.change_server": "Change Server",
|
||||||
"char_selection.enter_world": "Enter World",
|
"char_selection.enter_world": "Enter World",
|
||||||
"char_selection.logout": "Logout",
|
"char_selection.logout": "Logout",
|
||||||
"char_selection.create_new_charater": "Create New Character",
|
"char_selection.create_new_charater": "Create New Character",
|
||||||
|
"char_selection.creating_character": "Creating Character...",
|
||||||
"char_selection.character_creation": "Character Creation",
|
"char_selection.character_creation": "Character Creation",
|
||||||
|
|
||||||
"char_selection.human_default": "Human Default",
|
"char_selection.human_default": "Human Default",
|
||||||
|
@ -14,6 +14,7 @@ pub use specs::{
|
|||||||
|
|
||||||
use byteorder::{ByteOrder, LittleEndian};
|
use byteorder::{ByteOrder, LittleEndian};
|
||||||
use common::{
|
use common::{
|
||||||
|
character::CharacterItem,
|
||||||
comp::{
|
comp::{
|
||||||
self, ControlAction, ControlEvent, Controller, ControllerInputs, InventoryManip,
|
self, ControlAction, ControlEvent, Controller, ControllerInputs, InventoryManip,
|
||||||
InventoryUpdateEvent,
|
InventoryUpdateEvent,
|
||||||
@ -65,6 +66,7 @@ pub struct Client {
|
|||||||
pub server_info: ServerInfo,
|
pub server_info: ServerInfo,
|
||||||
pub world_map: (Arc<DynamicImage>, Vec2<u32>),
|
pub world_map: (Arc<DynamicImage>, Vec2<u32>),
|
||||||
pub player_list: HashMap<u64, String>,
|
pub player_list: HashMap<u64, String>,
|
||||||
|
pub character_list: CharacterList,
|
||||||
|
|
||||||
postbox: PostBox<ClientMsg, ServerMsg>,
|
postbox: PostBox<ClientMsg, ServerMsg>,
|
||||||
|
|
||||||
@ -83,6 +85,15 @@ pub struct Client {
|
|||||||
pending_chunks: HashMap<Vec2<i32>, Instant>,
|
pending_chunks: HashMap<Vec2<i32>, Instant>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Holds data related to the current players characters, as well as some
|
||||||
|
/// additional state to handle UI.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct CharacterList {
|
||||||
|
pub characters: Vec<CharacterItem>,
|
||||||
|
pub loading: bool,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Create a new `Client`.
|
/// Create a new `Client`.
|
||||||
pub fn new<A: Into<SocketAddr>>(addr: A, view_distance: Option<u32>) -> Result<Self, Error> {
|
pub fn new<A: Into<SocketAddr>>(addr: A, view_distance: Option<u32>) -> Result<Self, Error> {
|
||||||
@ -158,6 +169,7 @@ impl Client {
|
|||||||
server_info,
|
server_info,
|
||||||
world_map,
|
world_map,
|
||||||
player_list: HashMap::new(),
|
player_list: HashMap::new(),
|
||||||
|
character_list: CharacterList::default(),
|
||||||
|
|
||||||
postbox,
|
postbox,
|
||||||
|
|
||||||
@ -224,9 +236,30 @@ impl Client {
|
|||||||
pub fn request_character(&mut self, name: String, body: comp::Body, main: Option<String>) {
|
pub fn request_character(&mut self, name: String, body: comp::Body, main: Option<String>) {
|
||||||
self.postbox
|
self.postbox
|
||||||
.send_message(ClientMsg::Character { name, body, main });
|
.send_message(ClientMsg::Character { name, body, main });
|
||||||
|
|
||||||
self.client_state = ClientState::Pending;
|
self.client_state = ClientState::Pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load the current players character list
|
||||||
|
pub fn load_characters(&mut self) {
|
||||||
|
self.character_list.loading = true;
|
||||||
|
self.postbox.send_message(ClientMsg::RequestCharacterList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// New character creation
|
||||||
|
pub fn create_character(&mut self, alias: String, tool: Option<String>, body: comp::Body) {
|
||||||
|
self.character_list.loading = true;
|
||||||
|
self.postbox
|
||||||
|
.send_message(ClientMsg::CreateCharacter { alias, tool, body });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Character deletion
|
||||||
|
pub fn delete_character(&mut self, character_id: i32) {
|
||||||
|
self.character_list.loading = true;
|
||||||
|
self.postbox
|
||||||
|
.send_message(ClientMsg::DeleteCharacter(character_id));
|
||||||
|
}
|
||||||
|
|
||||||
/// Send disconnect message to the server
|
/// Send disconnect message to the server
|
||||||
pub fn request_logout(&mut self) { self.postbox.send_message(ClientMsg::Disconnect); }
|
pub fn request_logout(&mut self) { self.postbox.send_message(ClientMsg::Disconnect); }
|
||||||
|
|
||||||
@ -819,6 +852,14 @@ impl Client {
|
|||||||
frontend_events.push(Event::Disconnect);
|
frontend_events.push(Event::Disconnect);
|
||||||
self.postbox.send_message(ClientMsg::Terminate);
|
self.postbox.send_message(ClientMsg::Terminate);
|
||||||
},
|
},
|
||||||
|
ServerMsg::CharacterListUpdate(character_list) => {
|
||||||
|
self.character_list.characters = character_list;
|
||||||
|
self.character_list.loading = false;
|
||||||
|
},
|
||||||
|
ServerMsg::CharacterActionError(error) => {
|
||||||
|
warn!("CharacterActionError: {:?}.", error);
|
||||||
|
self.character_list.error = Some(error);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(err) = self.postbox.error() {
|
} else if let Some(err) = self.postbox.error() {
|
||||||
|
20
common/src/character.rs
Normal file
20
common/src/character.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
use crate::comp;
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// The limit on how many characters that a player can have
|
||||||
|
pub const MAX_CHARACTERS_PER_PLAYER: usize = 8;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Character {
|
||||||
|
pub id: Option<i32>,
|
||||||
|
pub alias: String,
|
||||||
|
pub tool: Option<String>, // TODO: Remove once we start persisting inventories
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the character data sent by the server after loading from the
|
||||||
|
/// database.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct CharacterItem {
|
||||||
|
pub character: Character,
|
||||||
|
pub body: comp::Body,
|
||||||
|
}
|
@ -7,7 +7,7 @@ pub struct Body {
|
|||||||
pub body_type: BodyType,
|
pub body_type: BodyType,
|
||||||
pub hair_style: u8,
|
pub hair_style: u8,
|
||||||
pub beard: u8,
|
pub beard: u8,
|
||||||
pub eyebrows: Eyebrows,
|
pub eyebrows: u8,
|
||||||
pub accessory: u8,
|
pub accessory: u8,
|
||||||
pub hair_color: u8,
|
pub hair_color: u8,
|
||||||
pub skin: u8,
|
pub skin: u8,
|
||||||
@ -29,7 +29,7 @@ impl Body {
|
|||||||
body_type,
|
body_type,
|
||||||
hair_style: rng.gen_range(0, race.num_hair_styles(body_type)),
|
hair_style: rng.gen_range(0, race.num_hair_styles(body_type)),
|
||||||
beard: rng.gen_range(0, race.num_beards(body_type)),
|
beard: rng.gen_range(0, race.num_beards(body_type)),
|
||||||
eyebrows: *(&ALL_EYEBROWS).choose(rng).unwrap(),
|
eyebrows: rng.gen_range(0, race.num_eyebrows(body_type)),
|
||||||
accessory: rng.gen_range(0, race.num_accessories(body_type)),
|
accessory: rng.gen_range(0, race.num_accessories(body_type)),
|
||||||
hair_color: rng.gen_range(0, race.num_hair_colors()) as u8,
|
hair_color: rng.gen_range(0, race.num_hair_colors()) as u8,
|
||||||
skin: rng.gen_range(0, race.num_skin_colors()) as u8,
|
skin: rng.gen_range(0, race.num_skin_colors()) as u8,
|
||||||
@ -445,6 +445,8 @@ impl Race {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn num_eyebrows(self, _body_type: BodyType) -> u8 { 1 }
|
||||||
|
|
||||||
pub fn num_beards(self, body_type: BodyType) -> u8 {
|
pub fn num_beards(self, body_type: BodyType) -> u8 {
|
||||||
match (self, body_type) {
|
match (self, body_type) {
|
||||||
(Race::Danari, BodyType::Female) => 1,
|
(Race::Danari, BodyType::Female) => 1,
|
||||||
|
@ -7,14 +7,21 @@ const MAX_ALIAS_LEN: usize = 32;
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
pub alias: String,
|
pub alias: String,
|
||||||
|
pub character_id: Option<i32>,
|
||||||
pub view_distance: Option<u32>,
|
pub view_distance: Option<u32>,
|
||||||
uuid: Uuid,
|
uuid: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
pub fn new(alias: String, view_distance: Option<u32>, uuid: Uuid) -> Self {
|
pub fn new(
|
||||||
|
alias: String,
|
||||||
|
character_id: Option<i32>,
|
||||||
|
view_distance: Option<u32>,
|
||||||
|
uuid: Uuid,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
alias,
|
alias,
|
||||||
|
character_id,
|
||||||
view_distance,
|
view_distance,
|
||||||
uuid,
|
uuid,
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,7 @@ pub enum ServerEvent {
|
|||||||
Mount(EcsEntity, EcsEntity),
|
Mount(EcsEntity, EcsEntity),
|
||||||
Unmount(EcsEntity),
|
Unmount(EcsEntity),
|
||||||
Possess(Uid, Uid),
|
Possess(Uid, Uid),
|
||||||
CreateCharacter {
|
SelectCharacter {
|
||||||
entity: EcsEntity,
|
entity: EcsEntity,
|
||||||
name: String,
|
name: String,
|
||||||
body: comp::Body,
|
body: comp::Body,
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
pub mod astar;
|
pub mod astar;
|
||||||
|
pub mod character;
|
||||||
pub mod clock;
|
pub mod clock;
|
||||||
pub mod comp;
|
pub mod comp;
|
||||||
pub mod effect;
|
pub mod effect;
|
||||||
|
@ -7,6 +7,13 @@ pub enum ClientMsg {
|
|||||||
view_distance: Option<u32>,
|
view_distance: Option<u32>,
|
||||||
token_or_username: String,
|
token_or_username: String,
|
||||||
},
|
},
|
||||||
|
RequestCharacterList,
|
||||||
|
CreateCharacter {
|
||||||
|
alias: String,
|
||||||
|
tool: Option<String>,
|
||||||
|
body: comp::Body,
|
||||||
|
},
|
||||||
|
DeleteCharacter(i32),
|
||||||
Character {
|
Character {
|
||||||
name: String,
|
name: String,
|
||||||
body: comp::Body,
|
body: comp::Body,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use super::{ClientState, EcsCompPacket};
|
use super::{ClientState, EcsCompPacket};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
character::CharacterItem,
|
||||||
comp, state, sync,
|
comp, state, sync,
|
||||||
terrain::{Block, TerrainChunk},
|
terrain::{Block, TerrainChunk},
|
||||||
ChatType,
|
ChatType,
|
||||||
@ -33,6 +34,10 @@ pub enum ServerMsg {
|
|||||||
time_of_day: state::TimeOfDay,
|
time_of_day: state::TimeOfDay,
|
||||||
world_map: (Vec2<u32>, Vec<u32>),
|
world_map: (Vec2<u32>, Vec<u32>),
|
||||||
},
|
},
|
||||||
|
/// A list of characters belonging to the a authenticated player was sent
|
||||||
|
CharacterListUpdate(Vec<CharacterItem>),
|
||||||
|
/// An error occured while creating or deleting a character
|
||||||
|
CharacterActionError(String),
|
||||||
PlayerListUpdate(PlayerListUpdate),
|
PlayerListUpdate(PlayerListUpdate),
|
||||||
StateAnswer(Result<ClientState, (RequestStateError, ClientState)>),
|
StateAnswer(Result<ClientState, (RequestStateError, ClientState)>),
|
||||||
/// Trigger cleanup for when the client goes back to the `Registered` state
|
/// Trigger cleanup for when the client goes back to the `Registered` state
|
||||||
|
@ -8,6 +8,8 @@ services:
|
|||||||
- "14004:14004"
|
- "14004:14004"
|
||||||
- "14005:14005"
|
- "14005:14005"
|
||||||
restart: on-failure:0
|
restart: on-failure:0
|
||||||
|
volumes:
|
||||||
|
- "./db:/opt/db"
|
||||||
watchtower:
|
watchtower:
|
||||||
image: containrrr/watchtower
|
image: containrrr/watchtower
|
||||||
volumes:
|
volumes:
|
||||||
|
@ -32,3 +32,7 @@ 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" }
|
authc = { git = "https://gitlab.com/veloren/auth.git", rev = "65571ade0d954a0e0bd995fdb314854ff146ab97" }
|
||||||
|
libsqlite3-sys = { version = "0.9.1", features = ["bundled"] }
|
||||||
|
diesel = { version = "1.4.3", features = ["sqlite"] }
|
||||||
|
diesel_migrations = "1.4.0"
|
||||||
|
dotenv = "0.15.0"
|
@ -69,7 +69,7 @@ impl Server {
|
|||||||
ServerEvent::Possess(possessor_uid, possesse_uid) => {
|
ServerEvent::Possess(possessor_uid, possesse_uid) => {
|
||||||
handle_possess(&self, possessor_uid, possesse_uid)
|
handle_possess(&self, possessor_uid, possesse_uid)
|
||||||
},
|
},
|
||||||
ServerEvent::CreateCharacter {
|
ServerEvent::SelectCharacter {
|
||||||
entity,
|
entity,
|
||||||
name,
|
name,
|
||||||
body,
|
body,
|
||||||
|
@ -9,6 +9,7 @@ pub mod error;
|
|||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod metrics;
|
pub mod metrics;
|
||||||
|
pub mod persistence;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
pub mod state_ext;
|
pub mod state_ext;
|
||||||
pub mod sys;
|
pub mod sys;
|
||||||
@ -54,6 +55,9 @@ use world::{
|
|||||||
World,
|
World,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[macro_use] extern crate diesel;
|
||||||
|
#[macro_use] extern crate diesel_migrations;
|
||||||
|
|
||||||
const CLIENT_TIMEOUT: f64 = 20.0; // Seconds
|
const CLIENT_TIMEOUT: f64 = 20.0; // Seconds
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
@ -215,7 +219,16 @@ impl Server {
|
|||||||
.expect("Failed to initialize server metrics submodule."),
|
.expect("Failed to initialize server metrics submodule."),
|
||||||
server_settings: settings.clone(),
|
server_settings: settings.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Run pending DB migrations (if any)
|
||||||
|
debug!("Running DB migrations...");
|
||||||
|
|
||||||
|
if let Some(error) = persistence::run_migrations().err() {
|
||||||
|
log::info!("Migration error: {}", format!("{:#?}", error));
|
||||||
|
}
|
||||||
|
|
||||||
debug!("created veloren server with: {:?}", &settings);
|
debug!("created veloren server with: {:?}", &settings);
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Server version: {}[{}]",
|
"Server version: {}[{}]",
|
||||||
*common::util::GIT_HASH,
|
*common::util::GIT_HASH,
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS "character";
|
6
server/src/migrations/2020-04-11-202519_character/up.sql
Normal file
6
server/src/migrations/2020-04-11-202519_character/up.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "character" (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
player_uuid TEXT NOT NULL,
|
||||||
|
alias TEXT NOT NULL,
|
||||||
|
tool TEXT
|
||||||
|
);
|
1
server/src/migrations/2020-04-19-025352_body/down.sql
Normal file
1
server/src/migrations/2020-04-19-025352_body/down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS "body";
|
13
server/src/migrations/2020-04-19-025352_body/up.sql
Normal file
13
server/src/migrations/2020-04-19-025352_body/up.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "body" (
|
||||||
|
character_id INT NOT NULL PRIMARY KEY,
|
||||||
|
race SMALLINT NOT NULL,
|
||||||
|
body_type SMALLINT NOT NULL,
|
||||||
|
hair_style SMALLINT NOT NULL,
|
||||||
|
beard SMALLINT NOT NULL,
|
||||||
|
eyebrows SMALLINT NOT NULL,
|
||||||
|
accessory SMALLINT NOT NULL,
|
||||||
|
hair_color SMALLINT NOT NULL,
|
||||||
|
skin SMALLINT NOT NULL,
|
||||||
|
eye_color SMALLINT NOT NULL,
|
||||||
|
FOREIGN KEY(character_id) REFERENCES "character"(id) ON DELETE CASCADE
|
||||||
|
);
|
1
server/src/persistence/.env
Normal file
1
server/src/persistence/.env
Normal file
@ -0,0 +1 @@
|
|||||||
|
DATABASE_URL=../../../saves/db.sqlite
|
125
server/src/persistence/character.rs
Normal file
125
server/src/persistence/character.rs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
extern crate diesel;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
establish_connection,
|
||||||
|
models::{Body, Character, NewCharacter},
|
||||||
|
schema, Error,
|
||||||
|
};
|
||||||
|
use crate::comp;
|
||||||
|
use common::character::{Character as CharacterData, CharacterItem, MAX_CHARACTERS_PER_PLAYER};
|
||||||
|
use diesel::prelude::*;
|
||||||
|
|
||||||
|
type CharacterListResult = Result<Vec<CharacterItem>, Error>;
|
||||||
|
|
||||||
|
// Loading of characters happens immediately after login, and the data is only
|
||||||
|
// for the purpose of rendering the character and their level in the character
|
||||||
|
// list.
|
||||||
|
pub fn load_characters(uuid: &str) -> CharacterListResult {
|
||||||
|
use schema::{body, character::dsl::*};
|
||||||
|
|
||||||
|
let data: Vec<(Character, Body)> = character
|
||||||
|
.filter(player_uuid.eq(uuid))
|
||||||
|
.order(id.desc())
|
||||||
|
.inner_join(body::table)
|
||||||
|
.load::<(Character, Body)>(&establish_connection())?;
|
||||||
|
|
||||||
|
Ok(data
|
||||||
|
.iter()
|
||||||
|
.map(|(character_data, body_data)| CharacterItem {
|
||||||
|
character: CharacterData::from(character_data),
|
||||||
|
body: comp::Body::from(body_data),
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new character with provided comp::Character and comp::Body data.
|
||||||
|
/// Note that sqlite does not suppport returning the inserted data after a
|
||||||
|
/// successful insert. To workaround, we wrap this in a transaction which
|
||||||
|
/// inserts, queries for the newly created chaacter id, then uses the character
|
||||||
|
/// id for insertion of the `body` table entry
|
||||||
|
pub fn create_character(
|
||||||
|
uuid: &str,
|
||||||
|
alias: String,
|
||||||
|
tool: Option<String>,
|
||||||
|
body: &comp::Body,
|
||||||
|
) -> CharacterListResult {
|
||||||
|
check_character_limit(uuid)?;
|
||||||
|
|
||||||
|
let new_character = NewCharacter {
|
||||||
|
player_uuid: uuid,
|
||||||
|
alias: &alias,
|
||||||
|
tool: tool.as_deref(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let connection = establish_connection();
|
||||||
|
|
||||||
|
connection.transaction::<_, diesel::result::Error, _>(|| {
|
||||||
|
use schema::{body, character, character::dsl::*};
|
||||||
|
|
||||||
|
match body {
|
||||||
|
comp::Body::Humanoid(body_data) => {
|
||||||
|
diesel::insert_into(character::table)
|
||||||
|
.values(&new_character)
|
||||||
|
.execute(&connection)?;
|
||||||
|
|
||||||
|
let inserted_character = character
|
||||||
|
.filter(player_uuid.eq(uuid))
|
||||||
|
.order(id.desc())
|
||||||
|
.first::<Character>(&connection)?;
|
||||||
|
|
||||||
|
let new_body = Body {
|
||||||
|
character_id: inserted_character.id as i32,
|
||||||
|
race: body_data.race as i16,
|
||||||
|
body_type: body_data.body_type as i16,
|
||||||
|
hair_style: body_data.hair_style as i16,
|
||||||
|
beard: body_data.beard as i16,
|
||||||
|
eyebrows: body_data.eyebrows as i16,
|
||||||
|
accessory: body_data.accessory as i16,
|
||||||
|
hair_color: body_data.hair_color as i16,
|
||||||
|
skin: body_data.skin as i16,
|
||||||
|
eye_color: body_data.eye_color as i16,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(body::table)
|
||||||
|
.values(&new_body)
|
||||||
|
.execute(&connection)?;
|
||||||
|
},
|
||||||
|
_ => log::warn!("Creating non-humanoid characters is not supported."),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
load_characters(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_character(uuid: &str, character_id: i32) -> CharacterListResult {
|
||||||
|
use schema::character::dsl::*;
|
||||||
|
|
||||||
|
diesel::delete(character.filter(id.eq(character_id))).execute(&establish_connection())?;
|
||||||
|
|
||||||
|
load_characters(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_character_limit(uuid: &str) -> Result<(), Error> {
|
||||||
|
use diesel::dsl::count_star;
|
||||||
|
use schema::character::dsl::*;
|
||||||
|
|
||||||
|
let connection = establish_connection();
|
||||||
|
|
||||||
|
let character_count = character
|
||||||
|
.select(count_star())
|
||||||
|
.filter(player_uuid.eq(uuid))
|
||||||
|
.load::<i64>(&connection)?;
|
||||||
|
|
||||||
|
match character_count.first() {
|
||||||
|
Some(count) => {
|
||||||
|
if count < &(MAX_CHARACTERS_PER_PLAYER as i64) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::CharacterLimitReached)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
5
server/src/persistence/diesel.toml
Normal file
5
server/src/persistence/diesel.toml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# For documentation on how to configure this file,
|
||||||
|
# see diesel.rs/guides/configuring-diesel-cli
|
||||||
|
|
||||||
|
[print_schema]
|
||||||
|
file = "schema.rs"
|
62
server/src/persistence/mod.rs
Normal file
62
server/src/persistence/mod.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
pub mod character;
|
||||||
|
mod models;
|
||||||
|
|
||||||
|
mod schema;
|
||||||
|
|
||||||
|
extern crate diesel;
|
||||||
|
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel_migrations::embed_migrations;
|
||||||
|
use std::{env, fmt, fs, path::Path};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
// The player has alredy reached the max character limit
|
||||||
|
CharacterLimitReached,
|
||||||
|
// An error occured when performing a database action
|
||||||
|
DatabaseError(diesel::result::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", match self {
|
||||||
|
Self::DatabaseError(diesel_error) => diesel_error.to_string(),
|
||||||
|
Self::CharacterLimitReached => String::from("Character limit exceeded"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<diesel::result::Error> for Error {
|
||||||
|
fn from(error: diesel::result::Error) -> Error { Error::DatabaseError(error) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// See: https://docs.rs/diesel_migrations/1.4.0/diesel_migrations/macro.embed_migrations.html
|
||||||
|
// This macro is called at build-time, and produces the necessary migration info
|
||||||
|
// for the `embedded_migrations` call below.
|
||||||
|
embed_migrations!();
|
||||||
|
|
||||||
|
pub fn run_migrations() -> Result<(), diesel_migrations::RunMigrationsError> {
|
||||||
|
let _ = fs::create_dir(format!("{}/saves/", binary_absolute_path()));
|
||||||
|
embedded_migrations::run_with_output(&establish_connection(), &mut std::io::stdout())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn establish_connection() -> SqliteConnection {
|
||||||
|
let database_url = format!("{}/saves/db.sqlite", binary_absolute_path());
|
||||||
|
SqliteConnection::establish(&database_url)
|
||||||
|
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the absolute path of the binary so that the database is always stored
|
||||||
|
// beside it, no matter where the binary is run from
|
||||||
|
fn binary_absolute_path() -> String {
|
||||||
|
let binary_path;
|
||||||
|
match env::current_exe() {
|
||||||
|
Ok(exe_path) => binary_path = exe_path,
|
||||||
|
Err(e) => panic!("Failed to get current exe path: {}", e),
|
||||||
|
};
|
||||||
|
|
||||||
|
match Path::new(&binary_path.display().to_string()).parent() {
|
||||||
|
Some(path) => return path.display().to_string(),
|
||||||
|
None => panic!("Failed to get current exe parent path"),
|
||||||
|
};
|
||||||
|
}
|
65
server/src/persistence/models.rs
Normal file
65
server/src/persistence/models.rs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
use super::schema::{body, character};
|
||||||
|
use crate::comp;
|
||||||
|
use common::character::Character as CharacterData;
|
||||||
|
|
||||||
|
/// `Character` represents a playable character belonging to a player
|
||||||
|
#[derive(Identifiable, Queryable, Debug)]
|
||||||
|
#[table_name = "character"]
|
||||||
|
pub struct Character {
|
||||||
|
pub id: i32,
|
||||||
|
pub player_uuid: String,
|
||||||
|
pub alias: String,
|
||||||
|
pub tool: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable)]
|
||||||
|
#[table_name = "character"]
|
||||||
|
pub struct NewCharacter<'a> {
|
||||||
|
pub player_uuid: &'a str,
|
||||||
|
pub alias: &'a str,
|
||||||
|
pub tool: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Character> for CharacterData {
|
||||||
|
fn from(character: &Character) -> CharacterData {
|
||||||
|
CharacterData {
|
||||||
|
id: Some(character.id),
|
||||||
|
alias: String::from(&character.alias),
|
||||||
|
tool: character.tool.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Body` represents the body variety for a character
|
||||||
|
#[derive(Associations, Identifiable, Queryable, Debug, Insertable)]
|
||||||
|
#[belongs_to(Character)]
|
||||||
|
#[primary_key(character_id)]
|
||||||
|
#[table_name = "body"]
|
||||||
|
pub struct Body {
|
||||||
|
pub character_id: i32,
|
||||||
|
pub race: i16,
|
||||||
|
pub body_type: i16,
|
||||||
|
pub hair_style: i16,
|
||||||
|
pub beard: i16,
|
||||||
|
pub eyebrows: i16,
|
||||||
|
pub accessory: i16,
|
||||||
|
pub hair_color: i16,
|
||||||
|
pub skin: i16,
|
||||||
|
pub eye_color: i16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Body> for comp::Body {
|
||||||
|
fn from(body: &Body) -> comp::Body {
|
||||||
|
comp::Body::Humanoid(comp::humanoid::Body {
|
||||||
|
race: comp::humanoid::ALL_RACES[body.race as usize],
|
||||||
|
body_type: comp::humanoid::ALL_BODY_TYPES[body.body_type as usize],
|
||||||
|
hair_style: body.hair_style as u8,
|
||||||
|
beard: body.beard as u8,
|
||||||
|
eyebrows: body.eyebrows as u8,
|
||||||
|
accessory: body.accessory as u8,
|
||||||
|
hair_color: body.hair_color as u8,
|
||||||
|
skin: body.skin as u8,
|
||||||
|
eye_color: body.eye_color as u8,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
27
server/src/persistence/schema.rs
Normal file
27
server/src/persistence/schema.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
table! {
|
||||||
|
body (character_id) {
|
||||||
|
character_id -> Integer,
|
||||||
|
race -> SmallInt,
|
||||||
|
body_type -> SmallInt,
|
||||||
|
hair_style -> SmallInt,
|
||||||
|
beard -> SmallInt,
|
||||||
|
eyebrows -> SmallInt,
|
||||||
|
accessory -> SmallInt,
|
||||||
|
hair_color -> SmallInt,
|
||||||
|
skin -> SmallInt,
|
||||||
|
eye_color -> SmallInt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
character (id) {
|
||||||
|
id -> Integer,
|
||||||
|
player_uuid -> Text,
|
||||||
|
alias -> Text,
|
||||||
|
tool -> Nullable<Text>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
joinable!(body -> character (character_id));
|
||||||
|
|
||||||
|
allow_tables_to_appear_in_same_query!(body, character,);
|
@ -1,5 +1,5 @@
|
|||||||
use super::SysTimer;
|
use super::SysTimer;
|
||||||
use crate::{auth_provider::AuthProvider, client::Client, CLIENT_TIMEOUT};
|
use crate::{auth_provider::AuthProvider, client::Client, persistence, CLIENT_TIMEOUT};
|
||||||
use common::{
|
use common::{
|
||||||
comp::{Admin, CanBuild, ControlEvent, Controller, ForceUpdate, Ori, Player, Pos, Stats, Vel},
|
comp::{Admin, CanBuild, ControlEvent, Controller, ForceUpdate, Ori, Player, Pos, Stats, Vel},
|
||||||
event::{EventBus, ServerEvent},
|
event::{EventBus, ServerEvent},
|
||||||
@ -134,7 +134,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
Ok((username, uuid)) => (username, uuid),
|
Ok((username, uuid)) => (username, uuid),
|
||||||
};
|
};
|
||||||
|
|
||||||
let player = Player::new(username, view_distance, uuid);
|
let player = Player::new(username, None, view_distance, uuid);
|
||||||
|
|
||||||
if !player.is_valid() {
|
if !player.is_valid() {
|
||||||
// Invalid player
|
// Invalid player
|
||||||
@ -154,6 +154,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
client.notify(ServerMsg::PlayerListUpdate(PlayerListUpdate::Init(
|
client.notify(ServerMsg::PlayerListUpdate(PlayerListUpdate::Init(
|
||||||
player_list.clone(),
|
player_list.clone(),
|
||||||
)));
|
)));
|
||||||
|
|
||||||
// Add to list to notify all clients of the new player
|
// Add to list to notify all clients of the new player
|
||||||
new_players.push(entity);
|
new_players.push(entity);
|
||||||
},
|
},
|
||||||
@ -174,12 +175,11 @@ impl<'a> System<'a> for Sys {
|
|||||||
// Become Registered first.
|
// Become Registered first.
|
||||||
ClientState::Connected => client.error_state(RequestStateError::Impossible),
|
ClientState::Connected => client.error_state(RequestStateError::Impossible),
|
||||||
ClientState::Registered | ClientState::Spectator => {
|
ClientState::Registered | ClientState::Spectator => {
|
||||||
if let (Some(player), false) = (
|
// Only send login message if it wasn't already
|
||||||
players.get(entity),
|
// sent previously
|
||||||
// Only send login message if it wasn't already sent
|
if let (Some(player), false) =
|
||||||
// previously
|
(players.get(entity), client.login_msg_sent)
|
||||||
client.login_msg_sent,
|
{
|
||||||
) {
|
|
||||||
new_chat_msgs.push((
|
new_chat_msgs.push((
|
||||||
None,
|
None,
|
||||||
ServerMsg::broadcast(format!(
|
ServerMsg::broadcast(format!(
|
||||||
@ -187,10 +187,11 @@ impl<'a> System<'a> for Sys {
|
|||||||
&player.alias
|
&player.alias
|
||||||
)),
|
)),
|
||||||
));
|
));
|
||||||
|
|
||||||
client.login_msg_sent = true;
|
client.login_msg_sent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
server_emitter.emit(ServerEvent::CreateCharacter {
|
server_emitter.emit(ServerEvent::SelectCharacter {
|
||||||
entity,
|
entity,
|
||||||
name,
|
name,
|
||||||
body,
|
body,
|
||||||
@ -308,6 +309,55 @@ impl<'a> System<'a> for Sys {
|
|||||||
ClientMsg::Terminate => {
|
ClientMsg::Terminate => {
|
||||||
server_emitter.emit(ServerEvent::ClientDisconnect(entity));
|
server_emitter.emit(ServerEvent::ClientDisconnect(entity));
|
||||||
},
|
},
|
||||||
|
ClientMsg::RequestCharacterList => {
|
||||||
|
if let Some(player) = players.get(entity) {
|
||||||
|
match persistence::character::load_characters(
|
||||||
|
&player.uuid().to_string(),
|
||||||
|
) {
|
||||||
|
Ok(character_list) => {
|
||||||
|
client.notify(ServerMsg::CharacterListUpdate(character_list));
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
client
|
||||||
|
.notify(ServerMsg::CharacterActionError(error.to_string()));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ClientMsg::CreateCharacter { alias, tool, body } => {
|
||||||
|
if let Some(player) = players.get(entity) {
|
||||||
|
match persistence::character::create_character(
|
||||||
|
&player.uuid().to_string(),
|
||||||
|
alias,
|
||||||
|
tool,
|
||||||
|
&body,
|
||||||
|
) {
|
||||||
|
Ok(character_list) => {
|
||||||
|
client.notify(ServerMsg::CharacterListUpdate(character_list));
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
client
|
||||||
|
.notify(ServerMsg::CharacterActionError(error.to_string()));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ClientMsg::DeleteCharacter(character_id) => {
|
||||||
|
if let Some(player) = players.get(entity) {
|
||||||
|
match persistence::character::delete_character(
|
||||||
|
&player.uuid().to_string(),
|
||||||
|
character_id,
|
||||||
|
) {
|
||||||
|
Ok(character_list) => {
|
||||||
|
client.notify(ServerMsg::CharacterListUpdate(character_list));
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
client
|
||||||
|
.notify(ServerMsg::CharacterActionError(error.to_string()));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ pub mod key_state;
|
|||||||
pub mod logging;
|
pub mod logging;
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
pub mod mesh;
|
pub mod mesh;
|
||||||
pub mod meta;
|
|
||||||
pub mod render;
|
pub mod render;
|
||||||
pub mod scene;
|
pub mod scene;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
@ -27,15 +26,11 @@ pub mod window;
|
|||||||
// Reexports
|
// Reexports
|
||||||
pub use crate::error::Error;
|
pub use crate::error::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{audio::AudioFrontend, settings::Settings, singleplayer::Singleplayer, window::Window};
|
||||||
audio::AudioFrontend, meta::Meta, settings::Settings, singleplayer::Singleplayer,
|
|
||||||
window::Window,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A type used to store state that is shared between all play states.
|
/// A type used to store state that is shared between all play states.
|
||||||
pub struct GlobalState {
|
pub struct GlobalState {
|
||||||
pub settings: Settings,
|
pub settings: Settings,
|
||||||
pub meta: Meta,
|
|
||||||
pub window: Window,
|
pub window: Window,
|
||||||
pub audio: AudioFrontend,
|
pub audio: AudioFrontend,
|
||||||
pub info_message: Option<String>,
|
pub info_message: Option<String>,
|
||||||
|
@ -7,7 +7,6 @@ use veloren_voxygen::{
|
|||||||
i18n::{self, i18n_asset_key, VoxygenLocalization},
|
i18n::{self, i18n_asset_key, VoxygenLocalization},
|
||||||
logging,
|
logging,
|
||||||
menu::main::MainMenuState,
|
menu::main::MainMenuState,
|
||||||
meta::Meta,
|
|
||||||
settings::{AudioOutput, Settings},
|
settings::{AudioOutput, Settings},
|
||||||
window::Window,
|
window::Window,
|
||||||
Direction, GlobalState, PlayState, PlayStateResult,
|
Direction, GlobalState, PlayState, PlayStateResult,
|
||||||
@ -39,9 +38,6 @@ fn main() {
|
|||||||
|
|
||||||
logging::init(&settings, term_log_level, file_log_level);
|
logging::init(&settings, term_log_level, file_log_level);
|
||||||
|
|
||||||
// Load metadata
|
|
||||||
let meta = Meta::load();
|
|
||||||
|
|
||||||
// Save settings to add new fields or create the file if it is not already there
|
// Save settings to add new fields or create the file if it is not already there
|
||||||
if let Err(err) = settings.save_to_file() {
|
if let Err(err) = settings.save_to_file() {
|
||||||
panic!("Failed to save settings: {:?}", err);
|
panic!("Failed to save settings: {:?}", err);
|
||||||
@ -62,7 +58,6 @@ fn main() {
|
|||||||
audio,
|
audio,
|
||||||
window: Window::new(&settings).expect("Failed to create window!"),
|
window: Window::new(&settings).expect("Failed to create window!"),
|
||||||
settings,
|
settings,
|
||||||
meta,
|
|
||||||
info_message: None,
|
info_message: None,
|
||||||
singleplayer: None,
|
singleplayer: None,
|
||||||
};
|
};
|
||||||
@ -221,7 +216,6 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save any unsaved changes to settings and meta
|
// Save any unsaved changes to settings
|
||||||
global_state.settings.save_to_file_warn();
|
global_state.settings.save_to_file_warn();
|
||||||
global_state.meta.save_to_file_warn();
|
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,9 @@ impl PlayState for CharSelectionState {
|
|||||||
// Set up an fps clock.
|
// Set up an fps clock.
|
||||||
let mut clock = Clock::start();
|
let mut clock = Clock::start();
|
||||||
|
|
||||||
|
// Load the player's character list
|
||||||
|
self.client.borrow_mut().load_characters();
|
||||||
|
|
||||||
let mut current_client_state = self.client.borrow().get_client_state();
|
let mut current_client_state = self.client.borrow().get_client_state();
|
||||||
while let ClientState::Pending | ClientState::Registered = current_client_state {
|
while let ClientState::Pending | ClientState::Registered = current_client_state {
|
||||||
// Handle window events
|
// Handle window events
|
||||||
@ -62,22 +65,35 @@ impl PlayState for CharSelectionState {
|
|||||||
// Maintain the UI.
|
// Maintain the UI.
|
||||||
let events = self
|
let events = self
|
||||||
.char_selection_ui
|
.char_selection_ui
|
||||||
.maintain(global_state, &self.client.borrow());
|
.maintain(global_state, &mut self.client.borrow_mut());
|
||||||
|
|
||||||
for event in events {
|
for event in events {
|
||||||
match event {
|
match event {
|
||||||
ui::Event::Logout => {
|
ui::Event::Logout => {
|
||||||
return PlayStateResult::Pop;
|
return PlayStateResult::Pop;
|
||||||
},
|
},
|
||||||
|
ui::Event::AddCharacter { alias, tool, body } => {
|
||||||
|
self.client.borrow_mut().create_character(alias, tool, body);
|
||||||
|
},
|
||||||
|
ui::Event::DeleteCharacter(character_id) => {
|
||||||
|
self.client.borrow_mut().delete_character(character_id);
|
||||||
|
},
|
||||||
ui::Event::Play => {
|
ui::Event::Play => {
|
||||||
let char_data = self
|
let char_data = self
|
||||||
.char_selection_ui
|
.char_selection_ui
|
||||||
.get_character_data()
|
.get_character_list()
|
||||||
.expect("Character data is required to play");
|
.expect("Character data is required to play");
|
||||||
self.client.borrow_mut().request_character(
|
|
||||||
char_data.name,
|
if let Some(selected_character) =
|
||||||
char_data.body,
|
char_data.get(self.char_selection_ui.selected_character)
|
||||||
char_data.tool,
|
{
|
||||||
);
|
self.client.borrow_mut().request_character(
|
||||||
|
selected_character.character.alias.clone(),
|
||||||
|
selected_character.body,
|
||||||
|
selected_character.character.tool.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return PlayStateResult::Switch(Box::new(SessionState::new(
|
return PlayStateResult::Switch(Box::new(SessionState::new(
|
||||||
global_state,
|
global_state,
|
||||||
self.client.clone(),
|
self.client.clone(),
|
||||||
@ -91,10 +107,16 @@ impl PlayState for CharSelectionState {
|
|||||||
|
|
||||||
let humanoid_body = self
|
let humanoid_body = self
|
||||||
.char_selection_ui
|
.char_selection_ui
|
||||||
.get_character_data()
|
.get_character_list()
|
||||||
.and_then(|data| match data.body {
|
.and_then(|data| {
|
||||||
comp::Body::Humanoid(body) => Some(body),
|
if let Some(character) = data.get(self.char_selection_ui.selected_character) {
|
||||||
_ => None,
|
match character.body {
|
||||||
|
comp::Body::Humanoid(body) => Some(body),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Maintain the scene.
|
// Maintain the scene.
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
i18n::{i18n_asset_key, VoxygenLocalization},
|
i18n::{i18n_asset_key, VoxygenLocalization},
|
||||||
meta::CharacterData,
|
|
||||||
render::{Consts, Globals, Renderer},
|
render::{Consts, Globals, Renderer},
|
||||||
ui::{
|
ui::{
|
||||||
fonts::ConrodVoxygenFonts,
|
fonts::ConrodVoxygenFonts,
|
||||||
@ -14,6 +13,7 @@ use client::Client;
|
|||||||
use common::{
|
use common::{
|
||||||
assets,
|
assets,
|
||||||
assets::{load, load_expect},
|
assets::{load, load_expect},
|
||||||
|
character::{Character, CharacterItem, MAX_CHARACTERS_PER_PLAYER},
|
||||||
comp::{self, humanoid},
|
comp::{self, humanoid},
|
||||||
};
|
};
|
||||||
use conrod_core::{
|
use conrod_core::{
|
||||||
@ -65,6 +65,10 @@ widget_ids! {
|
|||||||
info_no,
|
info_no,
|
||||||
delete_text,
|
delete_text,
|
||||||
space,
|
space,
|
||||||
|
loading_characters_text,
|
||||||
|
creating_character_text,
|
||||||
|
deleting_character_text,
|
||||||
|
character_error_message,
|
||||||
|
|
||||||
// REMOVE THIS AFTER IMPLEMENTATION
|
// REMOVE THIS AFTER IMPLEMENTATION
|
||||||
daggers_grey,
|
daggers_grey,
|
||||||
@ -244,19 +248,41 @@ rotation_image_ids! {
|
|||||||
pub enum Event {
|
pub enum Event {
|
||||||
Logout,
|
Logout,
|
||||||
Play,
|
Play,
|
||||||
|
AddCharacter {
|
||||||
|
alias: String,
|
||||||
|
tool: Option<String>,
|
||||||
|
body: comp::Body,
|
||||||
|
},
|
||||||
|
DeleteCharacter(i32),
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
|
const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
|
||||||
const TEXT_COLOR_2: Color = Color::Rgba(1.0, 1.0, 1.0, 0.2);
|
const TEXT_COLOR_2: Color = Color::Rgba(1.0, 1.0, 1.0, 0.2);
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
enum InfoContent {
|
enum InfoContent {
|
||||||
None,
|
None,
|
||||||
Deletion(usize),
|
Deletion(usize),
|
||||||
//Name,
|
LoadingCharacters,
|
||||||
|
CreatingCharacter,
|
||||||
|
DeletingCharacter,
|
||||||
|
CharacterError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InfoContent {
|
||||||
|
pub fn has_content(&self, character_list_loading: &bool) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::None => false,
|
||||||
|
Self::CreatingCharacter | Self::DeletingCharacter | Self::LoadingCharacters => {
|
||||||
|
*character_list_loading
|
||||||
|
},
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
Select(Option<CharacterData>),
|
Select(Option<Vec<CharacterItem>>),
|
||||||
Create {
|
Create {
|
||||||
name: String,
|
name: String,
|
||||||
body: humanoid::Body,
|
body: humanoid::Body,
|
||||||
@ -271,16 +297,10 @@ pub struct CharSelectionUi {
|
|||||||
imgs: Imgs,
|
imgs: Imgs,
|
||||||
rot_imgs: ImgsRot,
|
rot_imgs: ImgsRot,
|
||||||
fonts: ConrodVoxygenFonts,
|
fonts: ConrodVoxygenFonts,
|
||||||
//character_creation: bool,
|
|
||||||
info_content: InfoContent,
|
info_content: InfoContent,
|
||||||
voxygen_i18n: Arc<VoxygenLocalization>,
|
voxygen_i18n: Arc<VoxygenLocalization>,
|
||||||
//deletion_confirmation: bool,
|
|
||||||
/*
|
|
||||||
pub character_name: String,
|
|
||||||
pub character_body: humanoid::Body,
|
|
||||||
pub character_tool: Option<&'static str>,
|
|
||||||
*/
|
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
|
pub selected_character: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CharSelectionUi {
|
impl CharSelectionUi {
|
||||||
@ -303,83 +323,86 @@ impl CharSelectionUi {
|
|||||||
let fonts = ConrodVoxygenFonts::load(&voxygen_i18n.fonts, &mut ui)
|
let fonts = ConrodVoxygenFonts::load(&voxygen_i18n.fonts, &mut ui)
|
||||||
.expect("Impossible to load fonts!");
|
.expect("Impossible to load fonts!");
|
||||||
|
|
||||||
// TODO: Randomize initial values.
|
|
||||||
Self {
|
Self {
|
||||||
ui,
|
ui,
|
||||||
ids,
|
ids,
|
||||||
imgs,
|
imgs,
|
||||||
rot_imgs,
|
rot_imgs,
|
||||||
fonts,
|
fonts,
|
||||||
info_content: InfoContent::None,
|
info_content: InfoContent::LoadingCharacters,
|
||||||
|
selected_character: 0,
|
||||||
voxygen_i18n,
|
voxygen_i18n,
|
||||||
//deletion_confirmation: false,
|
|
||||||
/*
|
|
||||||
character_creation: false,
|
|
||||||
selected_language: global_state.settings.language.selected_language.clone(),
|
|
||||||
character_name: "Character Name".to_string(),
|
|
||||||
character_body: humanoid::Body::random(),
|
|
||||||
character_tool: Some(STARTER_SWORD),
|
|
||||||
*/
|
|
||||||
mode: Mode::Select(None),
|
mode: Mode::Select(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_character_data(&self) -> Option<CharacterData> {
|
pub fn get_character_list(&self) -> Option<Vec<CharacterItem>> {
|
||||||
match &self.mode {
|
match &self.mode {
|
||||||
Mode::Select(data) => data.clone(),
|
Mode::Select(data) => data.clone(),
|
||||||
Mode::Create {
|
Mode::Create {
|
||||||
name, body, tool, ..
|
name, body, tool, ..
|
||||||
} => Some(CharacterData {
|
} => Some(vec![CharacterItem {
|
||||||
name: name.clone(),
|
character: Character {
|
||||||
|
id: None,
|
||||||
|
alias: name.clone(),
|
||||||
|
tool: tool.map(|specifier| specifier.to_string()),
|
||||||
|
},
|
||||||
body: comp::Body::Humanoid(body.clone()),
|
body: comp::Body::Humanoid(body.clone()),
|
||||||
tool: tool.map(|specifier| specifier.to_string()),
|
}]),
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_loadout(&mut self) -> Option<comp::Loadout> {
|
pub fn get_loadout(&mut self) -> Option<comp::Loadout> {
|
||||||
match &mut self.mode {
|
match &mut self.mode {
|
||||||
Mode::Select(characterdata) => {
|
Mode::Select(character_list) => {
|
||||||
let loadout = comp::Loadout {
|
if let Some(data) = character_list {
|
||||||
active_item: characterdata
|
if let Some(character_item) = data.get(self.selected_character) {
|
||||||
.as_ref()
|
let loadout = comp::Loadout {
|
||||||
.and_then(|d| d.tool.as_ref())
|
active_item: character_item.character.tool.as_ref().map(|tool| {
|
||||||
.map(|tool| comp::ItemConfig {
|
comp::ItemConfig {
|
||||||
item: (*load::<comp::Item>(&tool).unwrap_or_else(|err| {
|
item: (*load::<comp::Item>(&tool).unwrap_or_else(|err| {
|
||||||
error!(
|
error!(
|
||||||
"Could not load item {} maybe it no longer exists: {:?}",
|
"Could not load item {} maybe it no longer exists: \
|
||||||
&tool, err
|
{:?}",
|
||||||
);
|
&tool, err
|
||||||
load_expect("common.items.weapons.sword.starter_sword")
|
);
|
||||||
}))
|
load_expect("common.items.weapons.sword.starter_sword")
|
||||||
.clone(),
|
}))
|
||||||
ability1: None,
|
.clone(),
|
||||||
ability2: None,
|
ability1: None,
|
||||||
ability3: None,
|
ability2: None,
|
||||||
block_ability: None,
|
ability3: None,
|
||||||
dodge_ability: None,
|
block_ability: None,
|
||||||
}),
|
dodge_ability: None,
|
||||||
second_item: None,
|
}
|
||||||
shoulder: None,
|
}),
|
||||||
chest: Some(assets::load_expect_cloned(
|
second_item: None,
|
||||||
"common.items.armor.starter.rugged_chest",
|
shoulder: None,
|
||||||
)),
|
chest: Some(assets::load_expect_cloned(
|
||||||
belt: None,
|
"common.items.armor.starter.rugged_chest",
|
||||||
hand: None,
|
)),
|
||||||
pants: Some(assets::load_expect_cloned(
|
belt: None,
|
||||||
"common.items.armor.starter.rugged_pants",
|
hand: None,
|
||||||
)),
|
pants: Some(assets::load_expect_cloned(
|
||||||
foot: Some(assets::load_expect_cloned(
|
"common.items.armor.starter.rugged_pants",
|
||||||
"common.items.armor.starter.sandals_0",
|
)),
|
||||||
)),
|
foot: Some(assets::load_expect_cloned(
|
||||||
back: None,
|
"common.items.armor.starter.sandals_0",
|
||||||
ring: None,
|
)),
|
||||||
neck: None,
|
back: None,
|
||||||
lantern: None,
|
ring: None,
|
||||||
head: None,
|
neck: None,
|
||||||
tabard: None,
|
lantern: None,
|
||||||
};
|
head: None,
|
||||||
Some(loadout)
|
tabard: None,
|
||||||
|
};
|
||||||
|
Some(loadout)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Mode::Create { loadout, tool, .. } => {
|
Mode::Create { loadout, tool, .. } => {
|
||||||
loadout.active_item = tool.map(|tool| comp::ItemConfig {
|
loadout.active_item = tool.map(|tool| comp::ItemConfig {
|
||||||
@ -405,7 +428,7 @@ impl CharSelectionUi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Split this into multiple modules or functions.
|
// TODO: Split this into multiple modules or functions.
|
||||||
fn update_layout(&mut self, global_state: &mut GlobalState, client: &Client) -> Vec<Event> {
|
fn update_layout(&mut self, client: &mut Client) -> Vec<Event> {
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
let can_enter_world = match &self.mode {
|
let can_enter_world = match &self.mode {
|
||||||
@ -453,9 +476,16 @@ impl CharSelectionUi {
|
|||||||
.title_text_color(TEXT_COLOR)
|
.title_text_color(TEXT_COLOR)
|
||||||
.desc_text_color(TEXT_COLOR_2);
|
.desc_text_color(TEXT_COLOR_2);
|
||||||
|
|
||||||
|
// Set the info content if we encountered an error related to characters
|
||||||
|
if client.character_list.error.is_some() {
|
||||||
|
self.info_content = InfoContent::CharacterError;
|
||||||
|
}
|
||||||
|
|
||||||
// Information Window
|
// Information Window
|
||||||
if let InfoContent::None = self.info_content {
|
if self
|
||||||
} else {
|
.info_content
|
||||||
|
.has_content(&client.character_list.loading)
|
||||||
|
{
|
||||||
Rectangle::fill_with([520.0, 150.0], color::rgba(0.0, 0.0, 0.0, 0.9))
|
Rectangle::fill_with([520.0, 150.0], color::rgba(0.0, 0.0, 0.0, 0.9))
|
||||||
.mid_top_with_margin_on(ui_widgets.window, 300.0)
|
.mid_top_with_margin_on(ui_widgets.window, 300.0)
|
||||||
.set(self.ids.info_bg, ui_widgets);
|
.set(self.ids.info_bg, ui_widgets);
|
||||||
@ -467,6 +497,7 @@ impl CharSelectionUi {
|
|||||||
Rectangle::fill_with([275.0, 150.0], color::TRANSPARENT)
|
Rectangle::fill_with([275.0, 150.0], color::TRANSPARENT)
|
||||||
.bottom_left_with_margins_on(self.ids.info_frame, 0.0, 0.0)
|
.bottom_left_with_margins_on(self.ids.info_frame, 0.0, 0.0)
|
||||||
.set(self.ids.info_button_align, ui_widgets);
|
.set(self.ids.info_button_align, ui_widgets);
|
||||||
|
|
||||||
match self.info_content {
|
match self.info_content {
|
||||||
InfoContent::None => unreachable!(),
|
InfoContent::None => unreachable!(),
|
||||||
InfoContent::Deletion(character_index) => {
|
InfoContent::Deletion(character_index) => {
|
||||||
@ -505,21 +536,90 @@ impl CharSelectionUi {
|
|||||||
.was_clicked()
|
.was_clicked()
|
||||||
{
|
{
|
||||||
self.info_content = InfoContent::None;
|
self.info_content = InfoContent::None;
|
||||||
global_state.meta.delete_character(character_index);
|
|
||||||
|
if let Some(character_item) =
|
||||||
|
client.character_list.characters.get(character_index)
|
||||||
|
{
|
||||||
|
// Unsaved characters have no id, this should never be the case here
|
||||||
|
if let Some(character_id) = character_item.character.id {
|
||||||
|
self.info_content = InfoContent::DeletingCharacter;
|
||||||
|
|
||||||
|
events.push(Event::DeleteCharacter(character_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
InfoContent::LoadingCharacters => {
|
||||||
|
Text::new(&self.voxygen_i18n.get("char_selection.loading_characters"))
|
||||||
|
.mid_top_with_margin_on(self.ids.info_frame, 40.0)
|
||||||
|
.font_size(self.fonts.cyri.scale(24))
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.color(TEXT_COLOR)
|
||||||
|
.set(self.ids.loading_characters_text, ui_widgets);
|
||||||
|
},
|
||||||
|
InfoContent::CreatingCharacter => {
|
||||||
|
Text::new(&self.voxygen_i18n.get("char_selection.creating_character"))
|
||||||
|
.mid_top_with_margin_on(self.ids.info_frame, 40.0)
|
||||||
|
.font_size(self.fonts.cyri.scale(24))
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.color(TEXT_COLOR)
|
||||||
|
.set(self.ids.creating_character_text, ui_widgets);
|
||||||
|
},
|
||||||
|
InfoContent::DeletingCharacter => {
|
||||||
|
Text::new(&self.voxygen_i18n.get("char_selection.deleting_character"))
|
||||||
|
.mid_top_with_margin_on(self.ids.info_frame, 40.0)
|
||||||
|
.font_size(self.fonts.cyri.scale(24))
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.color(TEXT_COLOR)
|
||||||
|
.set(self.ids.deleting_character_text, ui_widgets);
|
||||||
|
},
|
||||||
|
InfoContent::CharacterError => {
|
||||||
|
if let Some(error_message) = &client.character_list.error {
|
||||||
|
Text::new(&format!(
|
||||||
|
"{}: {}",
|
||||||
|
&self.voxygen_i18n.get("common.error"),
|
||||||
|
error_message
|
||||||
|
))
|
||||||
|
.mid_top_with_margin_on(self.ids.info_frame, 40.0)
|
||||||
|
.font_size(self.fonts.cyri.scale(24))
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.color(TEXT_COLOR)
|
||||||
|
.set(self.ids.character_error_message, ui_widgets);
|
||||||
|
|
||||||
|
if Button::image(self.imgs.button)
|
||||||
|
.w_h(150.0, 40.0)
|
||||||
|
.bottom_right_with_margins_on(self.ids.info_button_align, 20.0, 20.0)
|
||||||
|
.hover_image(self.imgs.button_hover)
|
||||||
|
.press_image(self.imgs.button_press)
|
||||||
|
.label_y(Relative::Scalar(2.0))
|
||||||
|
.label(&self.voxygen_i18n.get("common.close"))
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.label_font_size(self.fonts.cyri.scale(18))
|
||||||
|
.label_color(TEXT_COLOR)
|
||||||
|
.set(self.ids.info_ok, ui_widgets)
|
||||||
|
.was_clicked()
|
||||||
|
{
|
||||||
|
self.info_content = InfoContent::None;
|
||||||
|
client.character_list.error = None;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.info_content = InfoContent::None;
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Character Selection /////////////////
|
// Character Selection /////////////////
|
||||||
match &mut self.mode {
|
match &mut self.mode {
|
||||||
Mode::Select(data) => {
|
Mode::Select(data) => {
|
||||||
// Set active body
|
// Set active body
|
||||||
*data = if let Some(character) = global_state
|
*data = if client
|
||||||
.meta
|
.character_list
|
||||||
.characters
|
.characters
|
||||||
.get(global_state.meta.selected_character)
|
.get(self.selected_character)
|
||||||
|
.is_some()
|
||||||
{
|
{
|
||||||
Some(character.clone())
|
Some(client.character_list.characters.clone())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@ -579,7 +679,7 @@ impl CharSelectionUi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enter World Button
|
// Enter World Button
|
||||||
let character_count = global_state.meta.characters.len();
|
let character_count = client.character_list.characters.len();
|
||||||
let enter_world_str = &self.voxygen_i18n.get("char_selection.enter_world");
|
let enter_world_str = &self.voxygen_i18n.get("char_selection.enter_world");
|
||||||
let enter_button = Button::image(self.imgs.button)
|
let enter_button = Button::image(self.imgs.button)
|
||||||
.mid_bottom_with_margin_on(ui_widgets.window, 10.0)
|
.mid_bottom_with_margin_on(ui_widgets.window, 10.0)
|
||||||
@ -648,13 +748,12 @@ impl CharSelectionUi {
|
|||||||
.resize(character_count, &mut ui_widgets.widget_id_generator());
|
.resize(character_count, &mut ui_widgets.widget_id_generator());
|
||||||
|
|
||||||
// Character selection
|
// Character selection
|
||||||
for (i, character) in global_state.meta.characters.iter().enumerate() {
|
for (i, character_item) in client.character_list.characters.iter().enumerate() {
|
||||||
let character_box =
|
let character_box = Button::image(if self.selected_character == i {
|
||||||
Button::image(if global_state.meta.selected_character == i {
|
self.imgs.selection_hover
|
||||||
self.imgs.selection_hover
|
} else {
|
||||||
} else {
|
self.imgs.selection
|
||||||
self.imgs.selection
|
});
|
||||||
});
|
|
||||||
let character_box = if i == 0 {
|
let character_box = if i == 0 {
|
||||||
character_box.top_left_with_margins_on(
|
character_box.top_left_with_margins_on(
|
||||||
self.ids.charlist_alignment,
|
self.ids.charlist_alignment,
|
||||||
@ -674,7 +773,7 @@ impl CharSelectionUi {
|
|||||||
.set(self.ids.character_boxes[i], ui_widgets)
|
.set(self.ids.character_boxes[i], ui_widgets)
|
||||||
.was_clicked()
|
.was_clicked()
|
||||||
{
|
{
|
||||||
global_state.meta.selected_character = i;
|
self.selected_character = i;
|
||||||
}
|
}
|
||||||
if Button::image(self.imgs.delete_button)
|
if Button::image(self.imgs.delete_button)
|
||||||
.w_h(30.0 * 0.5, 30.0 * 0.5)
|
.w_h(30.0 * 0.5, 30.0 * 0.5)
|
||||||
@ -692,7 +791,7 @@ impl CharSelectionUi {
|
|||||||
{
|
{
|
||||||
self.info_content = InfoContent::Deletion(i);
|
self.info_content = InfoContent::Deletion(i);
|
||||||
}
|
}
|
||||||
Text::new(&character.name)
|
Text::new(&character_item.character.alias)
|
||||||
.top_left_with_margins_on(self.ids.character_boxes[i], 6.0, 9.0)
|
.top_left_with_margins_on(self.ids.character_boxes[i], 6.0, 9.0)
|
||||||
.font_size(self.fonts.cyri.scale(19))
|
.font_size(self.fonts.cyri.scale(19))
|
||||||
.font_id(self.fonts.cyri.conrod_id)
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
@ -731,23 +830,34 @@ impl CharSelectionUi {
|
|||||||
2.0,
|
2.0,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let character_limit_reached = character_count >= MAX_CHARACTERS_PER_PLAYER;
|
||||||
|
|
||||||
|
let color = if character_limit_reached {
|
||||||
|
Color::Rgba(0.38, 0.38, 0.10, 1.0)
|
||||||
|
} else {
|
||||||
|
Color::Rgba(0.38, 1.0, 0.07, 1.0)
|
||||||
|
};
|
||||||
|
|
||||||
if create_char_button
|
if create_char_button
|
||||||
.w_h(386.0, 80.0)
|
.w_h(386.0, 80.0)
|
||||||
.hover_image(self.imgs.selection_hover)
|
.hover_image(self.imgs.selection_hover)
|
||||||
.press_image(self.imgs.selection_press)
|
.press_image(self.imgs.selection_press)
|
||||||
.label(&self.voxygen_i18n.get("char_selection.create_new_charater"))
|
.label(&self.voxygen_i18n.get("char_selection.create_new_charater"))
|
||||||
.label_color(Color::Rgba(0.38, 1.0, 0.07, 1.0))
|
.label_color(color)
|
||||||
.label_font_id(self.fonts.cyri.conrod_id)
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
.image_color(Color::Rgba(0.38, 1.0, 0.07, 1.0))
|
.image_color(color)
|
||||||
.set(self.ids.character_box_2, ui_widgets)
|
.set(self.ids.character_box_2, ui_widgets)
|
||||||
.was_clicked()
|
.was_clicked()
|
||||||
{
|
{
|
||||||
self.mode = Mode::Create {
|
if !character_limit_reached {
|
||||||
name: "Character Name".to_string(),
|
self.mode = Mode::Create {
|
||||||
body: humanoid::Body::random(),
|
name: "Character Name".to_string(),
|
||||||
loadout: comp::Loadout::default(),
|
body: humanoid::Body::random(),
|
||||||
tool: Some(STARTER_SWORD),
|
loadout: comp::Loadout::default(),
|
||||||
};
|
tool: Some(STARTER_SWORD),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Character_Creation
|
// Character_Creation
|
||||||
@ -791,12 +901,14 @@ impl CharSelectionUi {
|
|||||||
.set(self.ids.create_button, ui_widgets)
|
.set(self.ids.create_button, ui_widgets)
|
||||||
.was_clicked()
|
.was_clicked()
|
||||||
{
|
{
|
||||||
global_state.meta.selected_character =
|
self.info_content = InfoContent::CreatingCharacter;
|
||||||
global_state.meta.add_character(CharacterData {
|
|
||||||
name: name.clone(),
|
events.push(Event::AddCharacter {
|
||||||
body: comp::Body::Humanoid(body.clone()),
|
alias: name.clone(),
|
||||||
tool: tool.map(|tool| tool.to_string()),
|
tool: tool.map(|tool| tool.to_string()),
|
||||||
});
|
body: comp::Body::Humanoid(body.clone()),
|
||||||
|
});
|
||||||
|
|
||||||
to_select = true;
|
to_select = true;
|
||||||
}
|
}
|
||||||
// Character Name Input
|
// Character Name Input
|
||||||
@ -1290,20 +1402,16 @@ impl CharSelectionUi {
|
|||||||
body.skin = new_val as u8;
|
body.skin = new_val as u8;
|
||||||
}
|
}
|
||||||
// Eyebrows
|
// Eyebrows
|
||||||
let current_eyebrows = body.eyebrows;
|
|
||||||
if let Some(new_val) = char_slider(
|
if let Some(new_val) = char_slider(
|
||||||
self.ids.skin_slider,
|
self.ids.skin_slider,
|
||||||
self.voxygen_i18n.get("char_selection.eyebrows"),
|
self.voxygen_i18n.get("char_selection.eyebrows"),
|
||||||
self.ids.eyebrows_text,
|
self.ids.eyebrows_text,
|
||||||
humanoid::ALL_EYEBROWS.len() - 1,
|
body.race.num_eyebrows(body.body_type) as usize - 1,
|
||||||
humanoid::ALL_EYEBROWS
|
body.eyebrows as usize,
|
||||||
.iter()
|
|
||||||
.position(|&c| c == current_eyebrows)
|
|
||||||
.unwrap_or(0),
|
|
||||||
self.ids.eyebrows_slider,
|
self.ids.eyebrows_slider,
|
||||||
ui_widgets,
|
ui_widgets,
|
||||||
) {
|
) {
|
||||||
body.eyebrows = humanoid::ALL_EYEBROWS[new_val];
|
body.eyebrows = new_val as u8;
|
||||||
}
|
}
|
||||||
// EyeColor
|
// EyeColor
|
||||||
if let Some(new_val) = char_slider(
|
if let Some(new_val) = char_slider(
|
||||||
@ -1423,8 +1531,8 @@ impl CharSelectionUi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn maintain(&mut self, global_state: &mut GlobalState, client: &Client) -> Vec<Event> {
|
pub fn maintain(&mut self, global_state: &mut GlobalState, client: &mut Client) -> Vec<Event> {
|
||||||
let events = self.update_layout(global_state, client);
|
let events = self.update_layout(client);
|
||||||
self.ui.maintain(global_state.window.renderer_mut(), None);
|
self.ui.maintain(global_state.window.renderer_mut(), None);
|
||||||
events
|
events
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ use common::{
|
|||||||
critter::{BodyType as CBodyType, Species as CSpecies},
|
critter::{BodyType as CBodyType, Species as CSpecies},
|
||||||
dragon, fish_medium, fish_small,
|
dragon, fish_medium, fish_small,
|
||||||
golem::{BodyType as GBodyType, Species as GSpecies},
|
golem::{BodyType as GBodyType, Species as GSpecies},
|
||||||
humanoid::{Body, BodyType, EyeColor, Eyebrows, Race, Skin},
|
humanoid::{Body, BodyType, EyeColor, Race, Skin},
|
||||||
item::{
|
item::{
|
||||||
armor::{Armor, Back, Belt, Chest, Foot, Hand, Head, Pants, Shoulder, Tabard},
|
armor::{Armor, Back, Belt, Chest, Foot, Hand, Head, Pants, Shoulder, Tabard},
|
||||||
tool::{Tool, ToolKind},
|
tool::{Tool, ToolKind},
|
||||||
@ -156,7 +156,7 @@ impl HumHeadSpec {
|
|||||||
beard: u8,
|
beard: u8,
|
||||||
eye_color: u8,
|
eye_color: u8,
|
||||||
skin: u8,
|
skin: u8,
|
||||||
_eyebrows: Eyebrows,
|
_eyebrows: u8,
|
||||||
accessory: u8,
|
accessory: u8,
|
||||||
generate_mesh: impl FnOnce(&Segment, Vec3<f32>) -> Mesh<FigurePipeline>,
|
generate_mesh: impl FnOnce(&Segment, Vec3<f32>) -> Mesh<FigurePipeline>,
|
||||||
) -> Mesh<FigurePipeline> {
|
) -> Mesh<FigurePipeline> {
|
||||||
|
Reference in New Issue
Block a user