Merge branch 'xvar/queue-char-deletes' into 'master'

Changed character deletion to go via batch update

See merge request veloren/veloren!3562
This commit is contained in:
Ben Wallis 2023-03-12 23:21:54 +00:00
commit 45c8934b79
38 changed files with 459 additions and 384 deletions

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Загрузка персанажаў... char_selection-loading_characters = Загрузка персанажаў...
char_selection-delete_permanently = Назаўжды выдаліць гэтага персанажа? char_selection-delete_permanently = Назаўжды выдаліць гэтага персанажа?
char_selection-deleting_character = Выдаленне персанажа...
char_selection-change_server = Змяніць сервер char_selection-change_server = Змяніць сервер
char_selection-enter_world = Увайсці ў свет char_selection-enter_world = Увайсці ў свет
char_selection-logout = Выхад char_selection-logout = Выхад

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Carregant personatges... char_selection-loading_characters = Carregant personatges...
char_selection-delete_permanently = Esborrar permanentment aquest Personatge? char_selection-delete_permanently = Esborrar permanentment aquest Personatge?
char_selection-deleting_character = Esborrant Personatge...
char_selection-change_server = Canviar Servidor char_selection-change_server = Canviar Servidor
char_selection-enter_world = Entrar al Món char_selection-enter_world = Entrar al Món
char_selection-logout = Tancar Sessió char_selection-logout = Tancar Sessió

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Načítání postavy... char_selection-loading_characters = Načítání postavy...
char_selection-delete_permanently = Chcete smazat tuto postavu? char_selection-delete_permanently = Chcete smazat tuto postavu?
char_selection-deleting_character = Probíhá mazání postavy...
char_selection-change_server = Změnit server char_selection-change_server = Změnit server
char_selection-enter_world = Vstup do světa char_selection-enter_world = Vstup do světa
char_selection-logout = Odhlásit char_selection-logout = Odhlásit

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Lade Charaktere... char_selection-loading_characters = Lade Charaktere...
char_selection-delete_permanently = Den Charakter unwiderruflich löschen? char_selection-delete_permanently = Den Charakter unwiderruflich löschen?
char_selection-deleting_character = Lösche Charakter...
char_selection-change_server = Wechsle Server char_selection-change_server = Wechsle Server
char_selection-enter_world = Welt betreten char_selection-enter_world = Welt betreten
char_selection-spectate = Welt betrachten char_selection-spectate = Welt betrachten

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Loading characters... char_selection-loading_characters = Loading characters...
char_selection-delete_permanently = Permanently delete this Character? char_selection-delete_permanently = Permanently delete this Character?
char_selection-deleting_character = Deleting Character...
char_selection-change_server = Change Server char_selection-change_server = Change Server
char_selection-enter_world = Enter World char_selection-enter_world = Enter World
char_selection-spectate = Spectate World char_selection-spectate = Spectate World

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Cargando personajes... char_selection-loading_characters = Cargando personajes...
char_selection-delete_permanently = ¿Quieres eliminar a este personaje para siempre? char_selection-delete_permanently = ¿Quieres eliminar a este personaje para siempre?
char_selection-deleting_character = Eliminando personaje...
char_selection-change_server = Cambiar de servidor char_selection-change_server = Cambiar de servidor
char_selection-enter_world = Entrar al mundo char_selection-enter_world = Entrar al mundo
char_selection-spectate = Observar mundo char_selection-spectate = Observar mundo

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Cargando personajes... char_selection-loading_characters = Cargando personajes...
char_selection-delete_permanently = ¿Borrar este personaje permanentemente? char_selection-delete_permanently = ¿Borrar este personaje permanentemente?
char_selection-deleting_character = Borrando Personaje...
char_selection-change_server = Cambiar Servidor char_selection-change_server = Cambiar Servidor
char_selection-enter_world = Entrar al Mundo char_selection-enter_world = Entrar al Mundo
char_selection-spectate = Espectar Mundo char_selection-spectate = Espectar Mundo

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Pertsonaiak kargatzen... char_selection-loading_characters = Pertsonaiak kargatzen...
char_selection-delete_permanently = Pertsonaia hau betiko ezabatu nahi duzu? char_selection-delete_permanently = Pertsonaia hau betiko ezabatu nahi duzu?
char_selection-deleting_character = Pertsonaia ezabatzen...
char_selection-change_server = Aldatu zerbitzaria char_selection-change_server = Aldatu zerbitzaria
char_selection-enter_world = Sartu munduan char_selection-enter_world = Sartu munduan
char_selection-spectate = Ikuskatu mundua char_selection-spectate = Ikuskatu mundua

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Chargement des personnages... char_selection-loading_characters = Chargement des personnages...
char_selection-delete_permanently = Supprimer définitivement ce personnage ? char_selection-delete_permanently = Supprimer définitivement ce personnage ?
char_selection-deleting_character = Suppression du personnage...
char_selection-change_server = Changer de serveur char_selection-change_server = Changer de serveur
char_selection-enter_world = Rejoindre char_selection-enter_world = Rejoindre
char_selection-spectate = Spectateur char_selection-spectate = Spectateur

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Karakterek betöltése... char_selection-loading_characters = Karakterek betöltése...
char_selection-delete_permanently = Végérvényesen törlöd ezt a karaktert? char_selection-delete_permanently = Végérvényesen törlöd ezt a karaktert?
char_selection-deleting_character = Karakter törlése...
char_selection-change_server = Váltás másik szerverre char_selection-change_server = Váltás másik szerverre
char_selection-enter_world = Világ betöltése char_selection-enter_world = Világ betöltése
char_selection-logout = Kijelentkezés char_selection-logout = Kijelentkezés

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Caricamento personaggi... char_selection-loading_characters = Caricamento personaggi...
char_selection-delete_permanently = Eliminare permanentemente questo personaggio? char_selection-delete_permanently = Eliminare permanentemente questo personaggio?
char_selection-deleting_character = Eliminazione personaggio...
char_selection-change_server = Cambia server char_selection-change_server = Cambia server
char_selection-enter_world = Entra nel mondo char_selection-enter_world = Entra nel mondo
char_selection-spectate = Mondo spettatore char_selection-spectate = Mondo spettatore

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = キャラクターをロード中... char_selection-loading_characters = キャラクターをロード中...
char_selection-delete_permanently = このキャラクターを永久に削除しますか? char_selection-delete_permanently = このキャラクターを永久に削除しますか?
char_selection-deleting_character = キャラクターを削除中...
char_selection-change_server = サーバーを変更 char_selection-change_server = サーバーを変更
char_selection-enter_world = 世界に入る char_selection-enter_world = 世界に入る
char_selection-logout = ログアウト char_selection-logout = ログアウト

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = 캐릭터를 불러오는 중... char_selection-loading_characters = 캐릭터를 불러오는 중...
char_selection-delete_permanently = 이 캐릭터를 영구히 삭제하시겠습니까? char_selection-delete_permanently = 이 캐릭터를 영구히 삭제하시겠습니까?
char_selection-deleting_character = 캐릭터 삭제중...
char_selection-change_server = 서버 바꾸기 char_selection-change_server = 서버 바꾸기
char_selection-enter_world = 세계 들어가기 char_selection-enter_world = 세계 들어가기
char_selection-spectate = 세계 관전하기 char_selection-spectate = 세계 관전하기

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Karakters Worden Geladen... char_selection-loading_characters = Karakters Worden Geladen...
char_selection-delete_permanently = Karakter Permanent Verwijderen? char_selection-delete_permanently = Karakter Permanent Verwijderen?
char_selection-deleting_character = Karakter Wordt Verwijderen...
char_selection-change_server = Server Wisselen char_selection-change_server = Server Wisselen
char_selection-enter_world = Wereld Betreden char_selection-enter_world = Wereld Betreden
char_selection-logout = Uitloggen char_selection-logout = Uitloggen

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Laster inn karakterer... char_selection-loading_characters = Laster inn karakterer...
char_selection-delete_permanently = Slett denne karakteren permanent? char_selection-delete_permanently = Slett denne karakteren permanent?
char_selection-deleting_character = Sletter karakter...
char_selection-change_server = Bytt server char_selection-change_server = Bytt server
char_selection-enter_world = Gå inn i verden char_selection-enter_world = Gå inn i verden
char_selection-logout = Logg ut char_selection-logout = Logg ut

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Ładowanie Postaci... char_selection-loading_characters = Ładowanie Postaci...
char_selection-delete_permanently = Czy na pewno chcesz usunąć tę Postać na zawsze? char_selection-delete_permanently = Czy na pewno chcesz usunąć tę Postać na zawsze?
char_selection-deleting_character = Usuwanie Postaci...
char_selection-change_server = Zmień Serwer char_selection-change_server = Zmień Serwer
char_selection-enter_world = Dołącz do Świata char_selection-enter_world = Dołącz do Świata
char_selection-spectate = Oglądnij świat char_selection-spectate = Oglądnij świat

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Carregando Personagens... char_selection-loading_characters = Carregando Personagens...
char_selection-delete_permanently = Excluir permanentemente este Personagem? char_selection-delete_permanently = Excluir permanentemente este Personagem?
char_selection-deleting_character = Excluindo Personagem...
char_selection-change_server = Alterar Servidor char_selection-change_server = Alterar Servidor
char_selection-enter_world = Entrar no Mundo char_selection-enter_world = Entrar no Mundo
char_selection-spectate = Assistir Mundo char_selection-spectate = Assistir Mundo

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Se încarcă Personajele... char_selection-loading_characters = Se încarcă Personajele...
char_selection-delete_permanently = Vrei să ștergi acest Personaj pentru totdeauna? char_selection-delete_permanently = Vrei să ștergi acest Personaj pentru totdeauna?
char_selection-deleting_character = Se șterge Personajul...
char_selection-change_server = Schimbă Serverul char_selection-change_server = Schimbă Serverul
char_selection-enter_world = Intră în lume char_selection-enter_world = Intră în lume
char_selection-logout = Ieși din cont char_selection-logout = Ieși din cont

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Загрузка персонажей... char_selection-loading_characters = Загрузка персонажей...
char_selection-delete_permanently = Навсегда удалить этого персонажа? char_selection-delete_permanently = Навсегда удалить этого персонажа?
char_selection-deleting_character = Удаление Персонажа...
char_selection-change_server = Сменить сервер char_selection-change_server = Сменить сервер
char_selection-enter_world = Войти в мир char_selection-enter_world = Войти в мир
char_selection-spectate = Режим наблюдателя char_selection-spectate = Режим наблюдателя

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Учитавање ликова... char_selection-loading_characters = Учитавање ликова...
char_selection-delete_permanently = Заувек обриши Лик? char_selection-delete_permanently = Заувек обриши Лик?
char_selection-deleting_character = Обриши Лик...
char_selection-change_server = Промени Сервер char_selection-change_server = Промени Сервер
char_selection-enter_world = Уђи у Свет char_selection-enter_world = Уђи у Свет
char_selection-logout = Одјави се char_selection-logout = Одјави се

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Laddar rollpersoner... char_selection-loading_characters = Laddar rollpersoner...
char_selection-delete_permanently = Vill du radera rollpersonen permanent? char_selection-delete_permanently = Vill du radera rollpersonen permanent?
char_selection-deleting_character = Raderar rollperson...
char_selection-change_server = Byt server char_selection-change_server = Byt server
char_selection-enter_world = Öppna värld char_selection-enter_world = Öppna värld
char_selection-spectate = Övervaka värld char_selection-spectate = Övervaka värld

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = อยู่ระหว่างการโหลดตัวละคร ... char_selection-loading_characters = อยู่ระหว่างการโหลดตัวละคร ...
char_selection-delete_permanently = ลบตัวละครทิ้งหรือไม่? char_selection-delete_permanently = ลบตัวละครทิ้งหรือไม่?
char_selection-deleting_character = อยู่ระหว่างการลบตัวละคร ...
char_selection-change_server = เปลี่ยนเซิร์ฟเวอร์ char_selection-change_server = เปลี่ยนเซิร์ฟเวอร์
char_selection-enter_world = เข้าสู่โลก char_selection-enter_world = เข้าสู่โลก
char_selection-spectate = ชมโลก char_selection-spectate = ชมโลก

View File

@ -2,7 +2,6 @@ char_selection-loading_characters = Karakterler yükleniyor...
char_selection-delete_permanently = char_selection-delete_permanently =
Bu karakteri kalıcı olarak Bu karakteri kalıcı olarak
silmek istediğinden emin misin? silmek istediğinden emin misin?
char_selection-deleting_character = Karakter siliniyor...
char_selection-change_server = Sunucu Değiştir char_selection-change_server = Sunucu Değiştir
char_selection-enter_world = Dünyaya Gir char_selection-enter_world = Dünyaya Gir
char_selection-logout = Çıkış yap char_selection-logout = Çıkış yap

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Завантаження персонажів... char_selection-loading_characters = Завантаження персонажів...
char_selection-delete_permanently = Видалити цього персонажа назавжди? char_selection-delete_permanently = Видалити цього персонажа назавжди?
char_selection-deleting_character = Видалення персонажа...
char_selection-change_server = Змінити сервер char_selection-change_server = Змінити сервер
char_selection-enter_world = Увійти в світ char_selection-enter_world = Увійти в світ
char_selection-spectate = Режим Споглядача char_selection-spectate = Режим Споглядача

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Đang tải nhân vật... char_selection-loading_characters = Đang tải nhân vật...
char_selection-delete_permanently = Xóa vĩnh viễn Nhân vật này? char_selection-delete_permanently = Xóa vĩnh viễn Nhân vật này?
char_selection-deleting_character = Đang Xóa Nhân Vật...
char_selection-change_server = Đổi Máy Chủ char_selection-change_server = Đổi Máy Chủ
char_selection-enter_world = Vào Trò Chơi char_selection-enter_world = Vào Trò Chơi
char_selection-logout = Đăng xuất char_selection-logout = Đăng xuất

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = 加载人物中... char_selection-loading_characters = 加载人物中...
char_selection-delete_permanently = 确定永久删除此角色? char_selection-delete_permanently = 确定永久删除此角色?
char_selection-deleting_character = 删除角色中...
char_selection-change_server = 切换服务器 char_selection-change_server = 切换服务器
char_selection-enter_world = 进入世界 char_selection-enter_world = 进入世界
char_selection-spectate = 观察世界 char_selection-spectate = 观察世界

View File

@ -962,7 +962,17 @@ impl Client {
/// Character deletion /// Character deletion
pub fn delete_character(&mut self, character_id: CharacterId) { pub fn delete_character(&mut self, character_id: CharacterId) {
self.character_list.loading = true; // Pre-emptively remove the character to be deleted from the character list as
// character deletes are processed asynchronously by the server so we can't rely
// on a timely response to update the character list
if let Some(pos) = self
.character_list
.characters
.iter()
.position(|x| x.character.id == Some(character_id))
{
self.character_list.characters.remove(pos);
}
self.send_msg(ClientGeneral::DeleteCharacter(character_id)); self.send_msg(ClientGeneral::DeleteCharacter(character_id));
} }

View File

@ -233,6 +233,11 @@ pub enum ServerEvent {
admin: comp::Admin, admin: comp::Admin,
uuid: Uuid, uuid: Uuid,
}, },
DeleteCharacter {
entity: EcsEntity,
requesting_player_uuid: String,
character_id: CharacterId,
},
} }
pub struct EventBus<E> { pub struct EventBus<E> {

View File

@ -1,4 +1,7 @@
use crate::{client::Client, persistence::PersistedComponents, sys, Server, StateExt}; use crate::{
client::Client, events::player::handle_exit_ingame, persistence::PersistedComponents, sys,
CharacterUpdater, Server, StateExt,
};
use common::{ use common::{
character::CharacterId, character::CharacterId,
comp::{ comp::{
@ -32,13 +35,23 @@ pub fn handle_initialize_character(
character_id: CharacterId, character_id: CharacterId,
requested_view_distances: ViewDistances, requested_view_distances: ViewDistances,
) { ) {
let clamped_vds = requested_view_distances.clamp(server.settings().max_view_distance); let updater = server.state.ecs().fetch::<CharacterUpdater>();
server let pending_database_action = updater.has_pending_database_action(character_id);
.state drop(updater);
.initialize_character_data(entity, character_id, clamped_vds);
// Correct client if its requested VD is too high. if !pending_database_action {
if requested_view_distances.terrain != clamped_vds.terrain { let clamped_vds = requested_view_distances.clamp(server.settings().max_view_distance);
server.notify_client(entity, ServerGeneral::SetViewDistance(clamped_vds.terrain)); server
.state
.initialize_character_data(entity, character_id, clamped_vds);
// Correct client if its requested VD is too high.
if requested_view_distances.terrain != clamped_vds.terrain {
server.notify_client(entity, ServerGeneral::SetViewDistance(clamped_vds.terrain));
}
} else {
// A character delete or update was somehow initiated after the login commenced,
// so disconnect the client without saving any data and abort the login process.
handle_exit_ingame(server, entity, true);
} }
} }

View File

@ -28,6 +28,7 @@ use player::{handle_client_disconnect, handle_exit_ingame, handle_possess};
use specs::{Builder, Entity as EcsEntity, WorldExt}; use specs::{Builder, Entity as EcsEntity, WorldExt};
use trade::handle_process_trade_action; use trade::handle_process_trade_action;
use crate::events::player::handle_character_delete;
pub use group_manip::update_map_markers; pub use group_manip::update_map_markers;
pub(crate) use trade::cancel_trades_for; pub(crate) use trade::cancel_trades_for;
@ -151,6 +152,11 @@ impl Server {
ServerEvent::InitSpectator(entity, requested_view_distances) => { ServerEvent::InitSpectator(entity, requested_view_distances) => {
handle_initialize_spectator(self, entity, requested_view_distances) handle_initialize_spectator(self, entity, requested_view_distances)
}, },
ServerEvent::DeleteCharacter {
entity,
requesting_player_uuid,
character_id,
} => handle_character_delete(self, entity, requesting_player_uuid, character_id),
ServerEvent::UpdateCharacterData { ServerEvent::UpdateCharacterData {
entity, entity,
components, components,
@ -179,7 +185,7 @@ impl Server {
handle_loaded_character_data(self, entity, components, metadata); handle_loaded_character_data(self, entity, components, metadata);
}, },
ServerEvent::ExitIngame { entity } => { ServerEvent::ExitIngame { entity } => {
handle_exit_ingame(self, entity); handle_exit_ingame(self, entity, false);
}, },
ServerEvent::CreateNpc { ServerEvent::CreateNpc {
pos, pos,

View File

@ -4,6 +4,7 @@ use crate::{
presence::Presence, state_ext::StateExt, BattleModeBuffer, Server, presence::Presence, state_ext::StateExt, BattleModeBuffer, Server,
}; };
use common::{ use common::{
character::CharacterId,
comp, comp,
comp::{group, pet::is_tameable}, comp::{group, pet::is_tameable},
uid::{Uid, UidAllocator}, uid::{Uid, UidAllocator},
@ -14,13 +15,45 @@ use common_state::State;
use specs::{saveload::MarkerAllocator, Builder, Entity as EcsEntity, Join, WorldExt}; use specs::{saveload::MarkerAllocator, Builder, Entity as EcsEntity, Join, WorldExt};
use tracing::{debug, error, trace, warn, Instrument}; use tracing::{debug, error, trace, warn, Instrument};
pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) { pub fn handle_character_delete(
server: &mut Server,
entity: EcsEntity,
requesting_player_uuid: String,
character_id: CharacterId,
) {
// Can't process a character delete for a player that has an in-game presence,
// so kick them out before processing the delete.
// NOTE: This relies on StateExt::handle_initialize_character adding the
// Presence component when a character is initialized to detect whether a client
// is in-game.
let has_presence = {
let presences = server.state.ecs().read_storage::<Presence>();
presences.get(entity).is_some()
};
if has_presence {
warn!(
?requesting_player_uuid,
?character_id,
"Character delete received while in-game, disconnecting client."
);
handle_exit_ingame(server, entity, true);
}
let mut updater = server.state.ecs().fetch_mut::<CharacterUpdater>();
updater.queue_character_deletion(requesting_player_uuid, character_id);
}
pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity, skip_persistence: bool) {
span!(_guard, "handle_exit_ingame"); span!(_guard, "handle_exit_ingame");
let state = server.state_mut(); let state = server.state_mut();
// Sync the player's character data to the database. This must be done before // Sync the player's character data to the database. This must be done before
// removing any components from the entity // removing any components from the entity
let entity = persist_entity(state, entity); let entity = if !skip_persistence {
persist_entity(state, entity)
} else {
entity
};
// Create new entity with just `Client`, `Uid`, `Player`, and `...Stream` // Create new entity with just `Client`, `Uid`, `Player`, and `...Stream`
// components. // components.
@ -264,17 +297,15 @@ fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity {
}) })
.collect(); .collect();
character_updater.add_pending_logout_update( character_updater.add_pending_logout_update((
char_id, char_id,
( skill_set.clone(),
skill_set.clone(), inventory.clone(),
inventory.clone(), pets,
pets, waypoint,
waypoint, active_abilities.clone(),
active_abilities.clone(), map_marker,
map_marker, ));
),
);
}, },
PresenceKind::Spectator => { /* Do nothing, spectators do not need persisting */ }, PresenceKind::Spectator => { /* Do nothing, spectators do not need persisting */ },
PresenceKind::Possessor => { /* Do nothing, possessor's are not persisted */ }, PresenceKind::Possessor => { /* Do nothing, possessor's are not persisted */ },

View File

@ -9,6 +9,7 @@
option_zip, option_zip,
unwrap_infallible unwrap_infallible
)] )]
#![feature(hash_drain_filter)]
pub mod automod; pub mod automod;
mod character_creator; mod character_creator;
@ -92,7 +93,7 @@ use common_systems::add_local_systems;
use metrics::{EcsSystemMetrics, PhysicsMetrics, TickMetrics}; use metrics::{EcsSystemMetrics, PhysicsMetrics, TickMetrics};
use network::{ListenAddr, Network, Pid}; use network::{ListenAddr, Network, Pid};
use persistence::{ use persistence::{
character_loader::{CharacterLoader, CharacterLoaderResponseKind}, character_loader::{CharacterLoader, CharacterUpdaterMessage},
character_updater::CharacterUpdater, character_updater::CharacterUpdater,
}; };
use prometheus::Registry; use prometheus::Registry;
@ -125,6 +126,7 @@ use {
common_state::plugin::{memory_manager::EcsWorld, PluginMgr}, common_state::plugin::{memory_manager::EcsWorld, PluginMgr},
}; };
use crate::persistence::character_loader::CharacterScreenResponseKind;
use common::comp::Anchor; use common::comp::Anchor;
#[cfg(feature = "worldgen")] #[cfg(feature = "worldgen")]
use world::{ use world::{
@ -834,104 +836,115 @@ impl Server {
let character_loader = self.state.ecs().read_resource::<CharacterLoader>(); let character_loader = self.state.ecs().read_resource::<CharacterLoader>();
let character_updater = self.state.ecs().read_resource::<CharacterUpdater>(); let mut character_updater = self.state.ecs().write_resource::<CharacterUpdater>();
let updater_messages: Vec<CharacterUpdaterMessage> = character_updater.messages().collect();
// Get character-related database responses and notify the requesting client // Get character-related database responses and notify the requesting client
character_loader character_loader
.messages() .messages()
.chain(character_updater.messages()) .chain(updater_messages)
.for_each(|query_result| match query_result.result { .for_each(|message| match message {
CharacterLoaderResponseKind::CharacterList(result) => match result { CharacterUpdaterMessage::DatabaseBatchCompletion(batch_id) => {
Ok(character_list_data) => self.notify_client( character_updater.process_batch_completion(batch_id);
query_result.entity,
ServerGeneral::CharacterListUpdate(character_list_data),
),
Err(error) => self.notify_client(
query_result.entity,
ServerGeneral::CharacterActionError(error.to_string()),
),
}, },
CharacterLoaderResponseKind::CharacterCreation(result) => match result { CharacterUpdaterMessage::CharacterScreenResponse(response) => {
Ok((character_id, list)) => { match response.response_kind {
self.notify_client( CharacterScreenResponseKind::CharacterList(result) => match result {
query_result.entity, Ok(character_list_data) => self.notify_client(
ServerGeneral::CharacterListUpdate(list), response.target_entity,
); ServerGeneral::CharacterListUpdate(character_list_data),
self.notify_client( ),
query_result.entity, Err(error) => self.notify_client(
ServerGeneral::CharacterCreated(character_id), response.target_entity,
); ServerGeneral::CharacterActionError(error.to_string()),
}, ),
Err(error) => self.notify_client(
query_result.entity,
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, skill_set_persistence_load_error)) => {
let PersistedComponents {
body,
stats,
skill_set,
inventory,
waypoint,
pets,
active_abilities,
map_marker,
} = character_data;
let character_data = (
body,
stats,
skill_set,
inventory,
waypoint,
pets,
active_abilities,
map_marker,
);
ServerEvent::UpdateCharacterData {
entity: query_result.entity,
components: character_data,
metadata: skill_set_persistence_load_error,
}
}, },
Err(error) => { CharacterScreenResponseKind::CharacterCreation(result) => match result {
// We failed to load data for the character from the DB. Notify the Ok((character_id, list)) => {
// client to push the state back to character selection, with the error self.notify_client(
// to display response.target_entity,
self.notify_client( ServerGeneral::CharacterListUpdate(list),
query_result.entity, );
ServerGeneral::CharacterDataLoadResult(Err(error.to_string())), self.notify_client(
); response.target_entity,
ServerGeneral::CharacterCreated(character_id),
// Clean up the entity data on the server );
ServerEvent::ExitIngame { },
entity: query_result.entity, Err(error) => self.notify_client(
} response.target_entity,
ServerGeneral::CharacterActionError(error.to_string()),
),
}, },
}; CharacterScreenResponseKind::CharacterEdit(result) => match result {
Ok((character_id, list)) => {
self.notify_client(
response.target_entity,
ServerGeneral::CharacterListUpdate(list),
);
self.notify_client(
response.target_entity,
ServerGeneral::CharacterEdited(character_id),
);
},
Err(error) => self.notify_client(
response.target_entity,
ServerGeneral::CharacterActionError(error.to_string()),
),
},
CharacterScreenResponseKind::CharacterData(result) => {
let message = match *result {
Ok((character_data, skill_set_persistence_load_error)) => {
let PersistedComponents {
body,
stats,
skill_set,
inventory,
waypoint,
pets,
active_abilities,
map_marker,
} = character_data;
let character_data = (
body,
stats,
skill_set,
inventory,
waypoint,
pets,
active_abilities,
map_marker,
);
ServerEvent::UpdateCharacterData {
entity: response.target_entity,
components: character_data,
metadata: skill_set_persistence_load_error,
}
},
Err(error) => {
// We failed to load data for the character from the DB. Notify
// the client to push the
// state back to character selection, with the error
// to display
self.notify_client(
response.target_entity,
ServerGeneral::CharacterDataLoadResult(Err(
error.to_string()
)),
);
self.state // Clean up the entity data on the server
.ecs() ServerEvent::ExitIngame {
.read_resource::<EventBus<ServerEvent>>() entity: response.target_entity,
.emit_now(message); }
},
};
self.state
.ecs()
.read_resource::<EventBus<ServerEvent>>()
.emit_now(message);
},
}
}, },
}); });

View File

@ -588,12 +588,14 @@ pub fn edit_character(
char_list.map(|list| (character_id, list)) char_list.map(|list| (character_id, list))
} }
/// Delete a character. Returns the updated character list. /// Permanently deletes a character
pub fn delete_character( pub fn delete_character(
requesting_player_uuid: &str, requesting_player_uuid: &str,
char_id: CharacterId, char_id: CharacterId,
transaction: &mut Transaction, transaction: &mut Transaction,
) -> CharacterListResult { ) -> Result<(), PersistenceError> {
debug!(?requesting_player_uuid, ?char_id, "Deleting character");
let mut stmt = transaction.prepare_cached( let mut stmt = transaction.prepare_cached(
" "
SELECT COUNT(1) SELECT COUNT(1)
@ -609,9 +611,9 @@ pub fn delete_character(
drop(stmt); drop(stmt);
if result != 1 { if result != 1 {
return Err(PersistenceError::OtherError( // The character does not exist, or does not belong to the requesting player so
"Requested character to delete does not belong to the requesting player".to_string(), // silently drop the request.
)); return Ok(());
} }
// Delete skill groups // Delete skill groups
@ -698,7 +700,7 @@ pub fn delete_character(
))); )));
} }
load_character_list(requesting_player_uuid, transaction) Ok(())
} }
/// Before creating a character, we ensure that the limit on the number of /// Before creating a character, we ensure that the limit on the number of

View File

@ -31,34 +31,39 @@ enum CharacterLoaderRequestKind {
}, },
} }
/// Wrapper for results for character actions. Can be a list of
/// characters, or component data belonging to an individual character
#[derive(Debug)] #[derive(Debug)]
pub enum CharacterLoaderResponseKind { pub enum CharacterUpdaterMessage {
CharacterScreenResponse(CharacterScreenResponse),
DatabaseBatchCompletion(u64),
}
/// An event emitted from CharacterUpdater in response to a request made from
/// the character selection/editing screen
#[derive(Debug)]
pub struct CharacterScreenResponse {
pub target_entity: specs::Entity,
pub response_kind: CharacterScreenResponseKind,
}
impl CharacterScreenResponse {
pub fn is_err(&self) -> bool {
matches!(
&self.response_kind,
CharacterScreenResponseKind::CharacterData(box Err(_))
| CharacterScreenResponseKind::CharacterList(Err(_))
| CharacterScreenResponseKind::CharacterCreation(Err(_))
)
}
}
#[derive(Debug)]
pub enum CharacterScreenResponseKind {
CharacterList(CharacterListResult), CharacterList(CharacterListResult),
CharacterData(Box<CharacterDataResult>), CharacterData(Box<CharacterDataResult>),
CharacterCreation(CharacterCreationResult), CharacterCreation(CharacterCreationResult),
CharacterEdit(CharacterEditResult), CharacterEdit(CharacterEditResult),
} }
/// Common message format dispatched in response to an update request
#[derive(Debug)]
pub struct CharacterLoaderResponse {
pub entity: specs::Entity,
pub result: CharacterLoaderResponseKind,
}
impl CharacterLoaderResponse {
pub fn is_err(&self) -> bool {
matches!(
&self.result,
CharacterLoaderResponseKind::CharacterData(box Err(_))
| CharacterLoaderResponseKind::CharacterList(Err(_))
| CharacterLoaderResponseKind::CharacterCreation(Err(_))
)
}
}
/// A bi-directional messaging resource for making requests to modify or load /// A bi-directional messaging resource for making requests to modify or load
/// character data in a background thread. /// character data in a background thread.
/// ///
@ -72,14 +77,14 @@ impl CharacterLoaderResponse {
/// Responses are polled on each server tick in the format /// Responses are polled on each server tick in the format
/// [`CharacterLoaderResponse`] /// [`CharacterLoaderResponse`]
pub struct CharacterLoader { pub struct CharacterLoader {
update_rx: crossbeam_channel::Receiver<CharacterLoaderResponse>, update_rx: crossbeam_channel::Receiver<CharacterUpdaterMessage>,
update_tx: crossbeam_channel::Sender<CharacterLoaderRequest>, update_tx: crossbeam_channel::Sender<CharacterLoaderRequest>,
} }
impl CharacterLoader { impl CharacterLoader {
pub fn new(settings: Arc<RwLock<DatabaseSettings>>) -> Result<Self, PersistenceError> { pub fn new(settings: Arc<RwLock<DatabaseSettings>>) -> Result<Self, PersistenceError> {
let (update_tx, internal_rx) = crossbeam_channel::unbounded::<CharacterLoaderRequest>(); let (update_tx, internal_rx) = crossbeam_channel::unbounded::<CharacterLoaderRequest>();
let (internal_tx, update_rx) = crossbeam_channel::unbounded::<CharacterLoaderResponse>(); let (internal_tx, update_rx) = crossbeam_channel::unbounded::<CharacterUpdaterMessage>();
let builder = std::thread::Builder::new().name("persistence_loader".into()); let builder = std::thread::Builder::new().name("persistence_loader".into());
builder builder
@ -115,13 +120,13 @@ impl CharacterLoader {
fn process_request( fn process_request(
request: CharacterLoaderRequest, request: CharacterLoaderRequest,
connection: &Connection, connection: &Connection,
) -> CharacterLoaderResponse { ) -> CharacterUpdaterMessage {
let (entity, kind) = request; let (entity, kind) = request;
CharacterLoaderResponse { CharacterUpdaterMessage::CharacterScreenResponse(CharacterScreenResponse {
entity, target_entity: entity,
result: match kind { response_kind: match kind {
CharacterLoaderRequestKind::LoadCharacterList { player_uuid } => { CharacterLoaderRequestKind::LoadCharacterList { player_uuid } => {
CharacterLoaderResponseKind::CharacterList(load_character_list( CharacterScreenResponseKind::CharacterList(load_character_list(
&player_uuid, &player_uuid,
connection, connection,
)) ))
@ -137,10 +142,10 @@ impl CharacterLoader {
"Error loading character data for character_id: {}", character_id "Error loading character data for character_id: {}", character_id
); );
} }
CharacterLoaderResponseKind::CharacterData(Box::new(result)) CharacterScreenResponseKind::CharacterData(Box::new(result))
}, },
}, },
} })
} }
/// Loads a list of characters belonging to the player identified by /// Loads a list of characters belonging to the player identified by
@ -175,5 +180,5 @@ impl CharacterLoader {
} }
/// Returns a non-blocking iterator over CharacterLoaderResponse messages /// Returns a non-blocking iterator over CharacterLoaderResponse messages
pub fn messages(&self) -> TryIter<CharacterLoaderResponse> { self.update_rx.try_iter() } pub fn messages(&self) -> TryIter<CharacterUpdaterMessage> { self.update_rx.try_iter() }
} }

View File

@ -2,13 +2,15 @@ use crate::comp;
use common::character::CharacterId; use common::character::CharacterId;
use crate::persistence::{ use crate::persistence::{
character_loader::{CharacterLoaderResponse, CharacterLoaderResponseKind}, character_loader::{
CharacterScreenResponse, CharacterScreenResponseKind, CharacterUpdaterMessage,
},
error::PersistenceError, error::PersistenceError,
establish_connection, ConnectionMode, DatabaseSettings, EditableComponents, establish_connection, ConnectionMode, DatabaseSettings, EditableComponents,
PersistedComponents, VelorenConnection, PersistedComponents, VelorenConnection,
}; };
use crossbeam_channel::TryIter; use crossbeam_channel::TryIter;
use rusqlite::{DropBehavior, Transaction}; use rusqlite::DropBehavior;
use specs::Entity; use specs::Entity;
use std::{ use std::{
collections::HashMap, collections::HashMap,
@ -20,6 +22,7 @@ use std::{
use tracing::{debug, error, info, trace, warn}; use tracing::{debug, error, info, trace, warn};
pub type CharacterUpdateData = ( pub type CharacterUpdateData = (
CharacterId,
comp::SkillSet, comp::SkillSet,
comp::Inventory, comp::Inventory,
Vec<PetPersistenceData>, Vec<PetPersistenceData>,
@ -31,8 +34,11 @@ pub type CharacterUpdateData = (
pub type PetPersistenceData = (comp::Pet, comp::Body, comp::Stats); pub type PetPersistenceData = (comp::Pet, comp::Body, comp::Stats);
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
pub enum CharacterUpdaterEvent { enum CharacterUpdaterAction {
BatchUpdate(Vec<(CharacterId, CharacterUpdateData)>), BatchUpdate {
batch_id: u64,
updates: Vec<DatabaseActionKind>,
},
CreateCharacter { CreateCharacter {
entity: Entity, entity: Entity,
player_uuid: String, player_uuid: String,
@ -46,12 +52,34 @@ pub enum CharacterUpdaterEvent {
character_alias: String, character_alias: String,
editable_components: EditableComponents, editable_components: EditableComponents,
}, },
DisconnectedSuccess,
}
#[derive(Clone)]
enum DatabaseAction {
New(DatabaseActionKind),
Submitted { batch_id: u64 },
}
impl DatabaseAction {
fn take_new(&mut self, batch_id: u64) -> Option<DatabaseActionKind> {
match core::mem::replace(self, Self::Submitted { batch_id }) {
Self::New(action) => Some(action),
submitted @ Self::Submitted { .. } => {
*self = submitted; // restore old batch_id
None
},
}
}
}
#[derive(Clone)]
enum DatabaseActionKind {
UpdateCharacter(Box<CharacterUpdateData>),
DeleteCharacter { DeleteCharacter {
entity: Entity,
requesting_player_uuid: String, requesting_player_uuid: String,
character_id: CharacterId, character_id: CharacterId,
}, },
DisconnectedSuccess,
} }
/// A unidirectional messaging resource for saving characters in a /// A unidirectional messaging resource for saving characters in a
@ -60,19 +88,22 @@ pub enum CharacterUpdaterEvent {
/// This is used to make updates to a character and their persisted components, /// This is used to make updates to a character and their persisted components,
/// such as inventory, loadout, etc... /// such as inventory, loadout, etc...
pub struct CharacterUpdater { pub struct CharacterUpdater {
update_tx: Option<crossbeam_channel::Sender<CharacterUpdaterEvent>>, update_tx: Option<crossbeam_channel::Sender<CharacterUpdaterAction>>,
response_rx: crossbeam_channel::Receiver<CharacterLoaderResponse>, response_rx: crossbeam_channel::Receiver<CharacterUpdaterMessage>,
handle: Option<std::thread::JoinHandle<()>>, handle: Option<std::thread::JoinHandle<()>>,
pending_logout_updates: HashMap<CharacterId, CharacterUpdateData>, /// Pending actions to be performed during the next persistence batch, such
/// as updates for recently logged out players and character deletions
pending_database_actions: HashMap<CharacterId, DatabaseAction>,
/// Will disconnect all characters (without persistence) on the next tick if /// Will disconnect all characters (without persistence) on the next tick if
/// set to true /// set to true
disconnect_all_clients_requested: Arc<AtomicBool>, disconnect_all_clients_requested: Arc<AtomicBool>,
last_pending_database_event_id: u64,
} }
impl CharacterUpdater { impl CharacterUpdater {
pub fn new(settings: Arc<RwLock<DatabaseSettings>>) -> rusqlite::Result<Self> { pub fn new(settings: Arc<RwLock<DatabaseSettings>>) -> rusqlite::Result<Self> {
let (update_tx, update_rx) = crossbeam_channel::unbounded::<CharacterUpdaterEvent>(); let (update_tx, update_rx) = crossbeam_channel::unbounded::<CharacterUpdaterAction>();
let (response_tx, response_rx) = crossbeam_channel::unbounded::<CharacterLoaderResponse>(); let (response_tx, response_rx) = crossbeam_channel::unbounded::<CharacterUpdaterMessage>();
let disconnect_all_clients_requested = Arc::new(AtomicBool::new(false)); let disconnect_all_clients_requested = Arc::new(AtomicBool::new(false));
let disconnect_all_clients_requested_clone = Arc::clone(&disconnect_all_clients_requested); let disconnect_all_clients_requested_clone = Arc::clone(&disconnect_all_clients_requested);
@ -84,9 +115,9 @@ impl CharacterUpdater {
// taken that could cause the RwLock to become poisoned. // taken that could cause the RwLock to become poisoned.
let mut conn = let mut conn =
establish_connection(&settings.read().unwrap(), ConnectionMode::ReadWrite); establish_connection(&settings.read().unwrap(), ConnectionMode::ReadWrite);
while let Ok(updates) = update_rx.recv() { while let Ok(action) = update_rx.recv() {
match updates { match action {
CharacterUpdaterEvent::BatchUpdate(updates) => { CharacterUpdaterAction::BatchUpdate { batch_id, updates } => {
if disconnect_all_clients_requested_clone.load(Ordering::Relaxed) { if disconnect_all_clients_requested_clone.load(Ordering::Relaxed) {
debug!( debug!(
"Skipping persistence due to pending disconnection of all \ "Skipping persistence due to pending disconnection of all \
@ -95,17 +126,29 @@ impl CharacterUpdater {
continue; continue;
} }
conn.update_log_mode(&settings); conn.update_log_mode(&settings);
if let Err(e) = execute_batch_update(updates, &mut conn) {
if let Err(e) = execute_batch_update(updates.into_iter(), &mut conn) {
error!( error!(
?e,
"Error during character batch update, disconnecting all \ "Error during character batch update, disconnecting all \
clients to avoid loss of data integrity. Error: {:?}", clients to avoid loss of data integrity."
e
); );
disconnect_all_clients_requested_clone disconnect_all_clients_requested_clone
.store(true, Ordering::Relaxed); .store(true, Ordering::Relaxed);
}; };
if let Err(e) = response_tx
.send(CharacterUpdaterMessage::DatabaseBatchCompletion(batch_id))
{
error!(?e, "Could not send DatabaseBatchCompletion message");
} else {
debug!(
"Submitted DatabaseBatchCompletion - Batch ID: {}",
batch_id
);
}
}, },
CharacterUpdaterEvent::CreateCharacter { CharacterUpdaterAction::CreateCharacter {
entity, entity,
character_alias, character_alias,
player_uuid, player_uuid,
@ -134,7 +177,7 @@ impl CharacterUpdater {
), ),
} }
}, },
CharacterUpdaterEvent::EditCharacter { CharacterUpdaterAction::EditCharacter {
entity, entity,
character_id, character_id,
character_alias, character_alias,
@ -165,34 +208,7 @@ impl CharacterUpdater {
), ),
} }
}, },
CharacterUpdaterEvent::DeleteCharacter { CharacterUpdaterAction::DisconnectedSuccess => {
entity,
requesting_player_uuid,
character_id,
} => {
match execute_character_delete(
entity,
&requesting_player_uuid,
character_id,
&mut conn,
) {
Ok(response) => {
if let Err(e) = response_tx.send(response) {
error!(?e, "Could not send character deletion response");
} else {
debug!(
"Processed character delete for character ID {}",
character_id
);
}
},
Err(e) => error!(
"Error deleting character ID {}, error: {:?}",
character_id, e
),
}
},
CharacterUpdaterEvent::DisconnectedSuccess => {
info!( info!(
"CharacterUpdater received DisconnectedSuccess event, resuming \ "CharacterUpdater received DisconnectedSuccess event, resuming \
batch updates" batch updates"
@ -210,37 +226,56 @@ impl CharacterUpdater {
update_tx: Some(update_tx), update_tx: Some(update_tx),
response_rx, response_rx,
handle: Some(handle), handle: Some(handle),
pending_logout_updates: HashMap::new(), pending_database_actions: HashMap::new(),
disconnect_all_clients_requested, disconnect_all_clients_requested,
last_pending_database_event_id: 0,
}) })
} }
/// Adds a character to the list of characters that have recently logged out /// Adds a character to the list of characters that have recently logged out
/// and will be persisted in the next batch update. /// and will be persisted in the next batch update.
pub fn add_pending_logout_update( pub fn add_pending_logout_update(&mut self, update_data: CharacterUpdateData) {
&mut self, if self
character_id: CharacterId,
update_data: CharacterUpdateData,
) {
if !self
.disconnect_all_clients_requested .disconnect_all_clients_requested
.load(Ordering::Relaxed) .load(Ordering::Relaxed)
{ {
self.pending_logout_updates
.insert(character_id, update_data);
} else {
warn!( warn!(
"Ignoring request to add pending logout update for character ID {} as there is a \ "Ignoring request to add pending logout update for character ID {} as there is a \
disconnection of all clients in progress", disconnection of all clients in progress",
character_id update_data.0
); );
return;
} }
if self.pending_database_actions.contains_key(&update_data.0) {
warn!(
"Ignoring request to add pending logout update for character ID {} as there is \
already a pending delete for this character",
update_data.0
);
return;
}
self.pending_database_actions.insert(
update_data.0, // CharacterId
DatabaseAction::New(DatabaseActionKind::UpdateCharacter(Box::new(update_data))),
);
} }
/// Returns the character IDs of characters that have recently logged out pub fn has_pending_database_action(&self, character_id: CharacterId) -> bool {
/// and are awaiting persistence in the next batch update. self.pending_database_actions.get(&character_id).is_some()
pub fn characters_pending_logout(&self) -> impl Iterator<Item = CharacterId> + '_ { }
self.pending_logout_updates.keys().copied()
pub fn process_batch_completion(&mut self, completed_batch_id: u64) {
self.pending_database_actions.drain_filter(|_, event| {
matches!(event, DatabaseAction::Submitted {
batch_id,
} if completed_batch_id == *batch_id)
});
debug!(
"Processed database batch completion - Batch ID: {}",
completed_batch_id
)
} }
/// Returns a value indicating whether there is a pending request to /// Returns a value indicating whether there is a pending request to
@ -261,7 +296,7 @@ impl CharacterUpdater {
self.update_tx self.update_tx
.as_ref() .as_ref()
.unwrap() .unwrap()
.send(CharacterUpdaterEvent::CreateCharacter { .send(CharacterUpdaterAction::CreateCharacter {
entity, entity,
player_uuid: requesting_player_uuid, player_uuid: requesting_player_uuid,
character_alias: alias, character_alias: alias,
@ -284,7 +319,7 @@ impl CharacterUpdater {
self.update_tx self.update_tx
.as_ref() .as_ref()
.unwrap() .unwrap()
.send(CharacterUpdaterEvent::EditCharacter { .send(CharacterUpdaterAction::EditCharacter {
entity, entity,
player_uuid: requesting_player_uuid, player_uuid: requesting_player_uuid,
character_id, character_id,
@ -296,81 +331,64 @@ impl CharacterUpdater {
} }
} }
pub fn delete_character( fn next_pending_database_event_id(&mut self) -> u64 {
self.last_pending_database_event_id += 1;
self.last_pending_database_event_id
}
pub fn queue_character_deletion(
&mut self, &mut self,
entity: Entity,
requesting_player_uuid: String, requesting_player_uuid: String,
character_id: CharacterId, character_id: CharacterId,
) { ) {
if let Err(e) = // Insert the delete as a pending database action - if the player has recently
self.update_tx // logged out this will replace their pending update with a delete which
.as_ref() // is fine, as the user has actively chosen to delete the character.
.unwrap() self.pending_database_actions.insert(
.send(CharacterUpdaterEvent::DeleteCharacter { character_id,
entity, DatabaseAction::New(DatabaseActionKind::DeleteCharacter {
requesting_player_uuid, requesting_player_uuid,
character_id, character_id,
}) }),
{ );
error!(?e, "Could not send character deletion request");
} else {
// Once a delete request has been sent to the channel we must remove any pending
// updates for the character in the event that it has recently logged out.
// Since the user has actively chosen to delete the character there is no value
// in the pending update data anyway.
self.pending_logout_updates.remove(&character_id);
}
} }
/// Updates a collection of characters based on their id and components /// Updates a collection of characters based on their id and components
pub fn batch_update<'a>( pub fn batch_update(&mut self, updates: impl Iterator<Item = CharacterUpdateData>) {
&mut self, let batch_id = self.next_pending_database_event_id();
updates: impl Iterator<
Item = (
CharacterId,
&'a comp::SkillSet,
&'a comp::Inventory,
Vec<PetPersistenceData>,
Option<&'a comp::Waypoint>,
&'a comp::ability::ActiveAbilities,
Option<&'a comp::MapMarker>,
),
>,
) {
let updates = updates
.map(
|(
character_id,
skill_set,
inventory,
pets,
waypoint,
active_abilities,
map_marker,
)| {
(
character_id,
(
skill_set.clone(),
inventory.clone(),
pets,
waypoint.cloned(),
active_abilities.clone(),
map_marker.cloned(),
),
)
},
)
.chain(self.pending_logout_updates.drain())
.collect::<Vec<_>>();
if let Err(e) = self // Collect any new updates, ignoring updates from a previous update that are
.update_tx // still pending completion
.as_ref() let existing_pending_actions = self
.unwrap() .pending_database_actions
.send(CharacterUpdaterEvent::BatchUpdate(updates)) .iter_mut()
{ .filter_map(|(_, event)| event.take_new(batch_id));
error!(?e, "Could not send stats updates");
// Combine the pending actions with the updates for logged in characters
let pending_actions = existing_pending_actions
.into_iter()
.chain(updates.map(|update| DatabaseActionKind::UpdateCharacter(Box::new(update))))
.collect::<Vec<DatabaseActionKind>>();
if !pending_actions.is_empty() {
debug!(
"Sending persistence update batch ID {} containing {} updates",
batch_id,
pending_actions.len()
);
if let Err(e) =
self.update_tx
.as_ref()
.unwrap()
.send(CharacterUpdaterAction::BatchUpdate {
batch_id,
updates: pending_actions,
})
{
error!(?e, "Could not send persistence batch update");
}
} else {
trace!("Skipping persistence batch - no pending updates")
} }
} }
@ -380,7 +398,7 @@ impl CharacterUpdater {
self.update_tx self.update_tx
.as_ref() .as_ref()
.unwrap() .unwrap()
.send(CharacterUpdaterEvent::DisconnectedSuccess) .send(CharacterUpdaterAction::DisconnectedSuccess)
.expect( .expect(
"Failed to send DisconnectedSuccess event - not sending this event will prevent \ "Failed to send DisconnectedSuccess event - not sending this event will prevent \
future persistence batches from running", future persistence batches from running",
@ -388,30 +406,45 @@ impl CharacterUpdater {
} }
/// Returns a non-blocking iterator over CharacterLoaderResponse messages /// Returns a non-blocking iterator over CharacterLoaderResponse messages
pub fn messages(&self) -> TryIter<CharacterLoaderResponse> { self.response_rx.try_iter() } pub fn messages(&self) -> TryIter<CharacterUpdaterMessage> { self.response_rx.try_iter() }
} }
fn execute_batch_update( fn execute_batch_update(
updates: Vec<(CharacterId, CharacterUpdateData)>, updates: impl Iterator<Item = DatabaseActionKind>,
connection: &mut VelorenConnection, connection: &mut VelorenConnection,
) -> Result<(), PersistenceError> { ) -> Result<(), PersistenceError> {
let mut transaction = connection.connection.transaction()?; let mut transaction = connection.connection.transaction()?;
transaction.set_drop_behavior(DropBehavior::Rollback); transaction.set_drop_behavior(DropBehavior::Rollback);
trace!("Transaction started for character batch update"); trace!("Transaction started for character batch update");
updates.into_iter().try_for_each( updates.into_iter().try_for_each(|event| match event {
|(character_id, (stats, inventory, pets, waypoint, active_abilities, map_marker))| { DatabaseActionKind::UpdateCharacter(box (
super::character::update( character_id,
character_id, stats,
stats, inventory,
inventory, pets,
pets, waypoint,
waypoint, active_abilities,
active_abilities, map_marker,
map_marker, )) => super::character::update(
&mut transaction, character_id,
) stats,
}, inventory,
)?; pets,
waypoint,
active_abilities,
map_marker,
&mut transaction,
),
DatabaseActionKind::DeleteCharacter {
requesting_player_uuid,
character_id,
} => super::character::delete_character(
&requesting_player_uuid,
character_id,
&mut transaction,
),
})?;
transaction.commit()?; transaction.commit()?;
trace!("Commit for character batch update completed"); trace!("Commit for character batch update completed");
@ -424,16 +457,26 @@ fn execute_character_create(
requesting_player_uuid: &str, requesting_player_uuid: &str,
persisted_components: PersistedComponents, persisted_components: PersistedComponents,
connection: &mut VelorenConnection, connection: &mut VelorenConnection,
) -> Result<CharacterLoaderResponse, PersistenceError> { ) -> Result<CharacterUpdaterMessage, PersistenceError> {
let mut transaction = connection.connection.transaction()?; let mut transaction = connection.connection.transaction()?;
let result =
CharacterLoaderResponseKind::CharacterCreation(super::character::create_character( let response = CharacterScreenResponse {
requesting_player_uuid, target_entity: entity,
&alias, response_kind: CharacterScreenResponseKind::CharacterCreation(
persisted_components, super::character::create_character(
&mut transaction, requesting_player_uuid,
)); &alias,
check_response(entity, transaction, result) persisted_components,
&mut transaction,
),
),
};
if !response.is_err() {
transaction.commit()?;
};
Ok(CharacterUpdaterMessage::CharacterScreenResponse(response))
} }
fn execute_character_edit( fn execute_character_edit(
@ -443,45 +486,27 @@ fn execute_character_edit(
requesting_player_uuid: &str, requesting_player_uuid: &str,
editable_components: EditableComponents, editable_components: EditableComponents,
connection: &mut VelorenConnection, connection: &mut VelorenConnection,
) -> Result<CharacterLoaderResponse, PersistenceError> { ) -> Result<CharacterUpdaterMessage, PersistenceError> {
let mut transaction = connection.connection.transaction()?; let mut transaction = connection.connection.transaction()?;
let result = CharacterLoaderResponseKind::CharacterEdit(super::character::edit_character(
editable_components,
&mut transaction,
character_id,
requesting_player_uuid,
&alias,
));
check_response(entity, transaction, result)
}
fn execute_character_delete( let response = CharacterScreenResponse {
entity: Entity, target_entity: entity,
requesting_player_uuid: &str, response_kind: CharacterScreenResponseKind::CharacterEdit(
character_id: CharacterId, super::character::edit_character(
connection: &mut VelorenConnection, editable_components,
) -> Result<CharacterLoaderResponse, PersistenceError> { &mut transaction,
let mut transaction = connection.connection.transaction()?; character_id,
let result = CharacterLoaderResponseKind::CharacterList(super::character::delete_character( requesting_player_uuid,
requesting_player_uuid, &alias,
character_id, ),
&mut transaction, ),
)); };
check_response(entity, transaction, result)
}
fn check_response(
entity: Entity,
transaction: Transaction,
result: CharacterLoaderResponseKind,
) -> Result<CharacterLoaderResponse, PersistenceError> {
let response = CharacterLoaderResponse { entity, result };
if !response.is_err() { if !response.is_err() {
transaction.commit()?; transaction.commit()?;
}; };
Ok(response) Ok(CharacterUpdaterMessage::CharacterScreenResponse(response))
} }
impl Drop for CharacterUpdater { impl Drop for CharacterUpdater {

View File

@ -78,9 +78,7 @@ impl Sys {
if let Some(player) = players.get(entity) { if let Some(player) = players.get(entity) {
if presences.contains(entity) { if presences.contains(entity) {
debug!("player already ingame, aborting"); debug!("player already ingame, aborting");
} else if character_updater } else if character_updater.has_pending_database_action(character_id)
.characters_pending_logout()
.any(|x| x == character_id)
{ {
debug!("player recently logged out pending persistence, aborting"); debug!("player recently logged out pending persistence, aborting");
client.send(ServerGeneral::CharacterDataLoadResult(Err( client.send(ServerGeneral::CharacterDataLoadResult(Err(
@ -192,11 +190,11 @@ impl Sys {
}, },
ClientGeneral::DeleteCharacter(character_id) => { ClientGeneral::DeleteCharacter(character_id) => {
if let Some(player) = players.get(entity) { if let Some(player) = players.get(entity) {
character_updater.delete_character( server_emitter.emit(ServerEvent::DeleteCharacter {
entity, entity,
player.uuid().to_string(), requesting_player_uuid: player.uuid().to_string(),
character_id, character_id,
); });
} }
}, },
_ => { _ => {

View File

@ -92,12 +92,12 @@ impl<'a> System<'a> for Sys {
Some(( Some((
id, id,
skill_set, skill_set.clone(),
inventory, inventory.clone(),
pets, pets,
waypoint, waypoint.cloned(),
active_abilities, active_abilities.clone(),
map_marker, map_marker.cloned(),
)) ))
}, },
PresenceKind::Spectator | PresenceKind::Possessor => None, PresenceKind::Spectator | PresenceKind::Possessor => None,

View File

@ -260,7 +260,6 @@ enum InfoContent {
LoadingCharacters, LoadingCharacters,
CreatingCharacter, CreatingCharacter,
EditingCharacter, EditingCharacter,
DeletingCharacter,
JoiningCharacter, JoiningCharacter,
CharacterError(String), CharacterError(String),
} }
@ -455,7 +454,6 @@ impl Controls {
Some(InfoContent::LoadingCharacters) Some(InfoContent::LoadingCharacters)
| Some(InfoContent::CreatingCharacter) | Some(InfoContent::CreatingCharacter)
| Some(InfoContent::EditingCharacter) | Some(InfoContent::EditingCharacter)
| Some(InfoContent::DeletingCharacter)
) && !client.character_list().loading ) && !client.character_list().loading
{ {
*info_content = None; *info_content = None;
@ -779,11 +777,6 @@ impl Controls {
.size(fonts.cyri.scale(24)) .size(fonts.cyri.scale(24))
.into() .into()
}, },
InfoContent::DeletingCharacter => {
Text::new(i18n.get_msg("char_selection-deleting_character"))
.size(fonts.cyri.scale(24))
.into()
},
InfoContent::JoiningCharacter => { InfoContent::JoiningCharacter => {
Text::new(i18n.get_msg("char_selection-joining_character")) Text::new(i18n.get_msg("char_selection-joining_character"))
.size(fonts.cyri.scale(24)) .size(fonts.cyri.scale(24))
@ -1443,7 +1436,7 @@ impl Controls {
events.push(Event::SelectCharacter(None)); events.push(Event::SelectCharacter(None));
} }
} }
*info_content = Some(InfoContent::DeletingCharacter); *info_content = None;
} }
} }
}, },