Overhauled chat message representation to allow for more exhaustive localisation

This commit is contained in:
Joshua Barretto 2023-04-11 15:46:36 +01:00
parent bc4d1a71f6
commit edcc2f1870
19 changed files with 279 additions and 220 deletions

1
Cargo.lock generated
View File

@ -7173,6 +7173,7 @@ dependencies = [
name = "veloren-voxygen-i18n-helpers" name = "veloren-voxygen-i18n-helpers"
version = "0.10.0" version = "0.10.0"
dependencies = [ dependencies = [
"hashbrown 0.12.3",
"tracing", "tracing",
"veloren-client-i18n", "veloren-client-i18n",
"veloren-common", "veloren-common",

View File

@ -2280,13 +2280,15 @@ impl Client {
.any(|r| !matches!(r, group::Role::Pet)) .any(|r| !matches!(r, group::Role::Pet))
{ {
frontend_events frontend_events
.push(Event::Chat(comp::ChatType::Meta.chat_msg( // TODO: localise
.push(Event::Chat(comp::ChatType::Meta.into_plain_msg(
"Type /g or /group to chat with your group members", "Type /g or /group to chat with your group members",
))); )));
} }
if let Some(player_info) = self.player_list.get(&uid) { if let Some(player_info) = self.player_list.get(&uid) {
frontend_events.push(Event::Chat( frontend_events.push(Event::Chat(
comp::ChatType::GroupMeta("Group".into()).chat_msg(format!( // TODO: localise
comp::ChatType::GroupMeta("Group".into()).into_plain_msg(format!(
"[{}] joined group", "[{}] joined group",
self.personalize_alias(uid, player_info.player_alias.clone()) self.personalize_alias(uid, player_info.player_alias.clone())
)), )),
@ -2303,7 +2305,8 @@ impl Client {
Removed(uid) => { Removed(uid) => {
if let Some(player_info) = self.player_list.get(&uid) { if let Some(player_info) = self.player_list.get(&uid) {
frontend_events.push(Event::Chat( frontend_events.push(Event::Chat(
comp::ChatType::GroupMeta("Group".into()).chat_msg(format!( // TODO: localise
comp::ChatType::GroupMeta("Group".into()).into_plain_msg(format!(
"[{}] left group", "[{}] left group",
self.personalize_alias(uid, player_info.player_alias.clone()) self.personalize_alias(uid, player_info.player_alias.clone())
)), )),
@ -2790,20 +2793,20 @@ impl Client {
KillSource::Other => (), KillSource::Other => (),
}; };
}, },
comp::ChatType::Tell(from, to) | comp::ChatType::NpcTell(from, to, _) => { comp::ChatType::Tell(from, to) | comp::ChatType::NpcTell(from, to) => {
alias_of_uid(from); alias_of_uid(from);
alias_of_uid(to); alias_of_uid(to);
}, },
comp::ChatType::Say(uid) comp::ChatType::Say(uid)
| comp::ChatType::Region(uid) | comp::ChatType::Region(uid)
| comp::ChatType::World(uid) | comp::ChatType::World(uid)
| comp::ChatType::NpcSay(uid, _) => { | comp::ChatType::NpcSay(uid) => {
alias_of_uid(uid); alias_of_uid(uid);
}, },
comp::ChatType::Group(uid, _) | comp::ChatType::Faction(uid, _) => { comp::ChatType::Group(uid, _) | comp::ChatType::Faction(uid, _) => {
alias_of_uid(uid); alias_of_uid(uid);
}, },
comp::ChatType::Npc(uid, _) => alias_of_uid(uid), comp::ChatType::Npc(uid) => alias_of_uid(uid),
comp::ChatType::Meta => (), comp::ChatType::Meta => (),
}; };
result result

View File

@ -6,7 +6,7 @@ use crate::sync;
use common::{ use common::{
calendar::Calendar, calendar::Calendar,
character::{self, CharacterItem}, character::{self, CharacterItem},
comp::{self, invite::InviteKind, item::MaterialStatManifest}, comp::{self, invite::InviteKind, item::MaterialStatManifest, Content},
event::UpdateCharacterMetadata, event::UpdateCharacterMetadata,
lod, lod,
outcome::Outcome, outcome::Outcome,
@ -215,11 +215,10 @@ pub enum ServerGeneral {
} }
impl ServerGeneral { impl ServerGeneral {
pub fn server_msg<S>(chat_type: comp::ChatType<String>, msg: S) -> Self // TODO: Don't use `Into<Content>` since this treats all strings as plaintext,
where // properly localise server messages
S: Into<String>, pub fn server_msg(chat_type: comp::ChatType<String>, content: impl Into<Content>) -> Self {
{ ServerGeneral::ChatMsg(chat_type.into_msg(content.into()))
ServerGeneral::ChatMsg(chat_type.chat_msg(msg))
} }
} }

View File

@ -2,6 +2,7 @@ use crate::{
comp::{group::Group, BuffKind}, comp::{group::Group, BuffKind},
uid::Uid, uid::Uid,
}; };
use hashbrown::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specs::{Component, DenseVecStorage}; use specs::{Component, DenseVecStorage};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -29,8 +30,8 @@ impl Component for ChatMode {
} }
impl ChatMode { impl ChatMode {
/// Create a message from your current chat mode and uuid. /// Create a plain message from your current chat mode and uuid.
pub fn new_message(&self, from: Uid, message: String) -> UnresolvedChatMsg { pub fn to_plain_msg(&self, from: Uid, text: impl ToString) -> UnresolvedChatMsg {
let chat_type = match self { let chat_type = match self {
ChatMode::Tell(to) => ChatType::Tell(from, *to), ChatMode::Tell(to) => ChatType::Tell(from, *to),
ChatMode::Say => ChatType::Say(from), ChatMode::Say => ChatType::Say(from),
@ -39,7 +40,10 @@ impl ChatMode {
ChatMode::Faction(faction) => ChatType::Faction(from, faction.clone()), ChatMode::Faction(faction) => ChatType::Faction(from, faction.clone()),
ChatMode::World => ChatType::World(from), ChatMode::World => ChatType::World(from),
}; };
UnresolvedChatMsg { chat_type, message } UnresolvedChatMsg {
chat_type,
content: Content::Plain(text.to_string()),
}
} }
} }
@ -102,26 +106,28 @@ pub enum ChatType<G> {
/// World chat /// World chat
World(Uid), World(Uid),
/// Messages sent from NPCs (Not shown in chat but as speech bubbles) /// Messages sent from NPCs (Not shown in chat but as speech bubbles)
/// Npc(Uid),
/// The u16 field is a random number for selecting localization variants.
Npc(Uid, u16),
/// From NPCs but in the chat for clients in the near vicinity /// From NPCs but in the chat for clients in the near vicinity
NpcSay(Uid, u16), NpcSay(Uid),
/// From NPCs but in the chat for a specific client. Shows a chat bubble. /// From NPCs but in the chat for a specific client. Shows a chat bubble.
/// (from, to, localization variant) /// (from, to, localization variant)
NpcTell(Uid, Uid, u16), NpcTell(Uid, Uid),
/// Anything else /// Anything else
Meta, Meta,
} }
impl<G> ChatType<G> { impl<G> ChatType<G> {
pub fn chat_msg<S>(self, msg: S) -> GenericChatMsg<G> pub fn into_plain_msg(self, text: impl ToString) -> GenericChatMsg<G> {
where
S: Into<String>,
{
GenericChatMsg { GenericChatMsg {
chat_type: self, chat_type: self,
message: msg.into(), content: Content::Plain(text.to_string()),
}
}
pub fn into_msg(self, content: Content) -> GenericChatMsg<G> {
GenericChatMsg {
chat_type: self,
content,
} }
} }
@ -140,9 +146,9 @@ impl<G> ChatType<G> {
ChatType::Faction(u, _s) => Some(*u), ChatType::Faction(u, _s) => Some(*u),
ChatType::Region(u) => Some(*u), ChatType::Region(u) => Some(*u),
ChatType::World(u) => Some(*u), ChatType::World(u) => Some(*u),
ChatType::Npc(u, _r) => Some(*u), ChatType::Npc(u) => Some(*u),
ChatType::NpcSay(u, _r) => Some(*u), ChatType::NpcSay(u) => Some(*u),
ChatType::NpcTell(u, _t, _r) => Some(*u), ChatType::NpcTell(u, _t) => Some(*u),
ChatType::Meta => None, ChatType::Meta => None,
} }
} }
@ -156,9 +162,9 @@ impl<G> ChatType<G> {
| ChatType::CommandError | ChatType::CommandError
| ChatType::FactionMeta(_) | ChatType::FactionMeta(_)
| ChatType::GroupMeta(_) | ChatType::GroupMeta(_)
| ChatType::Npc(_, _) | ChatType::Npc(_)
| ChatType::NpcSay(_, _) | ChatType::NpcSay(_)
| ChatType::NpcTell(_, _, _) | ChatType::NpcTell(_, _)
| ChatType::Meta | ChatType::Meta
| ChatType::Kill(_, _) => None, | ChatType::Kill(_, _) => None,
ChatType::Tell(_, _) | ChatType::Group(_, _) | ChatType::Faction(_, _) => Some(true), ChatType::Tell(_, _) | ChatType::Group(_, _) | ChatType::Faction(_, _) => Some(true),
@ -167,11 +173,87 @@ impl<G> ChatType<G> {
} }
} }
/// The content of a chat message.
// TODO: This could be generalised to *any* in-game text, not just chat messages (hence it not being
// called `ChatContent`). A few examples:
//
// - Signposts, both those appearing as overhead messages and those displayed 'in-world' on a shop
// sign
// - UI elements
// - In-game notes/books (we could add a variant that allows structuring complex, novel textual
// information as a syntax tree or some other intermediate format that can be localised by the
// client)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Content {
/// The content is a plaintext string that should be shown to the user
/// verbatim.
Plain(String),
/// The content is a localizable message with the given arguments.
Localized {
/// i18n key
key: String,
/// Pseudorandom seed value that allows frontends to select a
/// deterministic (but pseudorandom) localised output
seed: u16,
/// i18n arguments
args: HashMap<String, String>,
},
}
impl From<String> for Content {
fn from(text: String) -> Self { Self::Plain(text) }
}
impl<'a> From<&'a str> for Content {
fn from(text: &'a str) -> Self { Self::Plain(text.to_string()) }
}
impl Content {
pub fn localized(key: impl ToString) -> Self {
Self::Localized {
key: key.to_string(),
r: rand::random(),
args: HashMap::default(),
}
}
pub fn localized_with_args<'a, S: ToString>(
key: impl ToString,
args: impl IntoIterator<Item = (&'a str, S)>,
) -> Self {
Self::Localized {
key: key.to_string(),
r: rand::random(),
args: args
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
}
}
pub fn as_plain(&self) -> Option<&str> {
match self {
Self::Plain(text) => Some(text.as_str()),
Self::Localized { .. } => None,
}
}
pub fn localize<F>(&self, i18n_variation: F) -> String
where
F: Fn(&str, u16, &HashMap<String, String>) -> String,
{
match self {
Content::Plain(text) => text.to_string(),
Content::Localized { key, seed, args } => i18n_variation(key, *seed, args),
}
}
}
// Stores chat text, type // Stores chat text, type
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenericChatMsg<G> { pub struct GenericChatMsg<G> {
pub chat_type: ChatType<G>, pub chat_type: ChatType<G>,
pub message: String, content: Content,
} }
pub type ChatMsg = GenericChatMsg<String>; pub type ChatMsg = GenericChatMsg<String>;
@ -183,19 +265,19 @@ impl<G> GenericChatMsg<G> {
pub const REGION_DISTANCE: f32 = 1000.0; pub const REGION_DISTANCE: f32 = 1000.0;
pub const SAY_DISTANCE: f32 = 100.0; pub const SAY_DISTANCE: f32 = 100.0;
pub fn npc(uid: Uid, message: String) -> Self { pub fn npc(uid: Uid, content: Content) -> Self {
let chat_type = ChatType::Npc(uid, rand::random()); let chat_type = ChatType::Npc(uid);
Self { chat_type, message } Self { chat_type, content }
} }
pub fn npc_say(uid: Uid, message: String) -> Self { pub fn npc_say(uid: Uid, content: Content) -> Self {
let chat_type = ChatType::NpcSay(uid, rand::random()); let chat_type = ChatType::NpcSay(uid);
Self { chat_type, message } Self { chat_type, content }
} }
pub fn npc_tell(from: Uid, to: Uid, message: String) -> Self { pub fn npc_tell(from: Uid, to: Uid, content: Content) -> Self {
let chat_type = ChatType::NpcTell(from, to, rand::random()); let chat_type = ChatType::NpcTell(from, to);
Self { chat_type, message } Self { chat_type, content }
} }
pub fn map_group<T>(self, mut f: impl FnMut(G) -> T) -> GenericChatMsg<T> { pub fn map_group<T>(self, mut f: impl FnMut(G) -> T) -> GenericChatMsg<T> {
@ -213,15 +295,15 @@ impl<G> GenericChatMsg<G> {
ChatType::Faction(a, b) => ChatType::Faction(a, b), ChatType::Faction(a, b) => ChatType::Faction(a, b),
ChatType::Region(a) => ChatType::Region(a), ChatType::Region(a) => ChatType::Region(a),
ChatType::World(a) => ChatType::World(a), ChatType::World(a) => ChatType::World(a),
ChatType::Npc(a, b) => ChatType::Npc(a, b), ChatType::Npc(a) => ChatType::Npc(a),
ChatType::NpcSay(a, b) => ChatType::NpcSay(a, b), ChatType::NpcSay(a) => ChatType::NpcSay(a),
ChatType::NpcTell(a, b, c) => ChatType::NpcTell(a, b, c), ChatType::NpcTell(a, b) => ChatType::NpcTell(a, b),
ChatType::Meta => ChatType::Meta, ChatType::Meta => ChatType::Meta,
}; };
GenericChatMsg { GenericChatMsg {
chat_type, chat_type,
message: self.message, content: self.content,
} }
} }
@ -234,15 +316,8 @@ impl<G> GenericChatMsg<G> {
} }
pub fn to_bubble(&self) -> Option<(SpeechBubble, Uid)> { pub fn to_bubble(&self) -> Option<(SpeechBubble, Uid)> {
let icon = self.icon(); self.uid()
if let ChatType::Npc(from, r) | ChatType::NpcSay(from, r) | ChatType::NpcTell(from, _, r) = .map(|from| (SpeechBubble::new(self.content.clone(), self.icon()), from))
self.chat_type
{
Some((SpeechBubble::npc_new(&self.message, r, icon), from))
} else {
self.uid()
.map(|from| (SpeechBubble::player_new(&self.message, icon), from))
}
} }
pub fn icon(&self) -> SpeechBubbleType { pub fn icon(&self) -> SpeechBubbleType {
@ -260,14 +335,18 @@ impl<G> GenericChatMsg<G> {
ChatType::Faction(_u, _s) => SpeechBubbleType::Faction, ChatType::Faction(_u, _s) => SpeechBubbleType::Faction,
ChatType::Region(_u) => SpeechBubbleType::Region, ChatType::Region(_u) => SpeechBubbleType::Region,
ChatType::World(_u) => SpeechBubbleType::World, ChatType::World(_u) => SpeechBubbleType::World,
ChatType::Npc(_u, _r) => SpeechBubbleType::None, ChatType::Npc(_u) => SpeechBubbleType::None,
ChatType::NpcSay(_u, _r) => SpeechBubbleType::Say, ChatType::NpcSay(_u) => SpeechBubbleType::Say,
ChatType::NpcTell(_f, _t, _) => SpeechBubbleType::Say, ChatType::NpcTell(_f, _t) => SpeechBubbleType::Say,
ChatType::Meta => SpeechBubbleType::None, ChatType::Meta => SpeechBubbleType::None,
} }
} }
pub fn uid(&self) -> Option<Uid> { self.chat_type.uid() } pub fn uid(&self) -> Option<Uid> { self.chat_type.uid() }
pub fn content(&self) -> &Content { &self.content }
pub fn set_content(&mut self, content: Content) { self.content = content; }
} }
/// Player factions are used to coordinate pvp vs hostile factions or segment /// Player factions are used to coordinate pvp vs hostile factions or segment
@ -283,15 +362,6 @@ impl From<String> for Faction {
fn from(s: String) -> Self { Faction(s) } fn from(s: String) -> Self { Faction(s) }
} }
/// The contents of a speech bubble
pub enum SpeechBubbleMessage {
/// This message was said by a player and needs no translation
Plain(String),
/// This message was said by an NPC. The fields are a i18n key and a random
/// u16 index
Localized(String, u16),
}
/// List of chat types for players and NPCs. Each one has its own icon. /// List of chat types for players and NPCs. Each one has its own icon.
/// ///
/// This is a subset of `ChatType`, and a superset of `ChatMode` /// This is a subset of `ChatType`, and a superset of `ChatMode`
@ -311,7 +381,7 @@ pub enum SpeechBubbleType {
/// Adds a speech bubble above the character /// Adds a speech bubble above the character
pub struct SpeechBubble { pub struct SpeechBubble {
pub message: SpeechBubbleMessage, pub content: Content,
pub icon: SpeechBubbleType, pub icon: SpeechBubbleType,
pub timeout: Instant, pub timeout: Instant,
} }
@ -320,33 +390,14 @@ impl SpeechBubble {
/// Default duration in seconds of speech bubbles /// Default duration in seconds of speech bubbles
pub const DEFAULT_DURATION: f64 = 5.0; pub const DEFAULT_DURATION: f64 = 5.0;
pub fn npc_new(i18n_key: &str, r: u16, icon: SpeechBubbleType) -> Self { pub fn new(content: Content, icon: SpeechBubbleType) -> Self {
let message = SpeechBubbleMessage::Localized(i18n_key.to_string(), r);
let timeout = Instant::now() + Duration::from_secs_f64(SpeechBubble::DEFAULT_DURATION); let timeout = Instant::now() + Duration::from_secs_f64(SpeechBubble::DEFAULT_DURATION);
Self { Self {
message, content,
icon, icon,
timeout, timeout,
} }
} }
pub fn player_new(message: &str, icon: SpeechBubbleType) -> Self { pub fn content(&self) -> &Content { &self.content }
let message = SpeechBubbleMessage::Plain(message.to_string());
let timeout = Instant::now() + Duration::from_secs_f64(SpeechBubble::DEFAULT_DURATION);
Self {
message,
icon,
timeout,
}
}
pub fn message<F>(&self, i18n_variation: F) -> String
where
F: Fn(&str, u16) -> String,
{
match &self.message {
SpeechBubbleMessage::Plain(m) => m.to_string(),
SpeechBubbleMessage::Localized(k, i) => i18n_variation(k, *i),
}
}
} }

View File

@ -75,7 +75,8 @@ pub use self::{
}, },
character_state::{CharacterActivity, CharacterState, StateUpdate}, character_state::{CharacterActivity, CharacterState, StateUpdate},
chat::{ chat::{
ChatMode, ChatMsg, ChatType, Faction, SpeechBubble, SpeechBubbleType, UnresolvedChatMsg, ChatMode, ChatMsg, ChatType, Content, Faction, SpeechBubble, SpeechBubbleType,
UnresolvedChatMsg,
}, },
combo::Combo, combo::Combo,
controller::{ controller::{

View File

@ -88,7 +88,7 @@ fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
); );
Some((npc_id, site_id)) Some((npc_id, site_id))
} else { } else {
warn!("No site found for respawning humaniod"); warn!("No site found for respawning humanoid");
None None
} }
}, },

View File

@ -22,7 +22,7 @@ use common::{
}, },
item_drop, item_drop,
projectile::ProjectileConstructor, projectile::ProjectileConstructor,
Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, Agent, Alignment, Body, CharacterState, Content, ControlAction, ControlEvent, Controller,
HealthChange, InputKind, InventoryAction, Pos, Scale, UnresolvedChatMsg, UtteranceKind, HealthChange, InputKind, InventoryAction, Pos, Scale, UnresolvedChatMsg, UtteranceKind,
}, },
effect::{BuffEffect, Effect}, effect::{BuffEffect, Effect},
@ -1529,10 +1529,10 @@ impl<'a> AgentData<'a> {
} }
} }
pub fn chat_npc(&self, msg: impl ToString, event_emitter: &mut Emitter<'_, ServerEvent>) { pub fn chat_npc(&self, key: impl ToString, event_emitter: &mut Emitter<'_, ServerEvent>) {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid, *self.uid,
msg.to_string(), Content::localized(key),
))); )));
} }

View File

@ -2745,7 +2745,7 @@ fn handle_tell(
} else { } else {
message_opt.join(" ") message_opt.join(" ")
}; };
server.state.send_chat(mode.new_message(target_uid, msg)); server.state.send_chat(mode.to_plain_msg(target_uid, msg));
server.notify_client(target, ServerGeneral::ChatMode(mode)); server.notify_client(target, ServerGeneral::ChatMode(mode));
Ok(()) Ok(())
} else { } else {
@ -2770,7 +2770,7 @@ fn handle_faction(
let msg = args.join(" "); let msg = args.join(" ");
if !msg.is_empty() { if !msg.is_empty() {
if let Some(uid) = server.state.ecs().read_storage().get(target) { if let Some(uid) = server.state.ecs().read_storage().get(target) {
server.state.send_chat(mode.new_message(*uid, msg)); server.state.send_chat(mode.to_plain_msg(*uid, msg));
} }
} }
server.notify_client(target, ServerGeneral::ChatMode(mode)); server.notify_client(target, ServerGeneral::ChatMode(mode));
@ -2797,7 +2797,7 @@ fn handle_group(
let msg = args.join(" "); let msg = args.join(" ");
if !msg.is_empty() { if !msg.is_empty() {
if let Some(uid) = server.state.ecs().read_storage().get(target) { if let Some(uid) = server.state.ecs().read_storage().get(target) {
server.state.send_chat(mode.new_message(*uid, msg)); server.state.send_chat(mode.to_plain_msg(*uid, msg));
} }
} }
server.notify_client(target, ServerGeneral::ChatMode(mode)); server.notify_client(target, ServerGeneral::ChatMode(mode));
@ -2921,7 +2921,7 @@ fn handle_region(
let msg = args.join(" "); let msg = args.join(" ");
if !msg.is_empty() { if !msg.is_empty() {
if let Some(uid) = server.state.ecs().read_storage().get(target) { if let Some(uid) = server.state.ecs().read_storage().get(target) {
server.state.send_chat(mode.new_message(*uid, msg)); server.state.send_chat(mode.to_plain_msg(*uid, msg));
} }
} }
server.notify_client(target, ServerGeneral::ChatMode(mode)); server.notify_client(target, ServerGeneral::ChatMode(mode));
@ -2942,7 +2942,7 @@ fn handle_say(
let msg = args.join(" "); let msg = args.join(" ");
if !msg.is_empty() { if !msg.is_empty() {
if let Some(uid) = server.state.ecs().read_storage().get(target) { if let Some(uid) = server.state.ecs().read_storage().get(target) {
server.state.send_chat(mode.new_message(*uid, msg)); server.state.send_chat(mode.to_plain_msg(*uid, msg));
} }
} }
server.notify_client(target, ServerGeneral::ChatMode(mode)); server.notify_client(target, ServerGeneral::ChatMode(mode));
@ -2963,7 +2963,7 @@ fn handle_world(
let msg = args.join(" "); let msg = args.join(" ");
if !msg.is_empty() { if !msg.is_empty() {
if let Some(uid) = server.state.ecs().read_storage().get(target) { if let Some(uid) = server.state.ecs().read_storage().get(target) {
server.state.send_chat(mode.new_message(*uid, msg)); server.state.send_chat(mode.to_plain_msg(*uid, msg));
} }
} }
server.notify_client(target, ServerGeneral::ChatMode(mode)); server.notify_client(target, ServerGeneral::ChatMode(mode));
@ -2992,8 +2992,9 @@ fn handle_join_faction(
.flatten() .flatten()
.map(|f| f.0); .map(|f| f.0);
server.state.send_chat( server.state.send_chat(
// TODO: Localise
ChatType::FactionMeta(faction.clone()) ChatType::FactionMeta(faction.clone())
.chat_msg(format!("[{}] joined faction ({})", alias, faction)), .into_plain_msg(format!("[{}] joined faction ({})", alias, faction)),
); );
(faction_join, mode) (faction_join, mode)
} else { } else {
@ -3009,8 +3010,9 @@ fn handle_join_faction(
}; };
if let Some(faction) = faction_leave { if let Some(faction) = faction_leave {
server.state.send_chat( server.state.send_chat(
// TODO: Localise
ChatType::FactionMeta(faction.clone()) ChatType::FactionMeta(faction.clone())
.chat_msg(format!("[{}] left faction ({})", alias, faction)), .into_plain_msg(format!("[{}] left faction ({})", alias, faction)),
); );
} }
server.notify_client(target, ServerGeneral::ChatMode(mode)); server.notify_client(target, ServerGeneral::ChatMode(mode));

View File

@ -35,7 +35,6 @@ use common::{
}; };
use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
use common_state::BlockChange; use common_state::BlockChange;
use comp::chat::GenericChatMsg;
use hashbrown::HashSet; use hashbrown::HashSet;
use rand::{distributions::WeightedIndex, Rng}; use rand::{distributions::WeightedIndex, Rng};
use rand_distr::Distribution; use rand_distr::Distribution;
@ -198,10 +197,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
_ => KillSource::Other, _ => KillSource::Other,
}; };
state.send_chat(GenericChatMsg { state.send_chat(comp::ChatType::Kill(kill_source, *uid).into_plain_msg(""));
chat_type: comp::ChatType::Kill(kill_source, *uid),
message: "".to_string(),
});
} }
} }

View File

@ -797,7 +797,11 @@ impl StateExt for State {
(*ecs.read_resource::<UidAllocator>()) (*ecs.read_resource::<UidAllocator>())
.retrieve_entity_internal(sender.0) .retrieve_entity_internal(sender.0)
.map_or(false, |e| { .map_or(false, |e| {
self.validate_chat_msg(e, &msg.chat_type, &msg.message) self.validate_chat_msg(
e,
&msg.chat_type,
msg.content().as_plain().unwrap_or_default(),
)
}) })
}) { }) {
match &msg.chat_type { match &msg.chat_type {
@ -827,7 +831,6 @@ impl StateExt for State {
} }
}, },
comp::ChatType::Kill(kill_source, uid) => { comp::ChatType::Kill(kill_source, uid) => {
let comp::chat::GenericChatMsg { message, .. } = msg;
let clients = ecs.read_storage::<Client>(); let clients = ecs.read_storage::<Client>();
let clients_count = clients.count(); let clients_count = clients.count();
// Avoid chat spam, send kill message only to group or nearby players if a // Avoid chat spam, send kill message only to group or nearby players if a
@ -870,7 +873,7 @@ impl StateExt for State {
} else { } else {
self.notify_players(ServerGeneral::server_msg( self.notify_players(ServerGeneral::server_msg(
comp::ChatType::Kill(kill_source.clone(), *uid), comp::ChatType::Kill(kill_source.clone(), *uid),
message, msg.content().clone(),
)) ))
} }
}, },
@ -900,7 +903,7 @@ impl StateExt for State {
} }
} }
}, },
comp::ChatType::Npc(uid, _r) => { comp::ChatType::Npc(uid) => {
let entity_opt = let entity_opt =
(*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0); (*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0);
@ -913,7 +916,7 @@ impl StateExt for State {
} }
} }
}, },
comp::ChatType::NpcSay(uid, _r) => { comp::ChatType::NpcSay(uid) => {
let entity_opt = let entity_opt =
(*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0); (*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0);
@ -926,7 +929,7 @@ impl StateExt for State {
} }
} }
}, },
comp::ChatType::NpcTell(from, to, _r) => { comp::ChatType::NpcTell(from, to) => {
for (client, uid) in for (client, uid) in
(&ecs.read_storage::<Client>(), &ecs.read_storage::<Uid>()).join() (&ecs.read_storage::<Client>(), &ecs.read_storage::<Uid>()).join()
{ {
@ -950,12 +953,10 @@ impl StateExt for State {
comp::ChatType::Group(from, g) => { comp::ChatType::Group(from, g) => {
if group_info.is_none() { if group_info.is_none() {
// group not found, reply with command error // group not found, reply with command error
let reply = comp::ChatMsg { let reply = comp::ChatType::CommandError.into_plain_msg(
chat_type: comp::ChatType::CommandError, "You are using group chat but do not belong to a group. Use /world or \
message: "You are using group chat but do not belong to a group. Use \ /region to change chat.",
/world or /region to change chat." );
.into(),
};
if let Some((client, _)) = if let Some((client, _)) =
(&ecs.read_storage::<Client>(), &ecs.read_storage::<Uid>()) (&ecs.read_storage::<Client>(), &ecs.read_storage::<Uid>())

View File

@ -6,7 +6,8 @@ use common::{
inventory::item::{ItemTag, MaterialStatManifest}, inventory::item::{ItemTag, MaterialStatManifest},
invite::{InviteKind, InviteResponse}, invite::{InviteKind, InviteResponse},
tool::AbilityMap, tool::AbilityMap,
BehaviorState, ControlAction, Item, TradingBehavior, UnresolvedChatMsg, UtteranceKind, BehaviorState, Content, ControlAction, Item, TradingBehavior, UnresolvedChatMsg,
UtteranceKind,
}, },
event::ServerEvent, event::ServerEvent,
rtsim::PersonalityTrait, rtsim::PersonalityTrait,
@ -434,7 +435,7 @@ pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool {
let (tradeid, pending, prices, inventories) = *boxval; let (tradeid, pending, prices, inventories) = *boxval;
if agent.behavior.is(BehaviorState::TRADING) { if agent.behavior.is(BehaviorState::TRADING) {
let who = usize::from(!agent.behavior.is(BehaviorState::TRADING_ISSUER)); let who = usize::from(!agent.behavior.is(BehaviorState::TRADING_ISSUER));
let mut message = |msg| { let mut message = |text| {
if let Some(with) = agent if let Some(with) = agent
.target .target
.as_ref() .as_ref()
@ -443,12 +444,14 @@ pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_tell( event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_tell(
*agent_data.uid, *agent_data.uid,
*with, *with,
msg, // TODO: localise this
Content::Plain(text),
))); )));
} else { } else {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say(
*agent_data.uid, *agent_data.uid,
msg, // TODO: localise this
Content::Plain(text),
))); )));
} }
}; };

View File

@ -11,7 +11,7 @@ use crate::{
EditableSettings, EditableSettings,
}; };
use common::{ use common::{
comp::{Admin, AdminRole, ChatType, Player, Presence, UnresolvedChatMsg, Waypoint}, comp::{Admin, AdminRole, ChatType, Player, Presence, Waypoint},
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
resources::Time, resources::Time,
terrain::TerrainChunkSize, terrain::TerrainChunkSize,
@ -48,7 +48,7 @@ impl Sys {
if !editable_settings.server_description.is_empty() { if !editable_settings.server_description.is_empty() {
client.send(ServerGeneral::server_msg( client.send(ServerGeneral::server_msg(
ChatType::CommandInfo, ChatType::CommandInfo,
&*editable_settings.server_description, editable_settings.server_description.as_str(),
))?; ))?;
} }
@ -62,10 +62,9 @@ impl Sys {
if !client.login_msg_sent.load(Ordering::Relaxed) { if !client.login_msg_sent.load(Ordering::Relaxed) {
if let Some(player_uid) = uids.get(entity) { if let Some(player_uid) = uids.get(entity) {
server_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg { server_emitter.emit(ServerEvent::Chat(
chat_type: ChatType::Online(*player_uid), ChatType::Online(*player_uid).into_plain_msg(""),
message: "".to_string(), ));
}));
client.login_msg_sent.store(true, Ordering::Relaxed); client.login_msg_sent.store(true, Ordering::Relaxed);
} }

View File

@ -28,7 +28,7 @@ impl Sys {
const CHAT_MODE_DEFAULT: &ChatMode = &ChatMode::default(); const CHAT_MODE_DEFAULT: &ChatMode = &ChatMode::default();
let mode = chat_modes.get(entity).unwrap_or(CHAT_MODE_DEFAULT); let mode = chat_modes.get(entity).unwrap_or(CHAT_MODE_DEFAULT);
// Send chat message // Send chat message
server_emitter.emit(ServerEvent::Chat(mode.new_message(*from, message))); server_emitter.emit(ServerEvent::Chat(mode.to_plain_msg(*from, message)));
} else { } else {
error!("Could not send message. Missing player uid"); error!("Could not send message. Missing player uid");
} }

View File

@ -11,3 +11,4 @@ common = {package = "veloren-common", path = "../../common"}
i18n = {package = "veloren-client-i18n", path = "../../client/i18n"} i18n = {package = "veloren-client-i18n", path = "../../client/i18n"}
# Utility # Utility
tracing = "0.1" tracing = "0.1"
hashbrown = { version = "0.12" }

View File

@ -1,19 +1,32 @@
#![feature(let_chains)] #![feature(let_chains)]
use common::comp::{ use common::comp::{
chat::{KillSource, KillType}, chat::{KillSource, KillType},
BuffKind, ChatMsg, ChatType, BuffKind, ChatMsg, ChatType, Content,
}; };
use common_net::msg::{ChatTypeContext, PlayerInfo}; use common_net::msg::{ChatTypeContext, PlayerInfo};
use hashbrown::HashMap;
use i18n::Localization; use i18n::Localization;
pub fn make_localizer(
localisation: &Localization,
) -> impl Fn(&str, u16, &HashMap<String, String>) -> String + Copy + '_ {
move |key: &str, seed: u16, args: &HashMap<String, String>| {
localisation
.get_variation_ctx(key, seed, &args.iter().collect())
.into_owned()
}
}
pub fn localize_chat_message( pub fn localize_chat_message(
mut msg: ChatMsg, msg: ChatMsg,
lookup_fn: impl Fn(&ChatMsg) -> ChatTypeContext, lookup_fn: impl Fn(&ChatMsg) -> ChatTypeContext,
localisation: &Localization, localisation: &Localization,
show_char_name: bool, show_char_name: bool,
) -> ChatMsg { ) -> (ChatType<String>, String) {
let info = lookup_fn(&msg); let info = lookup_fn(&msg);
let localizer = make_localizer(localisation);
let name_format = |uid: &common::uid::Uid| match info.player_alias.get(uid).cloned() { let name_format = |uid: &common::uid::Uid| match info.player_alias.get(uid).cloned() {
Some(pi) => insert_alias(info.you == *uid, pi, localisation), Some(pi) => insert_alias(info.you == *uid, pi, localisation),
None => info None => info
@ -23,13 +36,14 @@ pub fn localize_chat_message(
.expect("client didn't proved enough info"), .expect("client didn't proved enough info"),
}; };
let message_format = |from: &common::uid::Uid, message: &str, group: Option<&String>| { let message_format = |from: &common::uid::Uid, content: &Content, group: Option<&String>| {
let alias = name_format(from); let alias = name_format(from);
let name = if let Some(pi) = info.player_alias.get(from).cloned() && show_char_name { let name = if let Some(pi) = info.player_alias.get(from).cloned() && show_char_name {
pi.character.map(|c| c.name) pi.character.map(|c| c.name)
} else { } else {
None None
}; };
let message = content.localize(localizer);
match (group, name) { match (group, name) {
(Some(group), None) => format!("({group}) [{alias}]: {message}"), (Some(group), None) => format!("({group}) [{alias}]: {message}"),
(None, None) => format!("[{alias}]: {message}"), (None, None) => format!("[{alias}]: {message}"),
@ -50,40 +64,40 @@ pub fn localize_chat_message(
), ),
}) })
.into_owned(), .into_owned(),
ChatType::CommandError => msg.message.to_string(), ChatType::CommandError => msg.content().localize(localizer),
ChatType::CommandInfo => msg.message.to_string(), ChatType::CommandInfo => msg.content().localize(localizer),
ChatType::FactionMeta(_) => msg.message.to_string(), ChatType::FactionMeta(_) => msg.content().localize(localizer),
ChatType::GroupMeta(_) => msg.message.to_string(), ChatType::GroupMeta(_) => msg.content().localize(localizer),
ChatType::Tell(from, to) => { ChatType::Tell(from, to) => {
let from_alias = name_format(from); let from_alias = name_format(from);
let to_alias = name_format(to); let to_alias = name_format(to);
// TODO: internationalise // TODO: internationalise
if *from == info.you { if *from == info.you {
format!("To [{to_alias}]: {}", msg.message) format!("To [{to_alias}]: {}", msg.content().localize(localizer))
} else { } else {
format!("From [{from_alias}]: {}", msg.message) format!("From [{from_alias}]: {}", msg.content().localize(localizer))
} }
}, },
ChatType::Say(uid) => message_format(uid, &msg.message, None), ChatType::Say(uid) => message_format(uid, msg.content(), None),
ChatType::Group(uid, s) => message_format(uid, &msg.message, Some(s)), ChatType::Group(uid, s) => message_format(uid, msg.content(), Some(s)),
ChatType::Faction(uid, s) => message_format(uid, &msg.message, Some(s)), ChatType::Faction(uid, s) => message_format(uid, msg.content(), Some(s)),
ChatType::Region(uid) => message_format(uid, &msg.message, None), ChatType::Region(uid) => message_format(uid, msg.content(), None),
ChatType::World(uid) => message_format(uid, &msg.message, None), ChatType::World(uid) => message_format(uid, msg.content(), None),
// NPCs can't talk. Should be filtered by hud/mod.rs for voxygen and // NPCs can't talk. Should be filtered by hud/mod.rs for voxygen and
// should be filtered by server (due to not having a Pos) for chat-cli // should be filtered by server (due to not having a Pos) for chat-cli
ChatType::Npc(uid, _r) => message_format(uid, &msg.message, None), ChatType::Npc(uid) => message_format(uid, msg.content(), None),
ChatType::NpcSay(uid, _r) => message_format(uid, &msg.message, None), ChatType::NpcSay(uid) => message_format(uid, msg.content(), None),
ChatType::NpcTell(from, to, _r) => { ChatType::NpcTell(from, to) => {
let from_alias = name_format(from); let from_alias = name_format(from);
let to_alias = name_format(to); let to_alias = name_format(to);
// TODO: internationalise // TODO: internationalise
if *from == info.you { if *from == info.you {
format!("To [{to_alias}]: {}", msg.message) format!("To [{to_alias}]: {}", msg.content().localize(localizer))
} else { } else {
format!("From [{from_alias}]: {}", msg.message) format!("From [{from_alias}]: {}", msg.content().localize(localizer))
} }
}, },
ChatType::Meta => msg.message.to_string(), ChatType::Meta => msg.content().localize(localizer),
ChatType::Kill(kill_source, victim) => { ChatType::Kill(kill_source, victim) => {
let i18n_buff = |buff| match buff { let i18n_buff = |buff| match buff {
BuffKind::Burning => "hud-outcome-burning", BuffKind::Burning => "hud-outcome-burning",
@ -209,8 +223,7 @@ pub fn localize_chat_message(
}, },
}; };
msg.message = new_msg; (msg.chat_type, new_msg)
msg
} }
fn insert_alias(you: bool, info: PlayerInfo, localisation: &Localization) -> String { fn insert_alias(you: bool, info: PlayerInfo, localisation: &Localization) -> String {

View File

@ -226,8 +226,8 @@ impl<'a> Widget for Chat<'a> {
for message in self.new_messages.iter() { for message in self.new_messages.iter() {
// Log the output of commands since the ingame terminal doesn't support copying // Log the output of commands since the ingame terminal doesn't support copying
// the output to the clipboard // the output to the clipboard
if let ChatType::CommandInfo = message.chat_type { if let ChatType::CommandInfo = &message.chat_type {
tracing::info!("Chat command info: {}", message.message); tracing::info!("Chat command info: {:?}", message.content());
} }
} }
//new messages - update chat w/ them & scroll down if at bottom of chat //new messages - update chat w/ them & scroll down if at bottom of chat
@ -426,14 +426,6 @@ impl<'a> Widget for Chat<'a> {
let messages = &state let messages = &state
.messages .messages
.iter() .iter()
.map(|m| {
localize_chat_message(
m.clone(),
|msg| self.client.lookup_msg_context(msg),
self.localized_strings,
show_char_name,
)
})
.filter(|m| { .filter(|m| {
if let Some(chat_tab) = current_chat_tab { if let Some(chat_tab) = current_chat_tab {
chat_tab.filter.satisfies(m, &group_members) chat_tab.filter.satisfies(m, &group_members)
@ -447,13 +439,19 @@ impl<'a> Widget for Chat<'a> {
.uid() .uid()
.and_then(|uid| { .and_then(|uid| {
self.client self.client
.lookup_msg_context(&m) .lookup_msg_context(m)
.player_alias .player_alias
.get(&uid) .get(&uid)
.map(|i| i.is_moderator) .map(|i| i.is_moderator)
}) })
.unwrap_or(false); .unwrap_or(false);
(is_moderator, m) let (chat_type, text) = localize_chat_message(
m.clone(),
|msg| self.client.lookup_msg_context(msg),
self.localized_strings,
show_char_name,
);
(is_moderator, chat_type, text)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let n_badges = messages.iter().filter(|t| t.0).count(); let n_badges = messages.iter().filter(|t| t.0).count();
@ -478,14 +476,14 @@ impl<'a> Widget for Chat<'a> {
while let Some(item) = items.next(ui) { while let Some(item) = items.next(ui) {
// This would be easier if conrod used the v-metrics from rusttype. // This would be easier if conrod used the v-metrics from rusttype.
if item.i < messages.len() { if item.i < messages.len() {
let (is_moderator, message) = &messages[item.i]; let (is_moderator, chat_type, text) = &messages[item.i];
let (color, icon) = render_chat_line(&message.chat_type, self.imgs); let (color, icon) = render_chat_line(chat_type, self.imgs);
// For each ChatType needing localization get/set matching pre-formatted // For each ChatType needing localization get/set matching pre-formatted
// localized string. This string will be formatted with the data // localized string. This string will be formatted with the data
// provided in ChatType in the client/src/mod.rs // provided in ChatType in the client/src/mod.rs
// fn format_message called below // fn format_message called below
let text = Text::new(&message.message) let text = Text::new(text)
.font_size(self.fonts.opensans.scale(15)) .font_size(self.fonts.opensans.scale(15))
.font_id(self.fonts.opensans.conrod_id) .font_id(self.fonts.opensans.conrod_id)
.w(CHAT_BOX_WIDTH - 17.0) .w(CHAT_BOX_WIDTH - 17.0)
@ -688,10 +686,10 @@ impl<'a> Widget for Chat<'a> {
if let Some(msg) = msg.strip_prefix(chat_settings.chat_cmd_prefix) { if let Some(msg) = msg.strip_prefix(chat_settings.chat_cmd_prefix) {
match parse_cmd(msg) { match parse_cmd(msg) {
Ok((name, args)) => events.push(Event::SendCommand(name, args)), Ok((name, args)) => events.push(Event::SendCommand(name, args)),
Err(err) => self.new_messages.push_back(ChatMsg { // TODO: Localise
chat_type: ChatType::CommandError, Err(err) => self
message: err, .new_messages
}), .push_back(ChatType::CommandError.into_plain_msg(err)),
} }
} else { } else {
events.push(Event::SendMessage(msg)); events.push(Event::SendMessage(msg));
@ -783,9 +781,9 @@ fn render_chat_line(chat_type: &ChatType<String>, imgs: &Imgs) -> (Color, conrod
ChatType::Faction(_uid, _s) => (FACTION_COLOR, imgs.chat_faction_small), ChatType::Faction(_uid, _s) => (FACTION_COLOR, imgs.chat_faction_small),
ChatType::Region(_uid) => (REGION_COLOR, imgs.chat_region_small), ChatType::Region(_uid) => (REGION_COLOR, imgs.chat_region_small),
ChatType::World(_uid) => (WORLD_COLOR, imgs.chat_world_small), ChatType::World(_uid) => (WORLD_COLOR, imgs.chat_world_small),
ChatType::Npc(_uid, _r) => panic!("NPCs can't talk!"), // Should be filtered by hud/mod.rs ChatType::Npc(_uid) => panic!("NPCs can't talk!"), // Should be filtered by hud/mod.rs
ChatType::NpcSay(_uid, _r) => (SAY_COLOR, imgs.chat_say_small), ChatType::NpcSay(_uid) => (SAY_COLOR, imgs.chat_say_small),
ChatType::NpcTell(_from, _to, _r) => (TELL_COLOR, imgs.chat_tell_small), ChatType::NpcTell(_from, _to) => (TELL_COLOR, imgs.chat_tell_small),
ChatType::Meta => (INFO_COLOR, imgs.chat_command_info_small), ChatType::Meta => (INFO_COLOR, imgs.chat_command_info_small),
} }
} }

View File

@ -3278,7 +3278,7 @@ impl Hud {
// Don't put NPC messages in chat box. // Don't put NPC messages in chat box.
self.new_messages self.new_messages
.retain(|m| !matches!(m.chat_type, comp::ChatType::Npc(_, _))); .retain(|m| !matches!(m.chat_type, comp::ChatType::Npc(_)));
// Chat box // Chat box
if global_state.settings.interface.toggle_chat { if global_state.settings.interface.toggle_chat {

View File

@ -20,6 +20,7 @@ use conrod_core::{
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
}; };
use i18n::Localization; use i18n::Localization;
use i18n_helpers::make_localizer;
use keyboard_keynames::key_layout::KeyLayout; use keyboard_keynames::key_layout::KeyLayout;
const MAX_BUBBLE_WIDTH: f64 = 250.0; const MAX_BUBBLE_WIDTH: f64 = 250.0;
@ -534,8 +535,7 @@ impl<'a> Widget for Overhead<'a> {
// Speech bubble // Speech bubble
if let Some(bubble) = self.bubble { if let Some(bubble) = self.bubble {
let dark_mode = self.settings.speech_bubble_dark_mode; let dark_mode = self.settings.speech_bubble_dark_mode;
let localizer = |s: &str, i| -> String { self.i18n.get_variation(s, i).to_string() }; let bubble_contents: String = bubble.content().localize(make_localizer(self.i18n));
let bubble_contents: String = bubble.message(localizer);
let (text_color, shadow_color) = bubble_color(bubble, dark_mode); let (text_color, shadow_color) = bubble_color(bubble, dark_mode);
let mut text = Text::new(&bubble_contents) let mut text = Text::new(&bubble_contents)
.color(text_color) .color(text_color)

View File

@ -18,7 +18,7 @@ use common::{
inventory::slot::{EquipSlot, Slot}, inventory::slot::{EquipSlot, Slot},
invite::InviteKind, invite::InviteKind,
item::{tool::ToolKind, ItemDesc}, item::{tool::ToolKind, ItemDesc},
ChatMsg, ChatType, InputKind, InventoryUpdateEvent, Pos, PresenceKind, Stats, ChatType, Content, InputKind, InventoryUpdateEvent, Pos, PresenceKind, Stats,
UtteranceKind, Vel, UtteranceKind, Vel,
}, },
consts::MAX_MOUNT_RANGE, consts::MAX_MOUNT_RANGE,
@ -309,17 +309,17 @@ impl SessionState {
InviteAnswer::TimedOut => "timed out", InviteAnswer::TimedOut => "timed out",
}; };
let msg = format!("{} invite to {} {}", kind_str, target_name, answer_str); let msg = format!("{} invite to {} {}", kind_str, target_name, answer_str);
self.hud.new_message(ChatType::Meta.chat_msg(msg)); // TODO: Localise
self.hud.new_message(ChatType::Meta.into_plain_msg(msg));
}, },
client::Event::TradeComplete { result, trade: _ } => { client::Event::TradeComplete { result, trade: _ } => {
self.hud.clear_cursor(); self.hud.clear_cursor();
let i18n = global_state.i18n.read(); self.hud
let msg = match result { .new_message(ChatType::Meta.into_msg(Content::localized(match result {
TradeResult::Completed => i18n.get_msg("hud-trade-result-completed"), TradeResult::Completed => "hud-trade-result-completed",
TradeResult::Declined => i18n.get_msg("hud-trade-result-declined"), TradeResult::Declined => "hud-trade-result-declined",
TradeResult::NotEnoughSpace => i18n.get_msg("hud-trade-result-nospace"), TradeResult::NotEnoughSpace => "hud-trade-result-nospace",
}; })));
self.hud.new_message(ChatType::Meta.chat_msg(msg));
}, },
client::Event::InventoryUpdated(inv_event) => { client::Event::InventoryUpdated(inv_event) => {
let sfx_triggers = self.scene.sfx_mgr.triggers.read(); let sfx_triggers = self.scene.sfx_mgr.triggers.read();
@ -380,21 +380,13 @@ impl SessionState {
}, },
client::Event::Disconnect => return Ok(TickAction::Disconnect), client::Event::Disconnect => return Ok(TickAction::Disconnect),
client::Event::DisconnectionNotification(time) => { client::Event::DisconnectionNotification(time) => {
let i18n = global_state.i18n.read(); self.hud
.new_message(ChatType::CommandError.into_msg(match time {
let message = match time { 0 => Content::localized("hud-chat-goodbye"),
0 => String::from(i18n.get_msg("hud-chat-goodbye")), _ => Content::localized_with_args("hud-chat-connection_lost", [(
_ => i18n "time", time,
.get_msg_ctx("hud-chat-connection_lost", &i18n::fluent_args! { )]),
"time" => time }));
})
.into_owned(),
};
self.hud.new_message(ChatMsg {
chat_type: ChatType::CommandError,
message,
});
}, },
client::Event::Kicked(reason) => { client::Event::Kicked(reason) => {
global_state.info_message = Some(format!( global_state.info_message = Some(format!(
@ -992,18 +984,12 @@ impl PlayState for SessionState {
|e| e.name.to_owned(), |e| e.name.to_owned(),
) )
}); });
let msg = global_state self.hud.new_message(ChatType::Meta.into_msg(
.i18n Content::localized_with_args(
.read()
.get_msg_ctx(
"hud-trade-invite_sent", "hud-trade-invite_sent",
&i18n::fluent_args! { [("playername", &name)],
"playername" => &name ),
}, ));
)
.into_owned();
self.hud
.new_message(ChatType::Meta.chat_msg(msg));
client.send_invite(uid, InviteKind::Trade) client.send_invite(uid, InviteKind::Trade)
}; };
}, },
@ -1115,10 +1101,11 @@ impl PlayState for SessionState {
); );
}, },
}, },
Event::ScreenshotMessage(screenshot_message) => self.hud.new_message(ChatMsg {
chat_type: ChatType::CommandInfo, // TODO: Localise
message: screenshot_message, Event::ScreenshotMessage(screenshot_msg) => self
}), .hud
.new_message(ChatType::CommandInfo.into_plain_msg(screenshot_msg)),
Event::Zoom(delta) if self.zoom_lock => { Event::Zoom(delta) if self.zoom_lock => {
// only fire this Hud event when player has "intent" to zoom // only fire this Hud event when player has "intent" to zoom
@ -1414,11 +1401,15 @@ impl PlayState for SessionState {
match run_command(&mut self.client.borrow_mut(), global_state, &name, args) match run_command(&mut self.client.borrow_mut(), global_state, &name, args)
{ {
Ok(Some(info)) => { Ok(Some(info)) => {
self.hud.new_message(ChatType::CommandInfo.chat_msg(&info)) // TODO: Localise
self.hud
.new_message(ChatType::CommandInfo.into_plain_msg(&info))
}, },
Ok(None) => {}, // Server will provide an info message Ok(None) => {}, // Server will provide an info message
Err(error) => { Err(error) => {
self.hud.new_message(ChatType::CommandError.chat_msg(error)) // TODO: Localise
self.hud
.new_message(ChatType::CommandError.into_plain_msg(error))
}, },
}; };
}, },