mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
- 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:
parent
6cebf52a1c
commit
5a13b54cbf
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
**/target
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -33,6 +33,10 @@ maps
|
||||
screenshots
|
||||
todo.txt
|
||||
|
||||
# Game data
|
||||
*.sqlite
|
||||
*.sqlite-journal
|
||||
|
||||
# direnv
|
||||
/.envrc
|
||||
*.bat
|
||||
|
@ -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
74
Cargo.lock
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
20
common/src/character.rs
Normal file
@ -0,0 +1,20 @@
|
||||
use crate::comp;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// The limit on how many characters that a player can have
|
||||
pub const MAX_CHARACTERS_PER_PLAYER: usize = 8;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Character {
|
||||
pub id: Option<i32>,
|
||||
pub alias: String,
|
||||
pub tool: Option<String>, // TODO: Remove once we start persisting inventories
|
||||
}
|
||||
|
||||
/// Represents the character data sent by the server after loading from the
|
||||
/// database.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CharacterItem {
|
||||
pub character: Character,
|
||||
pub body: comp::Body,
|
||||
}
|
@ -7,7 +7,7 @@ pub struct Body {
|
||||
pub body_type: BodyType,
|
||||
pub 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,
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ pub enum ServerEvent {
|
||||
Mount(EcsEntity, EcsEntity),
|
||||
Unmount(EcsEntity),
|
||||
Possess(Uid, Uid),
|
||||
CreateCharacter {
|
||||
SelectCharacter {
|
||||
entity: EcsEntity,
|
||||
name: String,
|
||||
body: comp::Body,
|
||||
|
@ -14,6 +14,7 @@
|
||||
|
||||
pub mod assets;
|
||||
pub mod astar;
|
||||
pub mod character;
|
||||
pub mod clock;
|
||||
pub mod comp;
|
||||
pub mod effect;
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -8,6 +8,8 @@ services:
|
||||
- "14004:14004"
|
||||
- "14005:14005"
|
||||
restart: on-failure:0
|
||||
volumes:
|
||||
- "./db:/opt/db"
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
volumes:
|
||||
|
@ -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"
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS "character";
|
6
server/src/migrations/2020-04-11-202519_character/up.sql
Normal file
6
server/src/migrations/2020-04-11-202519_character/up.sql
Normal file
@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS "character" (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
player_uuid TEXT NOT NULL,
|
||||
alias TEXT NOT NULL,
|
||||
tool TEXT
|
||||
);
|
1
server/src/migrations/2020-04-19-025352_body/down.sql
Normal file
1
server/src/migrations/2020-04-19-025352_body/down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS "body";
|
13
server/src/migrations/2020-04-19-025352_body/up.sql
Normal file
13
server/src/migrations/2020-04-19-025352_body/up.sql
Normal file
@ -0,0 +1,13 @@
|
||||
CREATE TABLE IF NOT EXISTS "body" (
|
||||
character_id INT NOT NULL PRIMARY KEY,
|
||||
race SMALLINT NOT NULL,
|
||||
body_type SMALLINT NOT NULL,
|
||||
hair_style SMALLINT NOT NULL,
|
||||
beard SMALLINT NOT NULL,
|
||||
eyebrows SMALLINT NOT NULL,
|
||||
accessory SMALLINT NOT NULL,
|
||||
hair_color SMALLINT NOT NULL,
|
||||
skin SMALLINT NOT NULL,
|
||||
eye_color SMALLINT NOT NULL,
|
||||
FOREIGN KEY(character_id) REFERENCES "character"(id) ON DELETE CASCADE
|
||||
);
|
1
server/src/persistence/.env
Normal file
1
server/src/persistence/.env
Normal file
@ -0,0 +1 @@
|
||||
DATABASE_URL=../../../saves/db.sqlite
|
125
server/src/persistence/character.rs
Normal file
125
server/src/persistence/character.rs
Normal file
@ -0,0 +1,125 @@
|
||||
extern crate diesel;
|
||||
|
||||
use super::{
|
||||
establish_connection,
|
||||
models::{Body, Character, NewCharacter},
|
||||
schema, Error,
|
||||
};
|
||||
use crate::comp;
|
||||
use common::character::{Character as CharacterData, CharacterItem, MAX_CHARACTERS_PER_PLAYER};
|
||||
use diesel::prelude::*;
|
||||
|
||||
type CharacterListResult = Result<Vec<CharacterItem>, Error>;
|
||||
|
||||
// Loading of characters happens immediately after login, and the data is only
|
||||
// for the purpose of rendering the character and their level in the character
|
||||
// list.
|
||||
pub fn load_characters(uuid: &str) -> CharacterListResult {
|
||||
use schema::{body, character::dsl::*};
|
||||
|
||||
let data: Vec<(Character, Body)> = character
|
||||
.filter(player_uuid.eq(uuid))
|
||||
.order(id.desc())
|
||||
.inner_join(body::table)
|
||||
.load::<(Character, Body)>(&establish_connection())?;
|
||||
|
||||
Ok(data
|
||||
.iter()
|
||||
.map(|(character_data, body_data)| CharacterItem {
|
||||
character: CharacterData::from(character_data),
|
||||
body: comp::Body::from(body_data),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Create a new character with provided comp::Character and comp::Body data.
|
||||
/// Note that sqlite does not suppport returning the inserted data after a
|
||||
/// successful insert. To workaround, we wrap this in a transaction which
|
||||
/// inserts, queries for the newly created chaacter id, then uses the character
|
||||
/// id for insertion of the `body` table entry
|
||||
pub fn create_character(
|
||||
uuid: &str,
|
||||
alias: String,
|
||||
tool: Option<String>,
|
||||
body: &comp::Body,
|
||||
) -> CharacterListResult {
|
||||
check_character_limit(uuid)?;
|
||||
|
||||
let new_character = NewCharacter {
|
||||
player_uuid: uuid,
|
||||
alias: &alias,
|
||||
tool: tool.as_deref(),
|
||||
};
|
||||
|
||||
let connection = establish_connection();
|
||||
|
||||
connection.transaction::<_, diesel::result::Error, _>(|| {
|
||||
use schema::{body, character, character::dsl::*};
|
||||
|
||||
match body {
|
||||
comp::Body::Humanoid(body_data) => {
|
||||
diesel::insert_into(character::table)
|
||||
.values(&new_character)
|
||||
.execute(&connection)?;
|
||||
|
||||
let inserted_character = character
|
||||
.filter(player_uuid.eq(uuid))
|
||||
.order(id.desc())
|
||||
.first::<Character>(&connection)?;
|
||||
|
||||
let new_body = Body {
|
||||
character_id: inserted_character.id as i32,
|
||||
race: body_data.race as i16,
|
||||
body_type: body_data.body_type as i16,
|
||||
hair_style: body_data.hair_style as i16,
|
||||
beard: body_data.beard as i16,
|
||||
eyebrows: body_data.eyebrows as i16,
|
||||
accessory: body_data.accessory as i16,
|
||||
hair_color: body_data.hair_color as i16,
|
||||
skin: body_data.skin as i16,
|
||||
eye_color: body_data.eye_color as i16,
|
||||
};
|
||||
|
||||
diesel::insert_into(body::table)
|
||||
.values(&new_body)
|
||||
.execute(&connection)?;
|
||||
},
|
||||
_ => log::warn!("Creating non-humanoid characters is not supported."),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
load_characters(uuid)
|
||||
}
|
||||
|
||||
pub fn delete_character(uuid: &str, character_id: i32) -> CharacterListResult {
|
||||
use schema::character::dsl::*;
|
||||
|
||||
diesel::delete(character.filter(id.eq(character_id))).execute(&establish_connection())?;
|
||||
|
||||
load_characters(uuid)
|
||||
}
|
||||
|
||||
fn check_character_limit(uuid: &str) -> Result<(), Error> {
|
||||
use diesel::dsl::count_star;
|
||||
use schema::character::dsl::*;
|
||||
|
||||
let connection = establish_connection();
|
||||
|
||||
let character_count = character
|
||||
.select(count_star())
|
||||
.filter(player_uuid.eq(uuid))
|
||||
.load::<i64>(&connection)?;
|
||||
|
||||
match character_count.first() {
|
||||
Some(count) => {
|
||||
if count < &(MAX_CHARACTERS_PER_PLAYER as i64) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::CharacterLimitReached)
|
||||
}
|
||||
},
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
5
server/src/persistence/diesel.toml
Normal file
5
server/src/persistence/diesel.toml
Normal file
@ -0,0 +1,5 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "schema.rs"
|
62
server/src/persistence/mod.rs
Normal file
62
server/src/persistence/mod.rs
Normal file
@ -0,0 +1,62 @@
|
||||
pub mod character;
|
||||
mod models;
|
||||
|
||||
mod schema;
|
||||
|
||||
extern crate diesel;
|
||||
|
||||
use diesel::prelude::*;
|
||||
use diesel_migrations::embed_migrations;
|
||||
use std::{env, fmt, fs, path::Path};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
// The player has alredy reached the max character limit
|
||||
CharacterLimitReached,
|
||||
// An error occured when performing a database action
|
||||
DatabaseError(diesel::result::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", match self {
|
||||
Self::DatabaseError(diesel_error) => diesel_error.to_string(),
|
||||
Self::CharacterLimitReached => String::from("Character limit exceeded"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diesel::result::Error> for Error {
|
||||
fn from(error: diesel::result::Error) -> Error { Error::DatabaseError(error) }
|
||||
}
|
||||
|
||||
// See: https://docs.rs/diesel_migrations/1.4.0/diesel_migrations/macro.embed_migrations.html
|
||||
// This macro is called at build-time, and produces the necessary migration info
|
||||
// for the `embedded_migrations` call below.
|
||||
embed_migrations!();
|
||||
|
||||
pub fn run_migrations() -> Result<(), diesel_migrations::RunMigrationsError> {
|
||||
let _ = fs::create_dir(format!("{}/saves/", binary_absolute_path()));
|
||||
embedded_migrations::run_with_output(&establish_connection(), &mut std::io::stdout())
|
||||
}
|
||||
|
||||
fn establish_connection() -> SqliteConnection {
|
||||
let database_url = format!("{}/saves/db.sqlite", binary_absolute_path());
|
||||
SqliteConnection::establish(&database_url)
|
||||
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
|
||||
}
|
||||
|
||||
// Get the absolute path of the binary so that the database is always stored
|
||||
// beside it, no matter where the binary is run from
|
||||
fn binary_absolute_path() -> String {
|
||||
let binary_path;
|
||||
match env::current_exe() {
|
||||
Ok(exe_path) => binary_path = exe_path,
|
||||
Err(e) => panic!("Failed to get current exe path: {}", e),
|
||||
};
|
||||
|
||||
match Path::new(&binary_path.display().to_string()).parent() {
|
||||
Some(path) => return path.display().to_string(),
|
||||
None => panic!("Failed to get current exe parent path"),
|
||||
};
|
||||
}
|
65
server/src/persistence/models.rs
Normal file
65
server/src/persistence/models.rs
Normal file
@ -0,0 +1,65 @@
|
||||
use super::schema::{body, character};
|
||||
use crate::comp;
|
||||
use common::character::Character as CharacterData;
|
||||
|
||||
/// `Character` represents a playable character belonging to a player
|
||||
#[derive(Identifiable, Queryable, Debug)]
|
||||
#[table_name = "character"]
|
||||
pub struct Character {
|
||||
pub id: i32,
|
||||
pub player_uuid: String,
|
||||
pub alias: String,
|
||||
pub tool: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "character"]
|
||||
pub struct NewCharacter<'a> {
|
||||
pub player_uuid: &'a str,
|
||||
pub alias: &'a str,
|
||||
pub tool: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl From<&Character> for CharacterData {
|
||||
fn from(character: &Character) -> CharacterData {
|
||||
CharacterData {
|
||||
id: Some(character.id),
|
||||
alias: String::from(&character.alias),
|
||||
tool: character.tool.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `Body` represents the body variety for a character
|
||||
#[derive(Associations, Identifiable, Queryable, Debug, Insertable)]
|
||||
#[belongs_to(Character)]
|
||||
#[primary_key(character_id)]
|
||||
#[table_name = "body"]
|
||||
pub struct Body {
|
||||
pub character_id: i32,
|
||||
pub race: i16,
|
||||
pub body_type: i16,
|
||||
pub hair_style: i16,
|
||||
pub beard: i16,
|
||||
pub eyebrows: i16,
|
||||
pub accessory: i16,
|
||||
pub hair_color: i16,
|
||||
pub skin: i16,
|
||||
pub eye_color: i16,
|
||||
}
|
||||
|
||||
impl From<&Body> for comp::Body {
|
||||
fn from(body: &Body) -> comp::Body {
|
||||
comp::Body::Humanoid(comp::humanoid::Body {
|
||||
race: comp::humanoid::ALL_RACES[body.race as usize],
|
||||
body_type: comp::humanoid::ALL_BODY_TYPES[body.body_type as usize],
|
||||
hair_style: body.hair_style as u8,
|
||||
beard: body.beard as u8,
|
||||
eyebrows: body.eyebrows as u8,
|
||||
accessory: body.accessory as u8,
|
||||
hair_color: body.hair_color as u8,
|
||||
skin: body.skin as u8,
|
||||
eye_color: body.eye_color as u8,
|
||||
})
|
||||
}
|
||||
}
|
27
server/src/persistence/schema.rs
Normal file
27
server/src/persistence/schema.rs
Normal file
@ -0,0 +1,27 @@
|
||||
table! {
|
||||
body (character_id) {
|
||||
character_id -> Integer,
|
||||
race -> SmallInt,
|
||||
body_type -> SmallInt,
|
||||
hair_style -> SmallInt,
|
||||
beard -> SmallInt,
|
||||
eyebrows -> SmallInt,
|
||||
accessory -> SmallInt,
|
||||
hair_color -> SmallInt,
|
||||
skin -> SmallInt,
|
||||
eye_color -> SmallInt,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
character (id) {
|
||||
id -> Integer,
|
||||
player_uuid -> Text,
|
||||
alias -> Text,
|
||||
tool -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
joinable!(body -> character (character_id));
|
||||
|
||||
allow_tables_to_appear_in_same_query!(body, character,);
|
@ -1,5 +1,5 @@
|
||||
use super::SysTimer;
|
||||
use 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()));
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>,
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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> {
|
||||
|
Loading…
Reference in New Issue
Block a user