Merge branch 'shandley/persistence-characters' into 'master'

Character Persistence

See merge request veloren/veloren!916
This commit is contained in:
Forest Anderson
2020-05-09 15:41:25 +00:00
33 changed files with 807 additions and 147 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
**/target

4
.gitignore vendored
View File

@ -33,6 +33,10 @@ maps
screenshots screenshots
todo.txt todo.txt
# Game data
*.sqlite
*.sqlite-journal
# direnv # direnv
/.envrc /.envrc
*.bat *.bat

View File

@ -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
View File

@ -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",

View File

@ -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",

View File

@ -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
View 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,
}

View File

@ -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,

View File

@ -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,
} }

View File

@ -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,

View File

@ -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;

View File

@ -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,

View File

@ -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

View File

@ -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:

View File

@ -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"

View File

@ -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,

View File

@ -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,

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS "character";

View 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
);

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS "body";

View 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
);

View File

@ -0,0 +1 @@
DATABASE_URL=../../../saves/db.sqlite

View 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(()),
}
}

View 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"

View 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"),
};
}

View 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,
})
}
}

View 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,);

View File

@ -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()));
},
}
}
},
} }
} }
} }

View File

@ -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>,

View File

@ -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();
} }

View File

@ -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");
if let Some(selected_character) =
char_data.get(self.char_selection_ui.selected_character)
{
self.client.borrow_mut().request_character( self.client.borrow_mut().request_character(
char_data.name, selected_character.character.alias.clone(),
char_data.body, selected_character.body,
char_data.tool, 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| {
if let Some(character) = data.get(self.char_selection_ui.selected_character) {
match character.body {
comp::Body::Humanoid(body) => Some(body), comp::Body::Humanoid(body) => Some(body),
_ => None, _ => None,
}
} else {
None
}
}); });
// Maintain the scene. // Maintain the scene.

View File

@ -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,51 +323,47 @@ 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 {
body: comp::Body::Humanoid(body.clone()), id: None,
alias: name.clone(),
tool: tool.map(|specifier| specifier.to_string()), tool: tool.map(|specifier| specifier.to_string()),
}), },
body: comp::Body::Humanoid(body.clone()),
}]),
} }
} }
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) => {
if let Some(data) = character_list {
if let Some(character_item) = data.get(self.selected_character) {
let loadout = comp::Loadout { let loadout = comp::Loadout {
active_item: characterdata active_item: character_item.character.tool.as_ref().map(|tool| {
.as_ref() comp::ItemConfig {
.and_then(|d| d.tool.as_ref())
.map(|tool| 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")
@ -358,6 +374,7 @@ impl CharSelectionUi {
ability3: None, ability3: None,
block_ability: None, block_ability: None,
dodge_ability: None, dodge_ability: None,
}
}), }),
second_item: None, second_item: None,
shoulder: None, shoulder: None,
@ -380,6 +397,12 @@ impl CharSelectionUi {
tabard: None, tabard: None,
}; };
Some(loadout) 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,9 +748,8 @@ 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
@ -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,17 +830,27 @@ 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()
{ {
if !character_limit_reached {
self.mode = Mode::Create { self.mode = Mode::Create {
name: "Character Name".to_string(), name: "Character Name".to_string(),
body: humanoid::Body::random(), body: humanoid::Body::random(),
@ -749,6 +858,7 @@ impl CharSelectionUi {
tool: Some(STARTER_SWORD), 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
} }

View File

@ -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> {