Begin creating character editing

This commit is contained in:
Dr. Dystopia 2021-12-13 01:13:33 +01:00
parent de5ca67615
commit 408fe1e6b6
14 changed files with 341 additions and 31 deletions

View File

@ -738,6 +738,7 @@ impl Client {
let stream = match msg {
ClientGeneral::RequestCharacterList
| ClientGeneral::CreateCharacter { .. }
| ClientGeneral::EditCharacter { .. }
| ClientGeneral::DeleteCharacter(_)
| ClientGeneral::Character(_)
| ClientGeneral::Spectate => &mut self.character_screen_stream,
@ -842,6 +843,11 @@ impl Client {
});
}
pub fn edit_character(&mut self, alias: String, id: CharacterId, body: comp::Body) {
self.character_list.loading = true;
self.send_msg(ClientGeneral::EditCharacter { alias, id, body });
}
/// Character deletion
pub fn delete_character(&mut self, character_id: CharacterId) {
self.character_list.loading = true;

View File

@ -55,6 +55,11 @@ pub enum ClientGeneral {
body: comp::Body,
},
DeleteCharacter(CharacterId),
EditCharacter {
id: CharacterId,
alias: String,
body: comp::Body,
},
Character(CharacterId),
Spectate,
//Only in game
@ -105,6 +110,7 @@ impl ClientMsg {
&& match g {
ClientGeneral::RequestCharacterList
| ClientGeneral::CreateCharacter { .. }
| ClientGeneral::EditCharacter { .. }
| ClientGeneral::DeleteCharacter(_) => {
c_type != ClientType::ChatOnly && presence.is_none()
},

View File

@ -133,6 +133,7 @@ pub enum ServerGeneral {
CharacterActionError(String),
/// A new character was created
CharacterCreated(character::CharacterId),
CharacterEdited(character::CharacterId),
CharacterSuccess,
//Ingame related
GroupUpdate(comp::group::ChangeNotification<sync::Uid>),
@ -275,6 +276,7 @@ impl ServerMsg {
ServerGeneral::CharacterDataLoadError(_)
| ServerGeneral::CharacterListUpdate(_)
| ServerGeneral::CharacterActionError(_)
| ServerGeneral::CharacterEdited(_)
| ServerGeneral::CharacterCreated(_) => {
c_type != ClientType::ChatOnly && presence.is_none()
},

View File

@ -1,6 +1,7 @@
use crate::persistence::character_updater::CharacterUpdater;
use common::comp::{
inventory::loadout_builder::LoadoutBuilder, Body, Inventory, Item, SkillSet, Stats,
use common::{
character::CharacterId,
comp::{inventory::loadout_builder::LoadoutBuilder, Body, Inventory, Item, SkillSet, Stats},
};
use specs::{Entity, WriteExpect};
@ -73,6 +74,26 @@ pub fn create_character(
Ok(())
}
pub fn edit_character(
entity: Entity,
player_uuid: String,
id: CharacterId,
character_alias: String,
body: Body,
character_updater: &mut WriteExpect<'_, CharacterUpdater>,
) -> Result<(), CreationError> {
// quick fix whitelist validation for now; eventually replace the
// `Option<String>` with an index into a server-provided list of starter
// items, and replace `comp::body::Body` with `comp::body::humanoid::Body`
// throughout the messages involved
if !matches!(body, Body::Humanoid(_)) {
return Err(CreationError::InvalidBody);
}
character_updater.edit_character(entity, player_uuid, id, character_alias, (body,));
Ok(())
}
// Error handling
impl core::fmt::Display for CreationError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {

View File

@ -94,6 +94,7 @@ impl Client {
| ServerGeneral::CharacterListUpdate(_)
| ServerGeneral::CharacterActionError(_)
| ServerGeneral::CharacterCreated(_)
| ServerGeneral::CharacterEdited(_)
| ServerGeneral::CharacterSuccess => {
self.character_screen_stream.lock().unwrap().send(g)
},
@ -165,6 +166,7 @@ impl Client {
| ServerGeneral::CharacterListUpdate(_)
| ServerGeneral::CharacterActionError(_)
| ServerGeneral::CharacterCreated(_)
| ServerGeneral::CharacterEdited(_)
| ServerGeneral::CharacterSuccess => {
PreparedMsg::new(1, &g, &self.character_screen_stream_params)
},

View File

@ -813,6 +813,22 @@ impl Server {
ServerGeneral::CharacterActionError(error.to_string()),
),
},
CharacterLoaderResponseKind::CharacterEdit(result) => match result {
Ok((character_id, list)) => {
self.notify_client(
query_result.entity,
ServerGeneral::CharacterListUpdate(list),
);
self.notify_client(
query_result.entity,
ServerGeneral::CharacterEdited(character_id),
);
},
Err(error) => self.notify_client(
query_result.entity,
ServerGeneral::CharacterActionError(error.to_string()),
),
},
CharacterLoaderResponseKind::CharacterData(result) => {
let message = match *result {
Ok(character_data) => ServerEvent::UpdateCharacterData {

View File

@ -22,7 +22,7 @@ use crate::{
character_loader::{CharacterCreationResult, CharacterDataResult, CharacterListResult},
character_updater::PetPersistenceData,
error::PersistenceError::DatabaseError,
PersistedComponents,
EditableComponents, PersistedComponents,
},
};
use common::character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER};
@ -497,6 +497,35 @@ pub fn create_character(
load_character_list(uuid, transactionn).map(|list| (character_id, list))
}
pub fn edit_character(
editable_components: EditableComponents,
transaction: &mut Transaction,
character_id: CharacterId,
uuid: &str,
character_alias: &str,
) -> CharacterCreationResult {
let (body,) = editable_components;
let mut stmt = transaction
.prepare_cached("UPDATE body SET variant = ?1, body_data = ?2 WHERE character_id = ?3")?;
let (body_variant, body_data) = convert_body_to_database_json(&body)?;
stmt.execute(&[
&body_variant.to_string(),
&body_data,
&character_id as &dyn ToSql,
])?;
drop(stmt);
let mut stmt =
transaction.prepare_cached("UPDATE character SET alias = ?1 WHERE character_id = ?2")?;
stmt.execute(&[&character_alias, &character_id as &dyn ToSql])?;
drop(stmt);
load_character_list(uuid, transaction).map(|list| (character_id, list))
}
/// Delete a character. Returns the updated character list.
pub fn delete_character(
requesting_player_uuid: &str,

View File

@ -12,6 +12,7 @@ use tracing::error;
pub(crate) type CharacterListResult = Result<Vec<CharacterItem>, PersistenceError>;
pub(crate) type CharacterCreationResult =
Result<(CharacterId, Vec<CharacterItem>), PersistenceError>;
pub(crate) type CharacterEditResult = Result<(CharacterId, Vec<CharacterItem>), PersistenceError>;
pub(crate) type CharacterDataResult = Result<PersistedComponents, PersistenceError>;
type CharacterLoaderRequest = (specs::Entity, CharacterLoaderRequestKind);
@ -33,6 +34,7 @@ pub enum CharacterLoaderResponseKind {
CharacterList(CharacterListResult),
CharacterData(Box<CharacterDataResult>),
CharacterCreation(CharacterCreationResult),
CharacterEdit(CharacterEditResult),
}
/// Common message format dispatched in response to an update request

View File

@ -4,7 +4,8 @@ use common::character::CharacterId;
use crate::persistence::{
character_loader::{CharacterLoaderResponse, CharacterLoaderResponseKind},
error::PersistenceError,
establish_connection, ConnectionMode, DatabaseSettings, PersistedComponents, VelorenConnection,
establish_connection, ConnectionMode, DatabaseSettings, EditableComponents,
PersistedComponents, VelorenConnection,
};
use crossbeam_channel::TryIter;
use rusqlite::DropBehavior;
@ -36,6 +37,13 @@ pub enum CharacterUpdaterEvent {
character_alias: String,
persisted_components: PersistedComponents,
},
EditCharacter {
entity: Entity,
player_uuid: String,
character_id: CharacterId,
character_alias: String,
editable_components: EditableComponents,
},
DeleteCharacter {
entity: Entity,
requesting_player_uuid: String,
@ -124,6 +132,37 @@ impl CharacterUpdater {
),
}
},
CharacterUpdaterEvent::EditCharacter {
entity,
character_id,
character_alias,
player_uuid,
editable_components,
} => {
match execute_character_edit(
entity,
character_id,
character_alias,
&player_uuid,
editable_components,
&mut conn,
) {
Ok(response) => {
if let Err(e) = response_tx.send(response) {
error!(?e, "Could not send character edit response");
} else {
debug!(
"Processed character create for player {}",
player_uuid
);
}
},
Err(e) => error!(
"Error editing character for player {}, error: {:?}",
player_uuid, e
),
}
},
CharacterUpdaterEvent::DeleteCharacter {
entity,
requesting_player_uuid,
@ -231,6 +270,30 @@ impl CharacterUpdater {
}
}
pub fn edit_character(
&mut self,
entity: Entity,
requesting_player_uuid: String,
character_id: CharacterId,
alias: String,
editable_components: EditableComponents,
) {
if let Err(e) =
self.update_tx
.as_ref()
.unwrap()
.send(CharacterUpdaterEvent::EditCharacter {
entity,
player_uuid: requesting_player_uuid,
character_id,
character_alias: alias,
editable_components,
})
{
error!(?e, "Could not send character edit request");
}
}
pub fn delete_character(
&mut self,
entity: Entity,
@ -363,6 +426,34 @@ fn execute_character_create(
Ok(response)
}
fn execute_character_edit(
entity: Entity,
character_id: CharacterId,
alias: String,
requesting_player_uuid: &str,
editable_components: EditableComponents,
connection: &mut VelorenConnection,
) -> Result<CharacterLoaderResponse, PersistenceError> {
let mut transaction = connection.connection.transaction()?;
let response = CharacterLoaderResponse {
entity,
result: CharacterLoaderResponseKind::CharacterEdit(super::character::edit_character(
editable_components,
&mut transaction,
character_id,
requesting_player_uuid,
&alias,
)),
};
if !response.is_err() {
transaction.commit()?;
};
Ok(response)
}
fn execute_character_delete(
entity: Entity,
requesting_player_uuid: &str,

View File

@ -31,6 +31,8 @@ pub type PersistedComponents = (
Vec<PetPersistenceData>,
);
pub type EditableComponents = (comp::Body,);
// See: https://docs.rs/refinery/0.5.0/refinery/macro.embed_migrations.html
// This macro is called at build-time, and produces the necessary migration info
// for the `run_migrations` call below.

View File

@ -145,6 +145,28 @@ impl Sys {
}
}
},
ClientGeneral::EditCharacter { id, alias, body } => {
if let Err(error) = alias_validator.validate(&alias) {
debug!(?error, ?alias, "denied alias as it contained a banned word");
client.send(ServerGeneral::CharacterActionError(error.to_string()))?;
} else if let Some(player) = players.get(entity) {
if let Err(error) = character_creator::edit_character(
entity,
player.uuid().to_string(),
id,
alias,
body,
character_updater,
) {
debug!(
?error,
?body,
"Denied creating character because of invalid input."
);
client.send(ServerGeneral::CharacterActionError(error.to_string()))?;
}
}
},
ClientGeneral::DeleteCharacter(character_id) => {
if let Some(player) = players.get(entity) {
character_updater.delete_character(

View File

@ -283,6 +283,7 @@ impl Sys {
},
ClientGeneral::RequestCharacterList
| ClientGeneral::CreateCharacter { .. }
| ClientGeneral::EditCharacter { .. }
| ClientGeneral::DeleteCharacter(_)
| ClientGeneral::Character(_)
| ClientGeneral::Spectate

View File

@ -119,6 +119,16 @@ impl PlayState for CharSelectionState {
.borrow_mut()
.create_character(alias, mainhand, offhand, body);
},
ui::Event::EditCharacter {
alias,
character_id,
body,
} => {
self.client
.borrow_mut()
.edit_character(alias, character_id, body);
println!("Event::EditCharacter");
},
ui::Event::DeleteCharacter(character_id) => {
self.client.borrow_mut().delete_character(character_id);
},

View File

@ -76,6 +76,10 @@ image_ids_ice! {
delete_button_hover: "voxygen.element.ui.char_select.icons.bin_hover",
delete_button_press: "voxygen.element.ui.char_select.icons.bin_press",
edit_button: "voxygen.element.ui.char_select.icons.bin",
edit_button_hover: "voxygen.element.ui.char_select.icons.bin_hover",
edit_button_press: "voxygen.element.ui.char_select.icons.bin_press",
name_input: "voxygen.element.ui.generic.textbox",
// Tool Icons
@ -129,6 +133,11 @@ pub enum Event {
offhand: Option<String>,
body: comp::Body,
},
EditCharacter {
alias: String,
character_id: CharacterId,
body: comp::Body,
},
DeleteCharacter(CharacterId),
ClearCharacterListError,
SelectCharacter(Option<CharacterId>),
@ -146,7 +155,7 @@ enum Mode {
yes_button: button::State,
no_button: button::State,
},
Create {
CreateOrEdit {
name: String,
body: humanoid::Body,
inventory: Box<comp::inventory::Inventory>,
@ -163,6 +172,7 @@ enum Mode {
create_button: button::State,
rand_character_button: button::State,
rand_name_button: button::State,
character_id: Option<CharacterId>,
},
}
@ -194,7 +204,7 @@ impl Mode {
let inventory = Box::new(Inventory::new_with_loadout(loadout));
Self::Create {
Self::CreateOrEdit {
name,
body: humanoid::Body::random(),
inventory,
@ -210,6 +220,41 @@ impl Mode {
create_button: Default::default(),
rand_character_button: Default::default(),
rand_name_button: Default::default(),
character_id: None,
}
}
pub fn edit(name: String, character_id: CharacterId, body: humanoid::Body) -> Self {
// TODO: Load these from the server (presumably from a .ron) to allow for easier
// modification of custom starting weapons
let mainhand = Some(STARTER_SWORD);
let offhand = None;
let loadout = LoadoutBuilder::empty()
.defaults()
.active_mainhand(mainhand.map(Item::new_from_asset_expect))
.active_offhand(offhand.map(Item::new_from_asset_expect))
.build();
let inventory = Box::new(Inventory::new_with_loadout(loadout));
Self::CreateOrEdit {
name,
body,
inventory,
mainhand,
offhand,
body_type_buttons: Default::default(),
species_buttons: Default::default(),
tool_buttons: Default::default(),
sliders: Default::default(),
scroll: Default::default(),
name_input: Default::default(),
back_button: Default::default(),
create_button: Default::default(),
rand_character_button: Default::default(),
rand_name_button: Default::default(),
character_id: Some(character_id),
}
}
}
@ -247,6 +292,8 @@ enum Message {
EnterWorld,
Select(CharacterId),
Delete(usize),
Edit(usize),
ConfirmEdit(CharacterId),
NewCharacter,
CreateCharacter,
Name(String),
@ -421,12 +468,13 @@ impl Controls {
let characters = &client.character_list().characters;
let num = characters.len();
// Ensure we have enough button states
character_buttons.resize_with(num * 2, Default::default);
const CHAR_BUTTONS: usize = 3;
character_buttons.resize_with(num * CHAR_BUTTONS, Default::default);
// Character Selection List
let mut characters = characters
.iter()
.zip(character_buttons.chunks_exact_mut(2))
.zip(character_buttons.chunks_exact_mut(CHAR_BUTTONS))
.filter_map(|(character, buttons)| {
let mut buttons = buttons.iter_mut();
// TODO: eliminate option in character id?
@ -434,20 +482,43 @@ impl Controls {
(
id,
character,
(buttons.next().unwrap(), buttons.next().unwrap()),
(
buttons.next().unwrap(),
buttons.next().unwrap(),
buttons.next().unwrap(),
),
)
})
})
.enumerate()
.map(
|(i, (character_id, character, (select_button, delete_button)))| {
|(
i,
(
character_id,
character,
(select_button, edit_button, delete_button),
),
)| {
let select_col = if Some(i) == selected {
(255, 208, 69)
} else {
(255, 255, 255)
};
Overlay::new(
Container::new(
Container::new(Row::with_children(vec![
// Edit button
Button::new(
edit_button,
Space::new(Length::Units(16), Length::Units(16)),
)
.style(
style::button::Style::new(imgs.edit_button)
.hover_image(imgs.edit_button_hover)
.press_image(imgs.edit_button_press),
)
.on_press(Message::Edit(i))
.into(),
// Delete button
Button::new(
delete_button,
@ -458,8 +529,9 @@ impl Controls {
.hover_image(imgs.delete_button_hover)
.press_image(imgs.delete_button_press),
)
.on_press(Message::Delete(i)),
)
.on_press(Message::Delete(i))
.into(),
]))
.padding(4),
// Select Button
AspectRatioContainer::new(
@ -716,7 +788,7 @@ impl Controls {
content.into()
}
},
Mode::Create {
Mode::CreateOrEdit {
name,
body,
inventory: _,
@ -732,6 +804,7 @@ impl Controls {
ref mut create_button,
ref mut rand_character_button,
ref mut rand_name_button,
character_id,
} => {
let unselected_style = style::button::Style::new(imgs.icon_border)
.hover_image(imgs.icon_border_mo)
@ -1185,7 +1258,11 @@ impl Controls {
Message::Name,
)
.size(25)
.on_submit(Message::CreateCharacter),
.on_submit(if let Some(character_id) = character_id {
Message::ConfirmEdit(*character_id)
} else {
Message::CreateCharacter
}),
)
.padding(Padding::new().horizontal(7).top(5));
@ -1293,7 +1370,7 @@ impl Controls {
fn update(&mut self, message: Message, events: &mut Vec<Event>, characters: &[CharacterItem]) {
match message {
Message::Back => {
if matches!(&self.mode, Mode::Create { .. }) {
if matches!(&self.mode, Mode::CreateOrEdit { .. }) {
self.mode = Mode::select(None);
}
},
@ -1316,13 +1393,25 @@ impl Controls {
*info_content = Some(InfoContent::Deletion(idx));
}
},
Message::Edit(idx) => {
if matches!(&self.mode, Mode::Select { .. }) {
if let Some(character) = characters.get(idx) {
if let comp::Body::Humanoid(body) = character.body {
if let Some(id) = character.character.id {
self.mode = Mode::edit(character.character.alias.clone(), id, body);
println!("Message::Edit");
}
}
}
}
},
Message::NewCharacter => {
if matches!(&self.mode, Mode::Select { .. }) {
self.mode = Mode::create(self.default_name.clone());
}
},
Message::CreateCharacter => {
if let Mode::Create {
if let Mode::CreateOrEdit {
name,
body,
mainhand,
@ -1339,25 +1428,36 @@ impl Controls {
self.mode = Mode::select(Some(InfoContent::CreatingCharacter));
}
},
Message::ConfirmEdit(character_id) => {
if let Mode::CreateOrEdit { name, body, .. } = &self.mode {
events.push(Event::EditCharacter {
alias: name.clone(),
character_id,
body: comp::Body::Humanoid(*body),
});
self.mode = Mode::select(Some(InfoContent::CreatingCharacter));
println!("Message::ConfirmEdit");
}
},
Message::Name(value) => {
if let Mode::Create { name, .. } = &mut self.mode {
if let Mode::CreateOrEdit { name, .. } = &mut self.mode {
*name = value.chars().take(MAX_NAME_LENGTH).collect();
}
},
Message::BodyType(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.body_type = value;
body.validate();
}
},
Message::Species(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.species = value;
body.validate();
}
},
Message::Tool(value) => {
if let Mode::Create {
if let Mode::CreateOrEdit {
mainhand,
offhand,
inventory,
@ -1378,7 +1478,7 @@ impl Controls {
},
//Todo: Add species and body type to randomization.
Message::RandomizeCharacter => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
use rand::Rng;
let body_type = body.body_type;
let species = body.species;
@ -1394,7 +1494,7 @@ impl Controls {
},
Message::RandomizeName => {
if let Mode::Create { name, body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { name, body, .. } = &mut self.mode {
use common::npc;
*name = npc::get_npc_name(
npc::NpcKind::Humanoid,
@ -1429,43 +1529,43 @@ impl Controls {
events.push(Event::ClearCharacterListError);
},
Message::HairStyle(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.hair_style = value;
body.validate();
}
},
Message::HairColor(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.hair_color = value;
body.validate();
}
},
Message::Skin(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.skin = value;
body.validate();
}
},
Message::Eyes(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.eyes = value;
body.validate();
}
},
Message::EyeColor(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.eye_color = value;
body.validate();
}
},
Message::Accessory(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.accessory = value;
body.validate();
}
},
Message::Beard(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.beard = value;
body.validate();
}
@ -1484,7 +1584,7 @@ impl Controls {
.selected
.and_then(|id| characters.iter().find(|i| i.character.id == Some(id)))
.map(|i| (i.body, &i.inventory)),
Mode::Create {
Mode::CreateOrEdit {
inventory, body, ..
} => Some((comp::Body::Humanoid(*body), inventory)),
}