Merge branch 'imbris/character-location' into 'master'

Use `Content` for character location names

See merge request veloren/veloren!5412
This commit is contained in:
Imbris 2026-08-01 14:34:46 +00:00
commit c424a2a8fe
12 changed files with 102 additions and 74 deletions

View file

@ -1,5 +1,6 @@
hard_tabs = false
edition = "2024"
style_edition = "2024"
hard_tabs = false
format_code_in_doc_comments = true
format_strings = true

View file

@ -323,7 +323,7 @@ pub struct Client {
pending_invites: HashSet<Uid>,
// The pending trade the client is involved in, and it's id
pending_trade: Option<(TradeId, PendingTrade, Option<SitePrices>)>,
waypoint: Option<String>,
waypoint: Option<Content>,
network: Option<Network>,
participant: Option<Participant>,
@ -367,7 +367,7 @@ pub struct Client {
/// additional state to handle UI.
#[derive(Debug, Default)]
pub struct CharacterList {
pub characters: Vec<CharacterItem>,
pub characters: Vec<CharacterItem<Content>>,
pub loading: bool,
}
@ -2315,7 +2315,7 @@ impl Client {
}))
}
pub fn waypoint(&self) -> &Option<String> { &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));

View file

@ -147,7 +147,7 @@ pub enum ServerGeneral {
/// Result of loading character data
CharacterDataLoadResult(Result<UpdateCharacterMetadata, String>),
/// A list of characters belonging to the a authenticated player was sent
CharacterListUpdate(Vec<CharacterItem>),
CharacterListUpdate(Vec<CharacterItem<Content>>),
/// 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)]

View file

@ -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<Location> {
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<String>,
pub location: Option<Location>,
}

View file

@ -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 }),

View file

@ -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_::<i32>())
});
c.location = name;
});
/// Converts positions to location names for a list of characters.
fn get_location_names(
&self,
character_list: Vec<CharacterItem<Vec3<f32>>>,
) -> Vec<CharacterItem<Content>> {
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_::<i32>())
})
.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,

View file

@ -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::{
@ -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";
@ -124,6 +122,24 @@ pub fn load_items(connection: &Connection, root: i64) -> Result<Vec<Item>, Persi
Ok(items)
}
fn convert_waypoint_or_warn(
waypoint_json: Option<&str>,
char_id: CharacterId,
) -> (Option<Waypoint>, Option<MapMarker>) {
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
@ -175,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(
"
@ -395,13 +397,19 @@ pub fn load_character_list(player_uuid_: &str, connection: &Connection) -> Chara
let (recipe_book, _) = convert_recipe_book_from_database_items(&recipe_book_items)?;
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,
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()

View file

@ -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<Vec<CharacterItem>, PersistenceError>;
pub(crate) type CharacterCreationResult =
Result<(CharacterId, Vec<CharacterItem>), PersistenceError>;
pub(crate) type CharacterEditResult = Result<(CharacterId, Vec<CharacterItem>), PersistenceError>;
type CharacterList = Vec<CharacterItem<Vec3<f32>>>;
pub(crate) type CharacterListResult = Result<CharacterList, PersistenceError>;
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);

View file

@ -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 {

View file

@ -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 },
));

View file

@ -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),

View file

@ -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<Event>, characters: &[CharacterItem]) {
fn update(
&mut self,
message: Message,
events: &mut Vec<Event>,
characters: &[CharacterItem<Content>],
) {
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<Content>],
) -> 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<Content>],
) -> Option<(comp::Body, &'a Inventory)> {
self.controls.display_body_inventory(characters)
}