- 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.
This commit is contained in:
S Handley 2020-05-09 15:41:25 +00:00 committed by Forest Anderson
parent 6cebf52a1c
commit 5a13b54cbf
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");
if let Some(selected_character) =
char_data.get(self.char_selection_ui.selected_character)
{
self.client.borrow_mut().request_character(
char_data.name,
char_data.body,
char_data.tool,
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 {
.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,51 +323,47 @@ 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(),
body: comp::Body::Humanoid(body.clone()),
} => Some(vec![CharacterItem {
character: Character {
id: None,
alias: name.clone(),
tool: tool.map(|specifier| specifier.to_string()),
}),
},
body: comp::Body::Humanoid(body.clone()),
}]),
}
}
pub fn get_loadout(&mut self) -> Option<comp::Loadout> {
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 {
active_item: characterdata
.as_ref()
.and_then(|d| d.tool.as_ref())
.map(|tool| comp::ItemConfig {
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: {:?}",
"Could not load item {} maybe it no longer exists: \
{:?}",
&tool, err
);
load_expect("common.items.weapons.sword.starter_sword")
@ -358,6 +374,7 @@ impl CharSelectionUi {
ability3: None,
block_ability: None,
dodge_ability: None,
}
}),
second_item: None,
shoulder: None,
@ -380,6 +397,12 @@ impl CharSelectionUi {
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,9 +748,8 @@ 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 {
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
@ -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,17 +830,27 @@ 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()
{
if !character_limit_reached {
self.mode = Mode::Create {
name: "Character Name".to_string(),
body: humanoid::Body::random(),
@ -749,6 +858,7 @@ impl CharSelectionUi {
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()),
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> {