From 52a62b403eeca80aaa9236e1b3207796250efcdc Mon Sep 17 00:00:00 2001 From: Imbris Date: Thu, 23 Jul 2026 21:51:48 -0400 Subject: [PATCH 1/3] Use Content for character location names --- client/src/lib.rs | 6 +- common/net/src/msg/server.rs | 4 +- common/src/character.rs | 5 +- server/src/cmd.rs | 2 + server/src/lib.rs | 67 ++++++++++++---------- server/src/persistence/character/mod.rs | 12 +++- server/src/persistence/character_loader.rs | 9 +-- server/src/persistence/mod.rs | 3 - server/src/sys/waypoint.rs | 4 +- voxygen/src/hud/diary.rs | 9 ++- voxygen/src/menu/char_selection/ui.rs | 16 ++++-- 11 files changed, 81 insertions(+), 56 deletions(-) diff --git a/client/src/lib.rs b/client/src/lib.rs index 52fb4e9a20..7ba81141b5 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -323,7 +323,7 @@ pub struct Client { pending_invites: HashSet, // The pending trade the client is involved in, and it's id pending_trade: Option<(TradeId, PendingTrade, Option)>, - waypoint: Option, + waypoint: Option, network: Option, participant: Option, @@ -367,7 +367,7 @@ pub struct Client { /// additional state to handle UI. #[derive(Debug, Default)] pub struct CharacterList { - pub characters: Vec, + pub characters: Vec>, pub loading: bool, } @@ -2315,7 +2315,7 @@ impl Client { })) } - pub fn waypoint(&self) -> &Option { &self.waypoint } + pub fn waypoint(&self) -> Option<&Content> { self.waypoint.as_ref() } pub fn set_battle_mode(&mut self, battle_mode: BattleMode) { self.send_msg(ClientGeneral::SetBattleMode(battle_mode)); diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs index d13ea76133..73688ad2be 100644 --- a/common/net/src/msg/server.rs +++ b/common/net/src/msg/server.rs @@ -147,7 +147,7 @@ pub enum ServerGeneral { /// Result of loading character data CharacterDataLoadResult(Result), /// A list of characters belonging to the a authenticated player was sent - CharacterListUpdate(Vec), + CharacterListUpdate(Vec>), /// An error occurred while creating or deleting a character CharacterActionError(String), /// A new character was created @@ -304,7 +304,7 @@ pub enum InviteAnswer { /// not relevant to rendering. #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Notification { - WaypointSaved { location_name: String }, + WaypointSaved { location_name: Content }, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] diff --git a/common/src/character.rs b/common/src/character.rs index 5f50ea6b5a..59af9caf7c 100644 --- a/common/src/character.rs +++ b/common/src/character.rs @@ -21,11 +21,10 @@ pub struct Character { /// Data needed to render a single character item in the character list /// presented during character selection. #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CharacterItem { +pub struct CharacterItem { pub character: Character, pub body: comp::Body, pub hardcore: bool, pub inventory: Inventory, - // this string changes between database representation and human readable name in server.tick - pub location: Option, + pub location: Option, } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 40acb0972b..d365a5e525 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -3878,6 +3878,8 @@ fn handle_set_waypoint( ); if let Some(location_name) = location_name { + #[expect(deprecated, reason = "i18n location name")] + let location_name = Content::legacy(location_name); server.notify_client( target, ServerGeneral::Notification(Notification::WaypointSaved { location_name }), diff --git a/server/src/lib.rs b/server/src/lib.rs index a8cc31775d..632428099a 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -759,22 +759,32 @@ impl Server { /// Get a reference to the Chat Cache pub fn chat_cache(&self) -> &ChatCache { &self.chat_cache } - fn parse_locations(&self, character_list_data: &mut [CharacterItem]) { - character_list_data.iter_mut().for_each(|c| { - let name = c - .location - .as_ref() - .and_then(|s| { - persistence::parse_waypoint(s) - .ok() - .and_then(|(waypoint, _)| waypoint.map(|w| w.get_pos())) - }) - .and_then(|wpos| { - self.world - .get_location_name(self.index.as_index_ref(), wpos.xy().as_::()) - }); - c.location = name; - }); + /// Converts positions to location names for a list of characters. + fn get_location_names( + &self, + character_list: Vec>>, + ) -> Vec> { + character_list + .into_iter() + .map(|c| { + #[expect(deprecated, reason = "i18n location name")] + let name = c + .location + .as_ref() + .and_then(|wpos| { + self.world + .get_location_name(self.index.as_index_ref(), wpos.xy().as_::()) + }) + .map(Content::legacy); + CharacterItem { + character: c.character, + body: c.body, + hardcore: c.hardcore, + inventory: c.inventory, + location: name, + } + }) + .collect() } /// Execute a single server tick, handle input and update the game state by @@ -1061,24 +1071,22 @@ impl Server { CharacterUpdaterMessage::CharacterScreenResponse(response) => { match response.response_kind { CharacterScreenResponseKind::CharacterList(result) => match result { - Ok(mut character_list_data) => { - self.parse_locations(&mut character_list_data); - self.notify_client( - response.target_entity, - ServerGeneral::CharacterListUpdate(character_list_data), - ) - }, + Ok(list) => self.notify_client( + response.target_entity, + ServerGeneral::CharacterListUpdate(self.get_location_names(list)), + ), Err(error) => self.notify_client( response.target_entity, ServerGeneral::CharacterActionError(error.to_string()), ), }, CharacterScreenResponseKind::CharacterCreation(result) => match result { - Ok((character_id, mut list)) => { - self.parse_locations(&mut list); + Ok((character_id, list)) => { self.notify_client( response.target_entity, - ServerGeneral::CharacterListUpdate(list), + ServerGeneral::CharacterListUpdate( + self.get_location_names(list), + ), ); self.notify_client( response.target_entity, @@ -1091,11 +1099,12 @@ impl Server { ), }, CharacterScreenResponseKind::CharacterEdit(result) => match result { - Ok((character_id, mut list)) => { - self.parse_locations(&mut list); + Ok((character_id, list)) => { self.notify_client( response.target_entity, - ServerGeneral::CharacterListUpdate(list), + ServerGeneral::CharacterListUpdate( + self.get_location_names(list), + ), ); self.notify_client( response.target_entity, diff --git a/server/src/persistence/character/mod.rs b/server/src/persistence/character/mod.rs index e32fd84cce..ed1ee778e5 100644 --- a/server/src/persistence/character/mod.rs +++ b/server/src/persistence/character/mod.rs @@ -44,8 +44,6 @@ mod conversions; pub(crate) type EntityId = i64; -pub(crate) use conversions::convert_waypoint_from_database_json as parse_waypoint; - const CHARACTER_PSEUDO_CONTAINER_DEF_ID: &str = "veloren.core.pseudo_containers.character"; const INVENTORY_PSEUDO_CONTAINER_DEF_ID: &str = "veloren.core.pseudo_containers.inventory"; const LOADOUT_PSEUDO_CONTAINER_DEF_ID: &str = "veloren.core.pseudo_containers.loadout"; @@ -395,13 +393,21 @@ pub fn load_character_list(player_uuid_: &str, connection: &Connection) -> Chara let (recipe_book, _) = convert_recipe_book_from_database_items(&recipe_book_items)?; + let location = character_data + .waypoint + .as_ref() + .map(|s| convert_waypoint_from_database_json(s)) + .transpose()? + .and_then(|(waypoint, _)| waypoint) + .map(|w| w.get_pos()); + Ok(CharacterItem { character: char, body: char_body, hardcore: hardcore.is_some(), inventory: Inventory::with_loadout(loadout, char_body) .with_recipe_book(recipe_book), - location: character_data.waypoint.as_ref().cloned(), + location, }) }) .collect() diff --git a/server/src/persistence/character_loader.rs b/server/src/persistence/character_loader.rs index 68bcab12cc..912aa7f71e 100644 --- a/server/src/persistence/character_loader.rs +++ b/server/src/persistence/character_loader.rs @@ -12,11 +12,12 @@ use crossbeam_channel::{self, TryIter}; use rusqlite::Connection; use std::sync::{Arc, RwLock}; use tracing::{debug, error}; +use vek::Vec3; -pub(crate) type CharacterListResult = Result, PersistenceError>; -pub(crate) type CharacterCreationResult = - Result<(CharacterId, Vec), PersistenceError>; -pub(crate) type CharacterEditResult = Result<(CharacterId, Vec), PersistenceError>; +type CharacterList = Vec>>; +pub(crate) type CharacterListResult = Result; +pub(crate) type CharacterCreationResult = Result<(CharacterId, CharacterList), PersistenceError>; +pub(crate) type CharacterEditResult = Result<(CharacterId, CharacterList), PersistenceError>; pub(crate) type CharacterDataResult = Result<(PersistedComponents, UpdateCharacterMetadata), PersistenceError>; type CharacterLoaderRequest = (specs::Entity, CharacterLoaderRequestKind); diff --git a/server/src/persistence/mod.rs b/server/src/persistence/mod.rs index 3fbd5ef2eb..06635cf4e4 100644 --- a/server/src/persistence/mod.rs +++ b/server/src/persistence/mod.rs @@ -27,9 +27,6 @@ use std::{ }; use tracing::info; -// re-export waypoint parser for use to look up location names in character list -pub(crate) use character::parse_waypoint; - /// A struct of the components that are persisted to the DB for each character #[derive(Debug)] pub struct PersistedComponents { diff --git a/server/src/sys/waypoint.rs b/server/src/sys/waypoint.rs index 136cd2f1c3..fdd26efe20 100644 --- a/server/src/sys/waypoint.rs +++ b/server/src/sys/waypoint.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use crate::client::Client; use common::{ - comp::{CharacterState, PhysicsState, Player, Pos, Vel, Waypoint, WaypointArea}, + comp::{CharacterState, Content, PhysicsState, Player, Pos, Vel, Waypoint, WaypointArea}, resources::Time, }; use common_ecs::{Job, Origin, Phase, System}; @@ -83,6 +83,8 @@ impl<'a> System<'a> for Sys { ); if let Some(location_name) = location_name { + #[expect(deprecated, reason = "i18n location name")] + let location_name = Content::legacy(location_name); client.send_fallible(ServerGeneral::Notification( Notification::WaypointSaved { location_name }, )); diff --git a/voxygen/src/hud/diary.rs b/voxygen/src/hud/diary.rs index 6932871bc6..1b60540e96 100644 --- a/voxygen/src/hud/diary.rs +++ b/voxygen/src/hud/diary.rs @@ -1244,9 +1244,12 @@ impl Widget for Diary<'_> { CharacterStat::Waypoint => self .client .waypoint() - .as_ref() - .cloned() - .unwrap_or_else(|| "Unknown".to_string()), + .map(|c| self.localized_strings.get_content(c)) + .unwrap_or_else(|| { + self.localized_strings + .get_msg("char_selection-uncanny_valley") + .into_owned() + }), CharacterStat::Hitpoints => format!("{}", self.health.maximum() as u32), CharacterStat::Energy => format!("{}", self.energy.maximum() as u32), CharacterStat::Poise => format!("{}", self.poise.maximum() as u32), diff --git a/voxygen/src/menu/char_selection/ui.rs b/voxygen/src/menu/char_selection/ui.rs index 111707c768..c51f218e01 100644 --- a/voxygen/src/menu/char_selection/ui.rs +++ b/voxygen/src/menu/char_selection/ui.rs @@ -31,6 +31,7 @@ use common::{ terrain::TerrainChunkSize, vol::RectVolSize, }; +use common_i18n::Content; use common_net::msg::world_msg::SiteId; use i18n::{Localization, LocalizationHandle}; use rand::{RngExt, rng}; @@ -667,9 +668,9 @@ impl Controls { i18n.get_msg( "char_selection-uncanny_valley", ) - .to_string() + .into_owned() }, - |s| s.clone(), + |c| i18n.get_content(c), )) .into(), ]), @@ -1760,7 +1761,12 @@ impl Controls { .into() } - fn update(&mut self, message: Message, events: &mut Vec, characters: &[CharacterItem]) { + fn update( + &mut self, + message: Message, + events: &mut Vec, + characters: &[CharacterItem], + ) { match message { Message::Back => { if matches!(&self.mode, Mode::CreateOrEdit { .. }) { @@ -2050,7 +2056,7 @@ impl Controls { /// Get the character to display pub fn display_body_inventory<'a>( &'a self, - characters: &'a [CharacterItem], + characters: &'a [CharacterItem], ) -> Option<(comp::Body, &'a Inventory)> { match &self.mode { Mode::Select { .. } => self @@ -2133,7 +2139,7 @@ impl CharSelectionUi { pub fn display_body_inventory<'a>( &'a self, - characters: &'a [CharacterItem], + characters: &'a [CharacterItem], ) -> Option<(comp::Body, &'a Inventory)> { self.controls.display_body_inventory(characters) } From e13c4dd9f400e3e1d5f1cadb3144e9b2c43c4b05 Mon Sep 17 00:00:00 2001 From: Imbris Date: Thu, 30 Jul 2026 21:48:53 -0400 Subject: [PATCH 2/3] Consistently handle errors when deserializing waypoint data --- server/src/persistence/character/mod.rs | 50 +++++++++++++------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/server/src/persistence/character/mod.rs b/server/src/persistence/character/mod.rs index ed1ee778e5..726f64722e 100644 --- a/server/src/persistence/character/mod.rs +++ b/server/src/persistence/character/mod.rs @@ -8,7 +8,7 @@ extern crate rusqlite; use super::{error::PersistenceError, models::*}; use crate::{ - comp::{self, Inventory}, + comp::{self, Inventory, MapMarker, Waypoint}, persistence::{ EditableComponents, PersistedComponents, character::conversions::{ @@ -122,6 +122,24 @@ pub fn load_items(connection: &Connection, root: i64) -> Result, Persi Ok(items) } +fn convert_waypoint_or_warn( + waypoint_json: Option<&str>, + char_id: CharacterId, +) -> (Option, Option) { + match waypoint_json.map(convert_waypoint_from_database_json) { + Some(Ok(w)) => w, + Some(Err(e)) => { + warn!( + "Error reading waypoint from database for character ID + {}, error: {}", + char_id.0, e + ); + (None, None) + }, + None => (None, None), + } +} + /// Load stored data for a character. /// /// After first logging in, and after a character is selected, we fetch this @@ -173,22 +191,8 @@ pub fn load_character_data( }, )?; - let (char_waypoint, char_map_marker) = match character_data - .waypoint - .as_ref() - .map(|x| convert_waypoint_from_database_json(x)) - { - Some(Ok(w)) => w, - Some(Err(e)) => { - warn!( - "Error reading waypoint from database for character ID - {}, error: {}", - char_id.0, e - ); - (None, None) - }, - None => (None, None), - }; + let (char_waypoint, char_map_marker) = + convert_waypoint_or_warn(character_data.waypoint.as_deref(), char_id); let mut stmt = connection.prepare_cached( " @@ -393,13 +397,11 @@ pub fn load_character_list(player_uuid_: &str, connection: &Connection) -> Chara let (recipe_book, _) = convert_recipe_book_from_database_items(&recipe_book_items)?; - let location = character_data - .waypoint - .as_ref() - .map(|s| convert_waypoint_from_database_json(s)) - .transpose()? - .and_then(|(waypoint, _)| waypoint) - .map(|w| w.get_pos()); + let (char_waypoint, _char_map_marker) = convert_waypoint_or_warn( + character_data.waypoint.as_deref(), + CharacterId(character_data.character_id), + ); + let location = char_waypoint.map(|w| w.get_pos()); Ok(CharacterItem { character: char, From 3eaac9be7a82ed1c604951012400fe49b19cb555 Mon Sep 17 00:00:00 2001 From: Imbris Date: Thu, 30 Jul 2026 21:49:20 -0400 Subject: [PATCH 3/3] Make running rustfmt directly work (needed for format on save in my editor with new 2024 syntax) --- .rustfmt.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.rustfmt.toml b/.rustfmt.toml index 8be9cf36fe..948cb6877e 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,5 +1,6 @@ -hard_tabs = false +edition = "2024" style_edition = "2024" +hard_tabs = false format_code_in_doc_comments = true format_strings = true