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
commit 59c97731ad
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
todo.txt
# Game data
*.sqlite
*.sqlite-journal
# direnv
/.envrc
*.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
- Cultists clothing
- You can start the game by pressing "enter" from the character selection menu
- Added server-side character saving
### 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"
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",

View File

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

View File

@ -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<DynamicImage>, Vec2<u32>),
pub player_list: HashMap<u64, String>,
pub character_list: CharacterList,
postbox: PostBox<ClientMsg, ServerMsg>,
@ -83,6 +85,15 @@ pub struct Client {
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 {
/// Create a new `Client`.
pub fn new<A: Into<SocketAddr>>(addr: A, view_distance: Option<u32>) -> Result<Self, Error> {
@ -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<String>) {
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<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
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() {

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

View File

@ -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<i32>,
pub view_distance: Option<u32>,
uuid: Uuid,
}
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 {
alias,
character_id,
view_distance,
uuid,
}

View File

@ -90,7 +90,7 @@ pub enum ServerEvent {
Mount(EcsEntity, EcsEntity),
Unmount(EcsEntity),
Possess(Uid, Uid),
CreateCharacter {
SelectCharacter {
entity: EcsEntity,
name: String,
body: comp::Body,

View File

@ -14,6 +14,7 @@
pub mod assets;
pub mod astar;
pub mod character;
pub mod clock;
pub mod comp;
pub mod effect;

View File

@ -7,6 +7,13 @@ pub enum ClientMsg {
view_distance: Option<u32>,
token_or_username: String,
},
RequestCharacterList,
CreateCharacter {
alias: String,
tool: Option<String>,
body: comp::Body,
},
DeleteCharacter(i32),
Character {
name: String,
body: comp::Body,

View File

@ -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<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),
StateAnswer(Result<ClientState, (RequestStateError, ClientState)>),
/// Trigger cleanup for when the client goes back to the `Registered` state

View File

@ -8,6 +8,8 @@ services:
- "14004:14004"
- "14005:14005"
restart: on-failure:0
volumes:
- "./db:/opt/db"
watchtower:
image: containrrr/watchtower
volumes:

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -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<String>,
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<CharacterData>),
Select(Option<Vec<CharacterItem>>),
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<VoxygenLocalization>,
//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<CharacterData> {
pub fn get_character_list(&self) -> Option<Vec<CharacterItem>> {
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<comp::Loadout> {
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::<comp::Item>(&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::<comp::Item>(&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<Event> {
fn update_layout(&mut self, client: &mut Client) -> Vec<Event> {
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<Event> {
let events = self.update_layout(global_state, client);
pub fn maintain(&mut self, global_state: &mut GlobalState, client: &mut Client) -> Vec<Event> {
let events = self.update_layout(client);
self.ui.maintain(global_state.window.renderer_mut(), None);
events
}

View File

@ -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<f32>) -> Mesh<FigurePipeline>,
) -> Mesh<FigurePipeline> {