mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'shandley/persistence-characters' into 'master'
Character Persistence See merge request veloren/veloren!916
This commit is contained in:
commit
59c97731ad
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