From 5a13b54cbfa00e16fbb0dd840c08da8a435d74f0 Mon Sep 17 00:00:00 2001 From: S Handley Date: Sat, 9 May 2020 15:41:25 +0000 Subject: [PATCH] - Load characters after login. - Make the character screen load with an empty character list from the server, send event to the server for character creation with data, but not yet saving them to the DB. - Working but messy character saving to DB - Add the character_data to the client, rather than keep it in the GLobalState. --- .dockerignore | 1 + .gitignore | 4 + CHANGELOG.md | 1 + Cargo.lock | 74 ++++ assets/voxygen/i18n/en.ron | 3 + client/src/lib.rs | 41 +++ common/src/character.rs | 20 ++ common/src/comp/body/humanoid.rs | 6 +- common/src/comp/player.rs | 9 +- common/src/event.rs | 2 +- common/src/lib.rs | 1 + common/src/msg/client.rs | 7 + common/src/msg/server.rs | 5 + server-cli/docker-compose.yml | 2 + server/Cargo.toml | 4 + server/src/events/mod.rs | 2 +- server/src/lib.rs | 13 + .../2020-04-11-202519_character/down.sql | 1 + .../2020-04-11-202519_character/up.sql | 6 + .../2020-04-19-025352_body/down.sql | 1 + .../migrations/2020-04-19-025352_body/up.sql | 13 + server/src/persistence/.env | 1 + server/src/persistence/character.rs | 125 +++++++ server/src/persistence/diesel.toml | 5 + server/src/persistence/mod.rs | 62 ++++ server/src/persistence/models.rs | 65 ++++ server/src/persistence/schema.rs | 27 ++ server/src/sys/message.rs | 68 +++- voxygen/src/lib.rs | 7 +- voxygen/src/main.rs | 8 +- voxygen/src/menu/char_selection/mod.rs | 44 ++- voxygen/src/menu/char_selection/ui.rs | 322 ++++++++++++------ voxygen/src/scene/figure/load.rs | 4 +- 33 files changed, 807 insertions(+), 147 deletions(-) create mode 100644 .dockerignore create mode 100644 common/src/character.rs create mode 100644 server/src/migrations/2020-04-11-202519_character/down.sql create mode 100644 server/src/migrations/2020-04-11-202519_character/up.sql create mode 100644 server/src/migrations/2020-04-19-025352_body/down.sql create mode 100644 server/src/migrations/2020-04-19-025352_body/up.sql create mode 100644 server/src/persistence/.env create mode 100644 server/src/persistence/character.rs create mode 100644 server/src/persistence/diesel.toml create mode 100644 server/src/persistence/mod.rs create mode 100644 server/src/persistence/models.rs create mode 100644 server/src/persistence/schema.rs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..b60de5b5f2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +**/target diff --git a/.gitignore b/.gitignore index ad3297ca3c..eb77a24e0d 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,10 @@ maps screenshots todo.txt +# Game data +*.sqlite +*.sqlite-journal + # direnv /.envrc *.bat diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aec553aeb..1978b3b1ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Villagers tools and clothing - Cultists clothing - You can start the game by pressing "enter" from the character selection menu +- Added server-side character saving ### Changed diff --git a/Cargo.lock b/Cargo.lock index 98cf25c4af..2f87657016 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1100,6 +1100,38 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "directories" version = "2.0.2" @@ -1166,6 +1198,12 @@ dependencies = [ "nom 4.2.3", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "downcast-rs" version = "1.1.1" @@ -2401,6 +2439,17 @@ dependencies = [ "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]] name = "libssh2-sys" version = "0.2.16" @@ -2550,6 +2599,27 @@ dependencies = [ "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]] name = "mime" version = "0.2.6" @@ -4952,8 +5022,12 @@ dependencies = [ "authc", "chrono", "crossbeam", + "diesel", + "diesel_migrations", + "dotenv", "hashbrown", "lazy_static", + "libsqlite3-sys", "log 0.4.8", "portpicker", "prometheus", diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index 866108d34b..9bdeaa9367 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -337,11 +337,14 @@ Enjoy your stay in the World of Veloren."#, /// Start chracter selection section + "char_selection.loading_characters": "Loading characters...", "char_selection.delete_permanently": "Permanently delete this Character?", + "char_selection.deleting_character": "Deleting Character...", "char_selection.change_server": "Change Server", "char_selection.enter_world": "Enter World", "char_selection.logout": "Logout", "char_selection.create_new_charater": "Create New Character", + "char_selection.creating_character": "Creating Character...", "char_selection.character_creation": "Character Creation", "char_selection.human_default": "Human Default", diff --git a/client/src/lib.rs b/client/src/lib.rs index 5f9b4b594a..df4b3bd8bb 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -14,6 +14,7 @@ pub use specs::{ use byteorder::{ByteOrder, LittleEndian}; use common::{ + character::CharacterItem, comp::{ self, ControlAction, ControlEvent, Controller, ControllerInputs, InventoryManip, InventoryUpdateEvent, @@ -65,6 +66,7 @@ pub struct Client { pub server_info: ServerInfo, pub world_map: (Arc, Vec2), pub player_list: HashMap, + pub character_list: CharacterList, postbox: PostBox, @@ -83,6 +85,15 @@ pub struct Client { pending_chunks: HashMap, 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, + pub loading: bool, + pub error: Option, +} + impl Client { /// Create a new `Client`. pub fn new>(addr: A, view_distance: Option) -> Result { @@ -158,6 +169,7 @@ impl Client { server_info, world_map, player_list: HashMap::new(), + character_list: CharacterList::default(), postbox, @@ -224,9 +236,30 @@ impl Client { pub fn request_character(&mut self, name: String, body: comp::Body, main: Option) { self.postbox .send_message(ClientMsg::Character { name, body, main }); + 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, 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 pub fn request_logout(&mut self) { self.postbox.send_message(ClientMsg::Disconnect); } @@ -819,6 +852,14 @@ impl Client { frontend_events.push(Event::Disconnect); 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() { diff --git a/common/src/character.rs b/common/src/character.rs new file mode 100644 index 0000000000..ed20c15092 --- /dev/null +++ b/common/src/character.rs @@ -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, + pub alias: String, + pub tool: Option, // 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, +} diff --git a/common/src/comp/body/humanoid.rs b/common/src/comp/body/humanoid.rs index 5164933e7d..91b6c483ef 100644 --- a/common/src/comp/body/humanoid.rs +++ b/common/src/comp/body/humanoid.rs @@ -7,7 +7,7 @@ pub struct Body { pub body_type: BodyType, pub hair_style: u8, pub beard: u8, - pub eyebrows: Eyebrows, + pub eyebrows: u8, pub accessory: u8, pub hair_color: u8, pub skin: u8, @@ -29,7 +29,7 @@ impl Body { body_type, hair_style: rng.gen_range(0, race.num_hair_styles(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)), hair_color: rng.gen_range(0, race.num_hair_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 { match (self, body_type) { (Race::Danari, BodyType::Female) => 1, diff --git a/common/src/comp/player.rs b/common/src/comp/player.rs index b7a6aa1a3d..d75a4c7069 100644 --- a/common/src/comp/player.rs +++ b/common/src/comp/player.rs @@ -7,14 +7,21 @@ const MAX_ALIAS_LEN: usize = 32; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Player { pub alias: String, + pub character_id: Option, pub view_distance: Option, uuid: Uuid, } impl Player { - pub fn new(alias: String, view_distance: Option, uuid: Uuid) -> Self { + pub fn new( + alias: String, + character_id: Option, + view_distance: Option, + uuid: Uuid, + ) -> Self { Self { alias, + character_id, view_distance, uuid, } diff --git a/common/src/event.rs b/common/src/event.rs index 924d92ef84..8c9949105e 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -90,7 +90,7 @@ pub enum ServerEvent { Mount(EcsEntity, EcsEntity), Unmount(EcsEntity), Possess(Uid, Uid), - CreateCharacter { + SelectCharacter { entity: EcsEntity, name: String, body: comp::Body, diff --git a/common/src/lib.rs b/common/src/lib.rs index 899578a760..1ea3df4c51 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -14,6 +14,7 @@ pub mod assets; pub mod astar; +pub mod character; pub mod clock; pub mod comp; pub mod effect; diff --git a/common/src/msg/client.rs b/common/src/msg/client.rs index 970208cc23..225d324188 100644 --- a/common/src/msg/client.rs +++ b/common/src/msg/client.rs @@ -7,6 +7,13 @@ pub enum ClientMsg { view_distance: Option, token_or_username: String, }, + RequestCharacterList, + CreateCharacter { + alias: String, + tool: Option, + body: comp::Body, + }, + DeleteCharacter(i32), Character { name: String, body: comp::Body, diff --git a/common/src/msg/server.rs b/common/src/msg/server.rs index 5e6fecac33..d997206753 100644 --- a/common/src/msg/server.rs +++ b/common/src/msg/server.rs @@ -1,5 +1,6 @@ use super::{ClientState, EcsCompPacket}; use crate::{ + character::CharacterItem, comp, state, sync, terrain::{Block, TerrainChunk}, ChatType, @@ -33,6 +34,10 @@ pub enum ServerMsg { time_of_day: state::TimeOfDay, world_map: (Vec2, Vec), }, + /// A list of characters belonging to the a authenticated player was sent + CharacterListUpdate(Vec), + /// An error occured while creating or deleting a character + CharacterActionError(String), PlayerListUpdate(PlayerListUpdate), StateAnswer(Result), /// Trigger cleanup for when the client goes back to the `Registered` state diff --git a/server-cli/docker-compose.yml b/server-cli/docker-compose.yml index 6999700d37..1e9346c3e3 100644 --- a/server-cli/docker-compose.yml +++ b/server-cli/docker-compose.yml @@ -8,6 +8,8 @@ services: - "14004:14004" - "14005:14005" restart: on-failure:0 + volumes: + - "./db:/opt/db" watchtower: image: containrrr/watchtower volumes: diff --git a/server/Cargo.toml b/server/Cargo.toml index e93df3f701..128ca49644 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -32,3 +32,7 @@ prometheus-static-metric = "0.2" rouille = "3.0.0" portpicker = { git = "https://github.com/wusyong/portpicker-rs", branch = "fix_ipv6" } 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" \ No newline at end of file diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 1f9ab9ff0e..c56b695682 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -69,7 +69,7 @@ impl Server { ServerEvent::Possess(possessor_uid, possesse_uid) => { handle_possess(&self, possessor_uid, possesse_uid) }, - ServerEvent::CreateCharacter { + ServerEvent::SelectCharacter { entity, name, body, diff --git a/server/src/lib.rs b/server/src/lib.rs index ecb35c6594..97126cb648 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -9,6 +9,7 @@ pub mod error; pub mod events; pub mod input; pub mod metrics; +pub mod persistence; pub mod settings; pub mod state_ext; pub mod sys; @@ -54,6 +55,9 @@ use world::{ World, }; +#[macro_use] extern crate diesel; +#[macro_use] extern crate diesel_migrations; + const CLIENT_TIMEOUT: f64 = 20.0; // Seconds #[derive(Copy, Clone)] @@ -215,7 +219,16 @@ impl Server { .expect("Failed to initialize server metrics submodule."), 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); + log::info!( "Server version: {}[{}]", *common::util::GIT_HASH, diff --git a/server/src/migrations/2020-04-11-202519_character/down.sql b/server/src/migrations/2020-04-11-202519_character/down.sql new file mode 100644 index 0000000000..208d112408 --- /dev/null +++ b/server/src/migrations/2020-04-11-202519_character/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS "character"; \ No newline at end of file diff --git a/server/src/migrations/2020-04-11-202519_character/up.sql b/server/src/migrations/2020-04-11-202519_character/up.sql new file mode 100644 index 0000000000..c1d19f2053 --- /dev/null +++ b/server/src/migrations/2020-04-11-202519_character/up.sql @@ -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 +); \ No newline at end of file diff --git a/server/src/migrations/2020-04-19-025352_body/down.sql b/server/src/migrations/2020-04-19-025352_body/down.sql new file mode 100644 index 0000000000..7d0323f2c3 --- /dev/null +++ b/server/src/migrations/2020-04-19-025352_body/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS "body"; \ No newline at end of file diff --git a/server/src/migrations/2020-04-19-025352_body/up.sql b/server/src/migrations/2020-04-19-025352_body/up.sql new file mode 100644 index 0000000000..98ac69a59f --- /dev/null +++ b/server/src/migrations/2020-04-19-025352_body/up.sql @@ -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 +); \ No newline at end of file diff --git a/server/src/persistence/.env b/server/src/persistence/.env new file mode 100644 index 0000000000..a165a1410b --- /dev/null +++ b/server/src/persistence/.env @@ -0,0 +1 @@ +DATABASE_URL=../../../saves/db.sqlite diff --git a/server/src/persistence/character.rs b/server/src/persistence/character.rs new file mode 100644 index 0000000000..2f45619534 --- /dev/null +++ b/server/src/persistence/character.rs @@ -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, 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, + 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::(&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::(&connection)?; + + match character_count.first() { + Some(count) => { + if count < &(MAX_CHARACTERS_PER_PLAYER as i64) { + Ok(()) + } else { + Err(Error::CharacterLimitReached) + } + }, + _ => Ok(()), + } +} diff --git a/server/src/persistence/diesel.toml b/server/src/persistence/diesel.toml new file mode 100644 index 0000000000..56bd2de8f1 --- /dev/null +++ b/server/src/persistence/diesel.toml @@ -0,0 +1,5 @@ +# For documentation on how to configure this file, +# see diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "schema.rs" diff --git a/server/src/persistence/mod.rs b/server/src/persistence/mod.rs new file mode 100644 index 0000000000..6aa08213f0 --- /dev/null +++ b/server/src/persistence/mod.rs @@ -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 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"), + }; +} diff --git a/server/src/persistence/models.rs b/server/src/persistence/models.rs new file mode 100644 index 0000000000..0ddc108b45 --- /dev/null +++ b/server/src/persistence/models.rs @@ -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, +} + +#[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, + }) + } +} diff --git a/server/src/persistence/schema.rs b/server/src/persistence/schema.rs new file mode 100644 index 0000000000..36a3f975d4 --- /dev/null +++ b/server/src/persistence/schema.rs @@ -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, + } +} + +joinable!(body -> character (character_id)); + +allow_tables_to_appear_in_same_query!(body, character,); diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs index c43d565dbf..1f8531f9c4 100644 --- a/server/src/sys/message.rs +++ b/server/src/sys/message.rs @@ -1,5 +1,5 @@ use super::SysTimer; -use crate::{auth_provider::AuthProvider, client::Client, CLIENT_TIMEOUT}; +use crate::{auth_provider::AuthProvider, client::Client, persistence, CLIENT_TIMEOUT}; use common::{ comp::{Admin, CanBuild, ControlEvent, Controller, ForceUpdate, Ori, Player, Pos, Stats, Vel}, event::{EventBus, ServerEvent}, @@ -134,7 +134,7 @@ impl<'a> System<'a> for Sys { 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() { // Invalid player @@ -154,6 +154,7 @@ impl<'a> System<'a> for Sys { client.notify(ServerMsg::PlayerListUpdate(PlayerListUpdate::Init( player_list.clone(), ))); + // Add to list to notify all clients of the new player new_players.push(entity); }, @@ -174,12 +175,11 @@ impl<'a> System<'a> for Sys { // Become Registered first. ClientState::Connected => client.error_state(RequestStateError::Impossible), ClientState::Registered | ClientState::Spectator => { - if let (Some(player), false) = ( - players.get(entity), - // Only send login message if it wasn't already sent - // previously - client.login_msg_sent, - ) { + // Only send login message if it wasn't already + // sent previously + if let (Some(player), false) = + (players.get(entity), client.login_msg_sent) + { new_chat_msgs.push(( None, ServerMsg::broadcast(format!( @@ -187,10 +187,11 @@ impl<'a> System<'a> for Sys { &player.alias )), )); + client.login_msg_sent = true; } - server_emitter.emit(ServerEvent::CreateCharacter { + server_emitter.emit(ServerEvent::SelectCharacter { entity, name, body, @@ -308,6 +309,55 @@ impl<'a> System<'a> for Sys { ClientMsg::Terminate => { 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())); + }, + } + } + }, } } } diff --git a/voxygen/src/lib.rs b/voxygen/src/lib.rs index 7737fa8d9e..f57bfdc35e 100644 --- a/voxygen/src/lib.rs +++ b/voxygen/src/lib.rs @@ -15,7 +15,6 @@ pub mod key_state; pub mod logging; pub mod menu; pub mod mesh; -pub mod meta; pub mod render; pub mod scene; pub mod session; @@ -27,15 +26,11 @@ pub mod window; // Reexports pub use crate::error::Error; -use crate::{ - audio::AudioFrontend, meta::Meta, settings::Settings, singleplayer::Singleplayer, - window::Window, -}; +use crate::{audio::AudioFrontend, settings::Settings, singleplayer::Singleplayer, window::Window}; /// A type used to store state that is shared between all play states. pub struct GlobalState { pub settings: Settings, - pub meta: Meta, pub window: Window, pub audio: AudioFrontend, pub info_message: Option, diff --git a/voxygen/src/main.rs b/voxygen/src/main.rs index b4b400aeac..ec17079329 100644 --- a/voxygen/src/main.rs +++ b/voxygen/src/main.rs @@ -7,7 +7,6 @@ use veloren_voxygen::{ i18n::{self, i18n_asset_key, VoxygenLocalization}, logging, menu::main::MainMenuState, - meta::Meta, settings::{AudioOutput, Settings}, window::Window, Direction, GlobalState, PlayState, PlayStateResult, @@ -39,9 +38,6 @@ fn main() { 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 if let Err(err) = settings.save_to_file() { panic!("Failed to save settings: {:?}", err); @@ -62,7 +58,6 @@ fn main() { audio, window: Window::new(&settings).expect("Failed to create window!"), settings, - meta, info_message: 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.meta.save_to_file_warn(); } diff --git a/voxygen/src/menu/char_selection/mod.rs b/voxygen/src/menu/char_selection/mod.rs index 558f5ff527..e33d905191 100644 --- a/voxygen/src/menu/char_selection/mod.rs +++ b/voxygen/src/menu/char_selection/mod.rs @@ -39,6 +39,9 @@ impl PlayState for CharSelectionState { // Set up an fps clock. 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(); while let ClientState::Pending | ClientState::Registered = current_client_state { // Handle window events @@ -62,22 +65,35 @@ impl PlayState for CharSelectionState { // Maintain the UI. let events = self .char_selection_ui - .maintain(global_state, &self.client.borrow()); + .maintain(global_state, &mut self.client.borrow_mut()); + for event in events { match event { ui::Event::Logout => { 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 => { let char_data = self .char_selection_ui - .get_character_data() + .get_character_list() .expect("Character data is required to play"); - self.client.borrow_mut().request_character( - char_data.name, - char_data.body, - char_data.tool, - ); + + if let Some(selected_character) = + char_data.get(self.char_selection_ui.selected_character) + { + 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( global_state, self.client.clone(), @@ -91,10 +107,16 @@ impl PlayState for CharSelectionState { let humanoid_body = self .char_selection_ui - .get_character_data() - .and_then(|data| match data.body { - comp::Body::Humanoid(body) => Some(body), - _ => None, + .get_character_list() + .and_then(|data| { + if let Some(character) = data.get(self.char_selection_ui.selected_character) { + match character.body { + comp::Body::Humanoid(body) => Some(body), + _ => None, + } + } else { + None + } }); // Maintain the scene. diff --git a/voxygen/src/menu/char_selection/ui.rs b/voxygen/src/menu/char_selection/ui.rs index 6dbe44f3cb..8587b59679 100644 --- a/voxygen/src/menu/char_selection/ui.rs +++ b/voxygen/src/menu/char_selection/ui.rs @@ -1,6 +1,5 @@ use crate::{ i18n::{i18n_asset_key, VoxygenLocalization}, - meta::CharacterData, render::{Consts, Globals, Renderer}, ui::{ fonts::ConrodVoxygenFonts, @@ -14,6 +13,7 @@ use client::Client; use common::{ assets, assets::{load, load_expect}, + character::{Character, CharacterItem, MAX_CHARACTERS_PER_PLAYER}, comp::{self, humanoid}, }; use conrod_core::{ @@ -65,6 +65,10 @@ widget_ids! { info_no, delete_text, space, + loading_characters_text, + creating_character_text, + deleting_character_text, + character_error_message, // REMOVE THIS AFTER IMPLEMENTATION daggers_grey, @@ -244,19 +248,41 @@ rotation_image_ids! { pub enum Event { Logout, Play, + AddCharacter { + alias: String, + tool: Option, + body: comp::Body, + }, + DeleteCharacter(i32), } 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); +#[derive(PartialEq)] enum InfoContent { None, 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 { - Select(Option), + Select(Option>), Create { name: String, body: humanoid::Body, @@ -271,16 +297,10 @@ pub struct CharSelectionUi { imgs: Imgs, rot_imgs: ImgsRot, fonts: ConrodVoxygenFonts, - //character_creation: bool, info_content: InfoContent, voxygen_i18n: Arc, - //deletion_confirmation: bool, - /* - pub character_name: String, - pub character_body: humanoid::Body, - pub character_tool: Option<&'static str>, - */ pub mode: Mode, + pub selected_character: usize, } impl CharSelectionUi { @@ -303,83 +323,86 @@ impl CharSelectionUi { let fonts = ConrodVoxygenFonts::load(&voxygen_i18n.fonts, &mut ui) .expect("Impossible to load fonts!"); - // TODO: Randomize initial values. Self { ui, ids, imgs, rot_imgs, fonts, - info_content: InfoContent::None, + info_content: InfoContent::LoadingCharacters, + selected_character: 0, 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), } } - pub fn get_character_data(&self) -> Option { + pub fn get_character_list(&self) -> Option> { match &self.mode { Mode::Select(data) => data.clone(), Mode::Create { name, body, tool, .. - } => Some(CharacterData { - name: name.clone(), + } => Some(vec![CharacterItem { + character: Character { + id: None, + alias: name.clone(), + tool: tool.map(|specifier| specifier.to_string()), + }, body: comp::Body::Humanoid(body.clone()), - tool: tool.map(|specifier| specifier.to_string()), - }), + }]), } } pub fn get_loadout(&mut self) -> Option { match &mut self.mode { - Mode::Select(characterdata) => { - let loadout = comp::Loadout { - active_item: characterdata - .as_ref() - .and_then(|d| d.tool.as_ref()) - .map(|tool| comp::ItemConfig { - item: (*load::(&tool).unwrap_or_else(|err| { - error!( - "Could not load item {} maybe it no longer exists: {:?}", - &tool, err - ); - load_expect("common.items.weapons.sword.starter_sword") - })) - .clone(), - ability1: None, - ability2: None, - ability3: None, - block_ability: None, - dodge_ability: None, - }), - second_item: None, - shoulder: None, - chest: Some(assets::load_expect_cloned( - "common.items.armor.starter.rugged_chest", - )), - belt: None, - hand: None, - pants: Some(assets::load_expect_cloned( - "common.items.armor.starter.rugged_pants", - )), - foot: Some(assets::load_expect_cloned( - "common.items.armor.starter.sandals_0", - )), - back: None, - ring: None, - neck: None, - lantern: None, - head: None, - tabard: None, - }; - Some(loadout) + Mode::Select(character_list) => { + if let Some(data) = character_list { + if let Some(character_item) = data.get(self.selected_character) { + let loadout = comp::Loadout { + active_item: character_item.character.tool.as_ref().map(|tool| { + comp::ItemConfig { + item: (*load::(&tool).unwrap_or_else(|err| { + error!( + "Could not load item {} maybe it no longer exists: \ + {:?}", + &tool, err + ); + load_expect("common.items.weapons.sword.starter_sword") + })) + .clone(), + ability1: None, + ability2: None, + ability3: None, + block_ability: None, + dodge_ability: None, + } + }), + second_item: None, + shoulder: None, + chest: Some(assets::load_expect_cloned( + "common.items.armor.starter.rugged_chest", + )), + belt: None, + hand: None, + pants: Some(assets::load_expect_cloned( + "common.items.armor.starter.rugged_pants", + )), + foot: Some(assets::load_expect_cloned( + "common.items.armor.starter.sandals_0", + )), + back: None, + ring: None, + neck: None, + lantern: None, + head: None, + tabard: None, + }; + Some(loadout) + } else { + None + } + } else { + None + } }, Mode::Create { loadout, tool, .. } => { loadout.active_item = tool.map(|tool| comp::ItemConfig { @@ -405,7 +428,7 @@ impl CharSelectionUi { } // TODO: Split this into multiple modules or functions. - fn update_layout(&mut self, global_state: &mut GlobalState, client: &Client) -> Vec { + fn update_layout(&mut self, client: &mut Client) -> Vec { let mut events = Vec::new(); let can_enter_world = match &self.mode { @@ -453,9 +476,16 @@ impl CharSelectionUi { .title_text_color(TEXT_COLOR) .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 - if let InfoContent::None = self.info_content { - } else { + if self + .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)) .mid_top_with_margin_on(ui_widgets.window, 300.0) .set(self.ids.info_bg, ui_widgets); @@ -467,6 +497,7 @@ impl CharSelectionUi { Rectangle::fill_with([275.0, 150.0], color::TRANSPARENT) .bottom_left_with_margins_on(self.ids.info_frame, 0.0, 0.0) .set(self.ids.info_button_align, ui_widgets); + match self.info_content { InfoContent::None => unreachable!(), InfoContent::Deletion(character_index) => { @@ -505,21 +536,90 @@ impl CharSelectionUi { .was_clicked() { 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 ///////////////// match &mut self.mode { Mode::Select(data) => { // Set active body - *data = if let Some(character) = global_state - .meta + *data = if client + .character_list .characters - .get(global_state.meta.selected_character) + .get(self.selected_character) + .is_some() { - Some(character.clone()) + Some(client.character_list.characters.clone()) } else { None }; @@ -579,7 +679,7 @@ impl CharSelectionUi { } // 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_button = Button::image(self.imgs.button) .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()); // Character selection - for (i, character) in global_state.meta.characters.iter().enumerate() { - let character_box = - Button::image(if global_state.meta.selected_character == i { - self.imgs.selection_hover - } else { - self.imgs.selection - }); + for (i, character_item) in client.character_list.characters.iter().enumerate() { + let character_box = Button::image(if self.selected_character == i { + self.imgs.selection_hover + } else { + self.imgs.selection + }); let character_box = if i == 0 { character_box.top_left_with_margins_on( self.ids.charlist_alignment, @@ -674,7 +773,7 @@ impl CharSelectionUi { .set(self.ids.character_boxes[i], ui_widgets) .was_clicked() { - global_state.meta.selected_character = i; + self.selected_character = i; } if Button::image(self.imgs.delete_button) .w_h(30.0 * 0.5, 30.0 * 0.5) @@ -692,7 +791,7 @@ impl CharSelectionUi { { 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) .font_size(self.fonts.cyri.scale(19)) .font_id(self.fonts.cyri.conrod_id) @@ -731,23 +830,34 @@ impl CharSelectionUi { 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 .w_h(386.0, 80.0) .hover_image(self.imgs.selection_hover) .press_image(self.imgs.selection_press) .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) - .image_color(Color::Rgba(0.38, 1.0, 0.07, 1.0)) + .image_color(color) .set(self.ids.character_box_2, ui_widgets) .was_clicked() { - self.mode = Mode::Create { - name: "Character Name".to_string(), - body: humanoid::Body::random(), - loadout: comp::Loadout::default(), - tool: Some(STARTER_SWORD), - }; + if !character_limit_reached { + self.mode = Mode::Create { + name: "Character Name".to_string(), + body: humanoid::Body::random(), + loadout: comp::Loadout::default(), + tool: Some(STARTER_SWORD), + }; + } } }, // Character_Creation @@ -791,12 +901,14 @@ impl CharSelectionUi { .set(self.ids.create_button, ui_widgets) .was_clicked() { - global_state.meta.selected_character = - global_state.meta.add_character(CharacterData { - name: name.clone(), - body: comp::Body::Humanoid(body.clone()), - tool: tool.map(|tool| tool.to_string()), - }); + self.info_content = InfoContent::CreatingCharacter; + + events.push(Event::AddCharacter { + alias: name.clone(), + tool: tool.map(|tool| tool.to_string()), + body: comp::Body::Humanoid(body.clone()), + }); + to_select = true; } // Character Name Input @@ -1290,20 +1402,16 @@ impl CharSelectionUi { body.skin = new_val as u8; } // Eyebrows - let current_eyebrows = body.eyebrows; if let Some(new_val) = char_slider( self.ids.skin_slider, self.voxygen_i18n.get("char_selection.eyebrows"), self.ids.eyebrows_text, - humanoid::ALL_EYEBROWS.len() - 1, - humanoid::ALL_EYEBROWS - .iter() - .position(|&c| c == current_eyebrows) - .unwrap_or(0), + body.race.num_eyebrows(body.body_type) as usize - 1, + body.eyebrows as usize, self.ids.eyebrows_slider, ui_widgets, ) { - body.eyebrows = humanoid::ALL_EYEBROWS[new_val]; + body.eyebrows = new_val as u8; } // EyeColor 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 { - let events = self.update_layout(global_state, client); + pub fn maintain(&mut self, global_state: &mut GlobalState, client: &mut Client) -> Vec { + let events = self.update_layout(client); self.ui.maintain(global_state.window.renderer_mut(), None); events } diff --git a/voxygen/src/scene/figure/load.rs b/voxygen/src/scene/figure/load.rs index 17876a8dfb..3b1debba8f 100644 --- a/voxygen/src/scene/figure/load.rs +++ b/voxygen/src/scene/figure/load.rs @@ -8,7 +8,7 @@ use common::{ critter::{BodyType as CBodyType, Species as CSpecies}, dragon, fish_medium, fish_small, golem::{BodyType as GBodyType, Species as GSpecies}, - humanoid::{Body, BodyType, EyeColor, Eyebrows, Race, Skin}, + humanoid::{Body, BodyType, EyeColor, Race, Skin}, item::{ armor::{Armor, Back, Belt, Chest, Foot, Hand, Head, Pants, Shoulder, Tabard}, tool::{Tool, ToolKind}, @@ -156,7 +156,7 @@ impl HumHeadSpec { beard: u8, eye_color: u8, skin: u8, - _eyebrows: Eyebrows, + _eyebrows: u8, accessory: u8, generate_mesh: impl FnOnce(&Segment, Vec3) -> Mesh, ) -> Mesh {