Changed character deletion to go via batch update

This commit is contained in:
Ben Wallis 2023-03-12 23:21:53 +00:00
parent fd34e48d15
commit 6eedc02286
38 changed files with 459 additions and 384 deletions

View File

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

View File

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

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Načítání postavy...
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-enter_world = Vstup do světa
char_selection-logout = Odhlásit

View File

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

View File

@ -1,6 +1,5 @@
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-spectate = Spectate World

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Cargando personajes...
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-enter_world = Entrar al mundo
char_selection-spectate = Observar mundo

View File

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

View File

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

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Chargement des personnages...
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-enter_world = Rejoindre
char_selection-spectate = Spectateur

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Karakterek betöltése...
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-enter_world = Világ betöltése
char_selection-logout = Kijelentkezés

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Ładowanie Postaci...
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-enter_world = Dołącz do Świata
char_selection-spectate = Oglądnij świat

View File

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

View File

@ -1,6 +1,5 @@
char_selection-loading_characters = Se încarcă Personajele...
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-enter_world = Intră în lume
char_selection-logout = Ieși din cont

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
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-deleting_character = Đang Xóa Nhân Vật...
char_selection-change_server = Đổi Máy Chủ
char_selection-enter_world = Vào Trò Chơi
char_selection-logout = Đăng xuất

View File

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

View File

@ -962,7 +962,17 @@ impl Client {
/// Character deletion
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));
}

View File

@ -233,6 +233,11 @@ pub enum ServerEvent {
admin: comp::Admin,
uuid: Uuid,
},
DeleteCharacter {
entity: EcsEntity,
requesting_player_uuid: String,
character_id: CharacterId,
},
}
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::{
character::CharacterId,
comp::{
@ -32,6 +35,11 @@ pub fn handle_initialize_character(
character_id: CharacterId,
requested_view_distances: ViewDistances,
) {
let updater = server.state.ecs().fetch::<CharacterUpdater>();
let pending_database_action = updater.has_pending_database_action(character_id);
drop(updater);
if !pending_database_action {
let clamped_vds = requested_view_distances.clamp(server.settings().max_view_distance);
server
.state
@ -40,6 +48,11 @@ pub fn handle_initialize_character(
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);
}
}
pub fn handle_initialize_spectator(

View File

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

View File

@ -4,6 +4,7 @@ use crate::{
presence::Presence, state_ext::StateExt, BattleModeBuffer, Server,
};
use common::{
character::CharacterId,
comp,
comp::{group, pet::is_tameable},
uid::{Uid, UidAllocator},
@ -14,13 +15,45 @@ use common_state::State;
use specs::{saveload::MarkerAllocator, Builder, Entity as EcsEntity, Join, WorldExt};
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");
let state = server.state_mut();
// Sync the player's character data to the database. This must be done before
// 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`
// components.
@ -264,17 +297,15 @@ fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity {
})
.collect();
character_updater.add_pending_logout_update(
character_updater.add_pending_logout_update((
char_id,
(
skill_set.clone(),
inventory.clone(),
pets,
waypoint,
active_abilities.clone(),
map_marker,
),
);
));
},
PresenceKind::Spectator => { /* Do nothing, spectators do not need persisting */ },
PresenceKind::Possessor => { /* Do nothing, possessor's are not persisted */ },

View File

@ -9,6 +9,7 @@
option_zip,
unwrap_infallible
)]
#![feature(hash_drain_filter)]
pub mod automod;
mod character_creator;
@ -92,7 +93,7 @@ use common_systems::add_local_systems;
use metrics::{EcsSystemMetrics, PhysicsMetrics, TickMetrics};
use network::{ListenAddr, Network, Pid};
use persistence::{
character_loader::{CharacterLoader, CharacterLoaderResponseKind},
character_loader::{CharacterLoader, CharacterUpdaterMessage},
character_updater::CharacterUpdater,
};
use prometheus::Registry;
@ -125,6 +126,7 @@ use {
common_state::plugin::{memory_manager::EcsWorld, PluginMgr},
};
use crate::persistence::character_loader::CharacterScreenResponseKind;
use common::comp::Anchor;
#[cfg(feature = "worldgen")]
use world::{
@ -834,56 +836,62 @@ impl Server {
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
character_loader
.messages()
.chain(character_updater.messages())
.for_each(|query_result| match query_result.result {
CharacterLoaderResponseKind::CharacterList(result) => match result {
.chain(updater_messages)
.for_each(|message| match message {
CharacterUpdaterMessage::DatabaseBatchCompletion(batch_id) => {
character_updater.process_batch_completion(batch_id);
},
CharacterUpdaterMessage::CharacterScreenResponse(response) => {
match response.response_kind {
CharacterScreenResponseKind::CharacterList(result) => match result {
Ok(character_list_data) => self.notify_client(
query_result.entity,
response.target_entity,
ServerGeneral::CharacterListUpdate(character_list_data),
),
Err(error) => self.notify_client(
query_result.entity,
response.target_entity,
ServerGeneral::CharacterActionError(error.to_string()),
),
},
CharacterLoaderResponseKind::CharacterCreation(result) => match result {
CharacterScreenResponseKind::CharacterCreation(result) => match result {
Ok((character_id, list)) => {
self.notify_client(
query_result.entity,
response.target_entity,
ServerGeneral::CharacterListUpdate(list),
);
self.notify_client(
query_result.entity,
response.target_entity,
ServerGeneral::CharacterCreated(character_id),
);
},
Err(error) => self.notify_client(
query_result.entity,
response.target_entity,
ServerGeneral::CharacterActionError(error.to_string()),
),
},
CharacterLoaderResponseKind::CharacterEdit(result) => match result {
CharacterScreenResponseKind::CharacterEdit(result) => match result {
Ok((character_id, list)) => {
self.notify_client(
query_result.entity,
response.target_entity,
ServerGeneral::CharacterListUpdate(list),
);
self.notify_client(
query_result.entity,
response.target_entity,
ServerGeneral::CharacterEdited(character_id),
);
},
Err(error) => self.notify_client(
query_result.entity,
response.target_entity,
ServerGeneral::CharacterActionError(error.to_string()),
),
},
CharacterLoaderResponseKind::CharacterData(result) => {
CharacterScreenResponseKind::CharacterData(result) => {
let message = match *result {
Ok((character_data, skill_set_persistence_load_error)) => {
let PersistedComponents {
@ -907,23 +915,26 @@ impl Server {
map_marker,
);
ServerEvent::UpdateCharacterData {
entity: query_result.entity,
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
// 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(
query_result.entity,
ServerGeneral::CharacterDataLoadResult(Err(error.to_string())),
response.target_entity,
ServerGeneral::CharacterDataLoadResult(Err(
error.to_string()
)),
);
// Clean up the entity data on the server
ServerEvent::ExitIngame {
entity: query_result.entity,
entity: response.target_entity,
}
},
};
@ -933,6 +944,8 @@ impl Server {
.read_resource::<EventBus<ServerEvent>>()
.emit_now(message);
},
}
},
});
drop(character_loader);

View File

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

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)]
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),
CharacterData(Box<CharacterDataResult>),
CharacterCreation(CharacterCreationResult),
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
/// character data in a background thread.
///
@ -72,14 +77,14 @@ impl CharacterLoaderResponse {
/// Responses are polled on each server tick in the format
/// [`CharacterLoaderResponse`]
pub struct CharacterLoader {
update_rx: crossbeam_channel::Receiver<CharacterLoaderResponse>,
update_rx: crossbeam_channel::Receiver<CharacterUpdaterMessage>,
update_tx: crossbeam_channel::Sender<CharacterLoaderRequest>,
}
impl CharacterLoader {
pub fn new(settings: Arc<RwLock<DatabaseSettings>>) -> Result<Self, PersistenceError> {
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());
builder
@ -115,13 +120,13 @@ impl CharacterLoader {
fn process_request(
request: CharacterLoaderRequest,
connection: &Connection,
) -> CharacterLoaderResponse {
) -> CharacterUpdaterMessage {
let (entity, kind) = request;
CharacterLoaderResponse {
entity,
result: match kind {
CharacterUpdaterMessage::CharacterScreenResponse(CharacterScreenResponse {
target_entity: entity,
response_kind: match kind {
CharacterLoaderRequestKind::LoadCharacterList { player_uuid } => {
CharacterLoaderResponseKind::CharacterList(load_character_list(
CharacterScreenResponseKind::CharacterList(load_character_list(
&player_uuid,
connection,
))
@ -137,10 +142,10 @@ impl CharacterLoader {
"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
@ -175,5 +180,5 @@ impl CharacterLoader {
}
/// 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 crate::persistence::{
character_loader::{CharacterLoaderResponse, CharacterLoaderResponseKind},
character_loader::{
CharacterScreenResponse, CharacterScreenResponseKind, CharacterUpdaterMessage,
},
error::PersistenceError,
establish_connection, ConnectionMode, DatabaseSettings, EditableComponents,
PersistedComponents, VelorenConnection,
};
use crossbeam_channel::TryIter;
use rusqlite::{DropBehavior, Transaction};
use rusqlite::DropBehavior;
use specs::Entity;
use std::{
collections::HashMap,
@ -20,6 +22,7 @@ use std::{
use tracing::{debug, error, info, trace, warn};
pub type CharacterUpdateData = (
CharacterId,
comp::SkillSet,
comp::Inventory,
Vec<PetPersistenceData>,
@ -31,8 +34,11 @@ pub type CharacterUpdateData = (
pub type PetPersistenceData = (comp::Pet, comp::Body, comp::Stats);
#[allow(clippy::large_enum_variant)]
pub enum CharacterUpdaterEvent {
BatchUpdate(Vec<(CharacterId, CharacterUpdateData)>),
enum CharacterUpdaterAction {
BatchUpdate {
batch_id: u64,
updates: Vec<DatabaseActionKind>,
},
CreateCharacter {
entity: Entity,
player_uuid: String,
@ -46,12 +52,34 @@ pub enum CharacterUpdaterEvent {
character_alias: String,
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 {
entity: Entity,
requesting_player_uuid: String,
character_id: CharacterId,
},
DisconnectedSuccess,
}
/// 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,
/// such as inventory, loadout, etc...
pub struct CharacterUpdater {
update_tx: Option<crossbeam_channel::Sender<CharacterUpdaterEvent>>,
response_rx: crossbeam_channel::Receiver<CharacterLoaderResponse>,
update_tx: Option<crossbeam_channel::Sender<CharacterUpdaterAction>>,
response_rx: crossbeam_channel::Receiver<CharacterUpdaterMessage>,
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
/// set to true
disconnect_all_clients_requested: Arc<AtomicBool>,
last_pending_database_event_id: u64,
}
impl CharacterUpdater {
pub fn new(settings: Arc<RwLock<DatabaseSettings>>) -> rusqlite::Result<Self> {
let (update_tx, update_rx) = crossbeam_channel::unbounded::<CharacterUpdaterEvent>();
let (response_tx, response_rx) = crossbeam_channel::unbounded::<CharacterLoaderResponse>();
let (update_tx, update_rx) = crossbeam_channel::unbounded::<CharacterUpdaterAction>();
let (response_tx, response_rx) = crossbeam_channel::unbounded::<CharacterUpdaterMessage>();
let disconnect_all_clients_requested = Arc::new(AtomicBool::new(false));
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.
let mut conn =
establish_connection(&settings.read().unwrap(), ConnectionMode::ReadWrite);
while let Ok(updates) = update_rx.recv() {
match updates {
CharacterUpdaterEvent::BatchUpdate(updates) => {
while let Ok(action) = update_rx.recv() {
match action {
CharacterUpdaterAction::BatchUpdate { batch_id, updates } => {
if disconnect_all_clients_requested_clone.load(Ordering::Relaxed) {
debug!(
"Skipping persistence due to pending disconnection of all \
@ -95,17 +126,29 @@ impl CharacterUpdater {
continue;
}
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!(
?e,
"Error during character batch update, disconnecting all \
clients to avoid loss of data integrity. Error: {:?}",
e
clients to avoid loss of data integrity."
);
disconnect_all_clients_requested_clone
.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,
character_alias,
player_uuid,
@ -134,7 +177,7 @@ impl CharacterUpdater {
),
}
},
CharacterUpdaterEvent::EditCharacter {
CharacterUpdaterAction::EditCharacter {
entity,
character_id,
character_alias,
@ -165,34 +208,7 @@ impl CharacterUpdater {
),
}
},
CharacterUpdaterEvent::DeleteCharacter {
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 => {
CharacterUpdaterAction::DisconnectedSuccess => {
info!(
"CharacterUpdater received DisconnectedSuccess event, resuming \
batch updates"
@ -210,37 +226,56 @@ impl CharacterUpdater {
update_tx: Some(update_tx),
response_rx,
handle: Some(handle),
pending_logout_updates: HashMap::new(),
pending_database_actions: HashMap::new(),
disconnect_all_clients_requested,
last_pending_database_event_id: 0,
})
}
/// Adds a character to the list of characters that have recently logged out
/// and will be persisted in the next batch update.
pub fn add_pending_logout_update(
&mut self,
character_id: CharacterId,
update_data: CharacterUpdateData,
) {
if !self
pub fn add_pending_logout_update(&mut self, update_data: CharacterUpdateData) {
if self
.disconnect_all_clients_requested
.load(Ordering::Relaxed)
{
self.pending_logout_updates
.insert(character_id, update_data);
} else {
warn!(
"Ignoring request to add pending logout update for character ID {} as there is a \
disconnection of all clients in progress",
character_id
update_data.0
);
}
return;
}
/// Returns the character IDs of characters that have recently logged out
/// and are awaiting persistence in the next batch update.
pub fn characters_pending_logout(&self) -> impl Iterator<Item = CharacterId> + '_ {
self.pending_logout_updates.keys().copied()
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))),
);
}
pub fn has_pending_database_action(&self, character_id: CharacterId) -> bool {
self.pending_database_actions.get(&character_id).is_some()
}
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
@ -261,7 +296,7 @@ impl CharacterUpdater {
self.update_tx
.as_ref()
.unwrap()
.send(CharacterUpdaterEvent::CreateCharacter {
.send(CharacterUpdaterAction::CreateCharacter {
entity,
player_uuid: requesting_player_uuid,
character_alias: alias,
@ -284,7 +319,7 @@ impl CharacterUpdater {
self.update_tx
.as_ref()
.unwrap()
.send(CharacterUpdaterEvent::EditCharacter {
.send(CharacterUpdaterAction::EditCharacter {
entity,
player_uuid: requesting_player_uuid,
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,
entity: Entity,
requesting_player_uuid: String,
character_id: CharacterId,
) {
// Insert the delete as a pending database action - if the player has recently
// logged out this will replace their pending update with a delete which
// is fine, as the user has actively chosen to delete the character.
self.pending_database_actions.insert(
character_id,
DatabaseAction::New(DatabaseActionKind::DeleteCharacter {
requesting_player_uuid,
character_id,
}),
);
}
/// Updates a collection of characters based on their id and components
pub fn batch_update(&mut self, updates: impl Iterator<Item = CharacterUpdateData>) {
let batch_id = self.next_pending_database_event_id();
// Collect any new updates, ignoring updates from a previous update that are
// still pending completion
let existing_pending_actions = self
.pending_database_actions
.iter_mut()
.filter_map(|(_, event)| event.take_new(batch_id));
// 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(CharacterUpdaterEvent::DeleteCharacter {
entity,
requesting_player_uuid,
character_id,
.send(CharacterUpdaterAction::BatchUpdate {
batch_id,
updates: pending_actions,
})
{
error!(?e, "Could not send character deletion request");
error!(?e, "Could not send persistence batch update");
}
} 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
pub fn batch_update<'a>(
&mut self,
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
.update_tx
.as_ref()
.unwrap()
.send(CharacterUpdaterEvent::BatchUpdate(updates))
{
error!(?e, "Could not send stats updates");
trace!("Skipping persistence batch - no pending updates")
}
}
@ -380,7 +398,7 @@ impl CharacterUpdater {
self.update_tx
.as_ref()
.unwrap()
.send(CharacterUpdaterEvent::DisconnectedSuccess)
.send(CharacterUpdaterAction::DisconnectedSuccess)
.expect(
"Failed to send DisconnectedSuccess event - not sending this event will prevent \
future persistence batches from running",
@ -388,19 +406,26 @@ impl CharacterUpdater {
}
/// 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(
updates: Vec<(CharacterId, CharacterUpdateData)>,
updates: impl Iterator<Item = DatabaseActionKind>,
connection: &mut VelorenConnection,
) -> Result<(), PersistenceError> {
let mut transaction = connection.connection.transaction()?;
transaction.set_drop_behavior(DropBehavior::Rollback);
trace!("Transaction started for character batch update");
updates.into_iter().try_for_each(
|(character_id, (stats, inventory, pets, waypoint, active_abilities, map_marker))| {
super::character::update(
updates.into_iter().try_for_each(|event| match event {
DatabaseActionKind::UpdateCharacter(box (
character_id,
stats,
inventory,
pets,
waypoint,
active_abilities,
map_marker,
)) => super::character::update(
character_id,
stats,
inventory,
@ -409,9 +434,17 @@ fn execute_batch_update(
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()?;
trace!("Commit for character batch update completed");
@ -424,16 +457,26 @@ fn execute_character_create(
requesting_player_uuid: &str,
persisted_components: PersistedComponents,
connection: &mut VelorenConnection,
) -> Result<CharacterLoaderResponse, PersistenceError> {
) -> Result<CharacterUpdaterMessage, PersistenceError> {
let mut transaction = connection.connection.transaction()?;
let result =
CharacterLoaderResponseKind::CharacterCreation(super::character::create_character(
let response = CharacterScreenResponse {
target_entity: entity,
response_kind: CharacterScreenResponseKind::CharacterCreation(
super::character::create_character(
requesting_player_uuid,
&alias,
persisted_components,
&mut transaction,
));
check_response(entity, transaction, result)
),
),
};
if !response.is_err() {
transaction.commit()?;
};
Ok(CharacterUpdaterMessage::CharacterScreenResponse(response))
}
fn execute_character_edit(
@ -443,45 +486,27 @@ fn execute_character_edit(
requesting_player_uuid: &str,
editable_components: EditableComponents,
connection: &mut VelorenConnection,
) -> Result<CharacterLoaderResponse, PersistenceError> {
) -> Result<CharacterUpdaterMessage, PersistenceError> {
let mut transaction = connection.connection.transaction()?;
let result = CharacterLoaderResponseKind::CharacterEdit(super::character::edit_character(
let response = CharacterScreenResponse {
target_entity: entity,
response_kind: CharacterScreenResponseKind::CharacterEdit(
super::character::edit_character(
editable_components,
&mut transaction,
character_id,
requesting_player_uuid,
&alias,
));
check_response(entity, transaction, result)
}
fn execute_character_delete(
entity: Entity,
requesting_player_uuid: &str,
character_id: CharacterId,
connection: &mut VelorenConnection,
) -> Result<CharacterLoaderResponse, PersistenceError> {
let mut transaction = connection.connection.transaction()?;
let result = CharacterLoaderResponseKind::CharacterList(super::character::delete_character(
requesting_player_uuid,
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() {
transaction.commit()?;
};
Ok(response)
Ok(CharacterUpdaterMessage::CharacterScreenResponse(response))
}
impl Drop for CharacterUpdater {

View File

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

View File

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

View File

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