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"
version = "0.10.0"
dependencies = [
"hashbrown 0.12.3",
"tracing",
"veloren-client-i18n",
"veloren-common",

View File

@ -2280,13 +2280,15 @@ impl Client {
.any(|r| !matches!(r, group::Role::Pet))
{
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",
)));
}
if let Some(player_info) = self.player_list.get(&uid) {
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",
self.personalize_alias(uid, player_info.player_alias.clone())
)),
@ -2303,7 +2305,8 @@ impl Client {
Removed(uid) => {
if let Some(player_info) = self.player_list.get(&uid) {
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",
self.personalize_alias(uid, player_info.player_alias.clone())
)),
@ -2790,20 +2793,20 @@ impl Client {
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(to);
},
comp::ChatType::Say(uid)
| comp::ChatType::Region(uid)
| comp::ChatType::World(uid)
| comp::ChatType::NpcSay(uid, _) => {
| comp::ChatType::NpcSay(uid) => {
alias_of_uid(uid);
},
comp::ChatType::Group(uid, _) | comp::ChatType::Faction(uid, _) => {
alias_of_uid(uid);
},
comp::ChatType::Npc(uid, _) => alias_of_uid(uid),
comp::ChatType::Npc(uid) => alias_of_uid(uid),
comp::ChatType::Meta => (),
};
result

View File

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

View File

@ -2,6 +2,7 @@ use crate::{
comp::{group::Group, BuffKind},
uid::Uid,
};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use specs::{Component, DenseVecStorage};
use std::time::{Duration, Instant};
@ -29,8 +30,8 @@ impl Component for ChatMode {
}
impl ChatMode {
/// Create a message from your current chat mode and uuid.
pub fn new_message(&self, from: Uid, message: String) -> UnresolvedChatMsg {
/// Create a plain message from your current chat mode and uuid.
pub fn to_plain_msg(&self, from: Uid, text: impl ToString) -> UnresolvedChatMsg {
let chat_type = match self {
ChatMode::Tell(to) => ChatType::Tell(from, *to),
ChatMode::Say => ChatType::Say(from),
@ -39,7 +40,10 @@ impl ChatMode {
ChatMode::Faction(faction) => ChatType::Faction(from, faction.clone()),
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(Uid),
/// Messages sent from NPCs (Not shown in chat but as speech bubbles)
///
/// The u16 field is a random number for selecting localization variants.
Npc(Uid, u16),
Npc(Uid),
/// 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, to, localization variant)
NpcTell(Uid, Uid, u16),
NpcTell(Uid, Uid),
/// Anything else
Meta,
}
impl<G> ChatType<G> {
pub fn chat_msg<S>(self, msg: S) -> GenericChatMsg<G>
where
S: Into<String>,
{
pub fn into_plain_msg(self, text: impl ToString) -> GenericChatMsg<G> {
GenericChatMsg {
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::Region(u) => Some(*u),
ChatType::World(u) => Some(*u),
ChatType::Npc(u, _r) => Some(*u),
ChatType::NpcSay(u, _r) => Some(*u),
ChatType::NpcTell(u, _t, _r) => Some(*u),
ChatType::Npc(u) => Some(*u),
ChatType::NpcSay(u) => Some(*u),
ChatType::NpcTell(u, _t) => Some(*u),
ChatType::Meta => None,
}
}
@ -156,9 +162,9 @@ impl<G> ChatType<G> {
| ChatType::CommandError
| ChatType::FactionMeta(_)
| ChatType::GroupMeta(_)
| ChatType::Npc(_, _)
| ChatType::NpcSay(_, _)
| ChatType::NpcTell(_, _, _)
| ChatType::Npc(_)
| ChatType::NpcSay(_)
| ChatType::NpcTell(_, _)
| ChatType::Meta
| ChatType::Kill(_, _) => None,
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
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenericChatMsg<G> {
pub chat_type: ChatType<G>,
pub message: String,
content: Content,
}
pub type ChatMsg = GenericChatMsg<String>;
@ -183,19 +265,19 @@ impl<G> GenericChatMsg<G> {
pub const REGION_DISTANCE: f32 = 1000.0;
pub const SAY_DISTANCE: f32 = 100.0;
pub fn npc(uid: Uid, message: String) -> Self {
let chat_type = ChatType::Npc(uid, rand::random());
Self { chat_type, message }
pub fn npc(uid: Uid, content: Content) -> Self {
let chat_type = ChatType::Npc(uid);
Self { chat_type, content }
}
pub fn npc_say(uid: Uid, message: String) -> Self {
let chat_type = ChatType::NpcSay(uid, rand::random());
Self { chat_type, message }
pub fn npc_say(uid: Uid, content: Content) -> Self {
let chat_type = ChatType::NpcSay(uid);
Self { chat_type, content }
}
pub fn npc_tell(from: Uid, to: Uid, message: String) -> Self {
let chat_type = ChatType::NpcTell(from, to, rand::random());
Self { chat_type, message }
pub fn npc_tell(from: Uid, to: Uid, content: Content) -> Self {
let chat_type = ChatType::NpcTell(from, to);
Self { chat_type, content }
}
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::Region(a) => ChatType::Region(a),
ChatType::World(a) => ChatType::World(a),
ChatType::Npc(a, b) => ChatType::Npc(a, b),
ChatType::NpcSay(a, b) => ChatType::NpcSay(a, b),
ChatType::NpcTell(a, b, c) => ChatType::NpcTell(a, b, c),
ChatType::Npc(a) => ChatType::Npc(a),
ChatType::NpcSay(a) => ChatType::NpcSay(a),
ChatType::NpcTell(a, b) => ChatType::NpcTell(a, b),
ChatType::Meta => ChatType::Meta,
};
GenericChatMsg {
chat_type,
message: self.message,
content: self.content,
}
}
@ -234,15 +316,8 @@ impl<G> GenericChatMsg<G> {
}
pub fn to_bubble(&self) -> Option<(SpeechBubble, Uid)> {
let icon = self.icon();
if let ChatType::Npc(from, r) | ChatType::NpcSay(from, r) | ChatType::NpcTell(from, _, r) =
self.chat_type
{
Some((SpeechBubble::npc_new(&self.message, r, icon), from))
} else {
self.uid()
.map(|from| (SpeechBubble::player_new(&self.message, icon), from))
}
self.uid()
.map(|from| (SpeechBubble::new(self.content.clone(), self.icon()), from))
}
pub fn icon(&self) -> SpeechBubbleType {
@ -260,14 +335,18 @@ impl<G> GenericChatMsg<G> {
ChatType::Faction(_u, _s) => SpeechBubbleType::Faction,
ChatType::Region(_u) => SpeechBubbleType::Region,
ChatType::World(_u) => SpeechBubbleType::World,
ChatType::Npc(_u, _r) => SpeechBubbleType::None,
ChatType::NpcSay(_u, _r) => SpeechBubbleType::Say,
ChatType::NpcTell(_f, _t, _) => SpeechBubbleType::Say,
ChatType::Npc(_u) => SpeechBubbleType::None,
ChatType::NpcSay(_u) => SpeechBubbleType::Say,
ChatType::NpcTell(_f, _t) => SpeechBubbleType::Say,
ChatType::Meta => SpeechBubbleType::None,
}
}
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
@ -283,15 +362,6 @@ impl From<String> for Faction {
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.
///
/// 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
pub struct SpeechBubble {
pub message: SpeechBubbleMessage,
pub content: Content,
pub icon: SpeechBubbleType,
pub timeout: Instant,
}
@ -320,33 +390,14 @@ impl SpeechBubble {
/// Default duration in seconds of speech bubbles
pub const DEFAULT_DURATION: f64 = 5.0;
pub fn npc_new(i18n_key: &str, r: u16, icon: SpeechBubbleType) -> Self {
let message = SpeechBubbleMessage::Localized(i18n_key.to_string(), r);
pub fn new(content: Content, icon: SpeechBubbleType) -> Self {
let timeout = Instant::now() + Duration::from_secs_f64(SpeechBubble::DEFAULT_DURATION);
Self {
message,
content,
icon,
timeout,
}
}
pub fn player_new(message: &str, icon: SpeechBubbleType) -> Self {
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),
}
}
pub fn content(&self) -> &Content { &self.content }
}

View File

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

View File

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

View File

@ -22,7 +22,7 @@ use common::{
},
item_drop,
projectile::ProjectileConstructor,
Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller,
Agent, Alignment, Body, CharacterState, Content, ControlAction, ControlEvent, Controller,
HealthChange, InputKind, InventoryAction, Pos, Scale, UnresolvedChatMsg, UtteranceKind,
},
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(
*self.uid,
msg.to_string(),
Content::localized(key),
)));
}

View File

@ -2745,7 +2745,7 @@ fn handle_tell(
} else {
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));
Ok(())
} else {
@ -2770,7 +2770,7 @@ fn handle_faction(
let msg = args.join(" ");
if !msg.is_empty() {
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));
@ -2797,7 +2797,7 @@ fn handle_group(
let msg = args.join(" ");
if !msg.is_empty() {
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));
@ -2921,7 +2921,7 @@ fn handle_region(
let msg = args.join(" ");
if !msg.is_empty() {
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));
@ -2942,7 +2942,7 @@ fn handle_say(
let msg = args.join(" ");
if !msg.is_empty() {
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));
@ -2963,7 +2963,7 @@ fn handle_world(
let msg = args.join(" ");
if !msg.is_empty() {
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));
@ -2992,8 +2992,9 @@ fn handle_join_faction(
.flatten()
.map(|f| f.0);
server.state.send_chat(
// TODO: Localise
ChatType::FactionMeta(faction.clone())
.chat_msg(format!("[{}] joined faction ({})", alias, faction)),
.into_plain_msg(format!("[{}] joined faction ({})", alias, faction)),
);
(faction_join, mode)
} else {
@ -3009,8 +3010,9 @@ fn handle_join_faction(
};
if let Some(faction) = faction_leave {
server.state.send_chat(
// TODO: Localise
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));

View File

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

View File

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

View File

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

View File

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

View File

@ -28,7 +28,7 @@ impl Sys {
const CHAT_MODE_DEFAULT: &ChatMode = &ChatMode::default();
let mode = chat_modes.get(entity).unwrap_or(CHAT_MODE_DEFAULT);
// 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 {
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"}
# Utility
tracing = "0.1"
hashbrown = { version = "0.12" }

View File

@ -1,19 +1,32 @@
#![feature(let_chains)]
use common::comp::{
chat::{KillSource, KillType},
BuffKind, ChatMsg, ChatType,
BuffKind, ChatMsg, ChatType, Content,
};
use common_net::msg::{ChatTypeContext, PlayerInfo};
use hashbrown::HashMap;
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(
mut msg: ChatMsg,
msg: ChatMsg,
lookup_fn: impl Fn(&ChatMsg) -> ChatTypeContext,
localisation: &Localization,
show_char_name: bool,
) -> ChatMsg {
) -> (ChatType<String>, String) {
let info = lookup_fn(&msg);
let localizer = make_localizer(localisation);
let name_format = |uid: &common::uid::Uid| match info.player_alias.get(uid).cloned() {
Some(pi) => insert_alias(info.you == *uid, pi, localisation),
None => info
@ -23,13 +36,14 @@ pub fn localize_chat_message(
.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 name = if let Some(pi) = info.player_alias.get(from).cloned() && show_char_name {
pi.character.map(|c| c.name)
} else {
None
};
let message = content.localize(localizer);
match (group, name) {
(Some(group), None) => format!("({group}) [{alias}]: {message}"),
(None, None) => format!("[{alias}]: {message}"),
@ -50,40 +64,40 @@ pub fn localize_chat_message(
),
})
.into_owned(),
ChatType::CommandError => msg.message.to_string(),
ChatType::CommandInfo => msg.message.to_string(),
ChatType::FactionMeta(_) => msg.message.to_string(),
ChatType::GroupMeta(_) => msg.message.to_string(),
ChatType::CommandError => msg.content().localize(localizer),
ChatType::CommandInfo => msg.content().localize(localizer),
ChatType::FactionMeta(_) => msg.content().localize(localizer),
ChatType::GroupMeta(_) => msg.content().localize(localizer),
ChatType::Tell(from, to) => {
let from_alias = name_format(from);
let to_alias = name_format(to);
// TODO: internationalise
if *from == info.you {
format!("To [{to_alias}]: {}", msg.message)
format!("To [{to_alias}]: {}", msg.content().localize(localizer))
} 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::Group(uid, s) => message_format(uid, &msg.message, Some(s)),
ChatType::Faction(uid, s) => message_format(uid, &msg.message, Some(s)),
ChatType::Region(uid) => message_format(uid, &msg.message, None),
ChatType::World(uid) => message_format(uid, &msg.message, None),
ChatType::Say(uid) => message_format(uid, msg.content(), None),
ChatType::Group(uid, s) => message_format(uid, msg.content(), Some(s)),
ChatType::Faction(uid, s) => message_format(uid, msg.content(), Some(s)),
ChatType::Region(uid) => message_format(uid, msg.content(), None),
ChatType::World(uid) => message_format(uid, msg.content(), None),
// 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
ChatType::Npc(uid, _r) => message_format(uid, &msg.message, None),
ChatType::NpcSay(uid, _r) => message_format(uid, &msg.message, None),
ChatType::NpcTell(from, to, _r) => {
ChatType::Npc(uid) => message_format(uid, msg.content(), None),
ChatType::NpcSay(uid) => message_format(uid, msg.content(), None),
ChatType::NpcTell(from, to) => {
let from_alias = name_format(from);
let to_alias = name_format(to);
// TODO: internationalise
if *from == info.you {
format!("To [{to_alias}]: {}", msg.message)
format!("To [{to_alias}]: {}", msg.content().localize(localizer))
} 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) => {
let i18n_buff = |buff| match buff {
BuffKind::Burning => "hud-outcome-burning",
@ -209,8 +223,7 @@ pub fn localize_chat_message(
},
};
msg.message = new_msg;
msg
(msg.chat_type, new_msg)
}
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() {
// Log the output of commands since the ingame terminal doesn't support copying
// the output to the clipboard
if let ChatType::CommandInfo = message.chat_type {
tracing::info!("Chat command info: {}", message.message);
if let ChatType::CommandInfo = &message.chat_type {
tracing::info!("Chat command info: {:?}", message.content());
}
}
//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
.messages
.iter()
.map(|m| {
localize_chat_message(
m.clone(),
|msg| self.client.lookup_msg_context(msg),
self.localized_strings,
show_char_name,
)
})
.filter(|m| {
if let Some(chat_tab) = current_chat_tab {
chat_tab.filter.satisfies(m, &group_members)
@ -447,13 +439,19 @@ impl<'a> Widget for Chat<'a> {
.uid()
.and_then(|uid| {
self.client
.lookup_msg_context(&m)
.lookup_msg_context(m)
.player_alias
.get(&uid)
.map(|i| i.is_moderator)
})
.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<_>>();
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) {
// This would be easier if conrod used the v-metrics from rusttype.
if item.i < messages.len() {
let (is_moderator, message) = &messages[item.i];
let (color, icon) = render_chat_line(&message.chat_type, self.imgs);
let (is_moderator, chat_type, text) = &messages[item.i];
let (color, icon) = render_chat_line(chat_type, self.imgs);
// For each ChatType needing localization get/set matching pre-formatted
// localized string. This string will be formatted with the data
// provided in ChatType in the client/src/mod.rs
// fn format_message called below
let text = Text::new(&message.message)
let text = Text::new(text)
.font_size(self.fonts.opensans.scale(15))
.font_id(self.fonts.opensans.conrod_id)
.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) {
match parse_cmd(msg) {
Ok((name, args)) => events.push(Event::SendCommand(name, args)),
Err(err) => self.new_messages.push_back(ChatMsg {
chat_type: ChatType::CommandError,
message: err,
}),
// TODO: Localise
Err(err) => self
.new_messages
.push_back(ChatType::CommandError.into_plain_msg(err)),
}
} else {
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::Region(_uid) => (REGION_COLOR, imgs.chat_region_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::NpcSay(_uid, _r) => (SAY_COLOR, imgs.chat_say_small),
ChatType::NpcTell(_from, _to, _r) => (TELL_COLOR, imgs.chat_tell_small),
ChatType::Npc(_uid) => panic!("NPCs can't talk!"), // Should be filtered by hud/mod.rs
ChatType::NpcSay(_uid) => (SAY_COLOR, imgs.chat_say_small),
ChatType::NpcTell(_from, _to) => (TELL_COLOR, imgs.chat_tell_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.
self.new_messages
.retain(|m| !matches!(m.chat_type, comp::ChatType::Npc(_, _)));
.retain(|m| !matches!(m.chat_type, comp::ChatType::Npc(_)));
// Chat box
if global_state.settings.interface.toggle_chat {

View File

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

View File

@ -18,7 +18,7 @@ use common::{
inventory::slot::{EquipSlot, Slot},
invite::InviteKind,
item::{tool::ToolKind, ItemDesc},
ChatMsg, ChatType, InputKind, InventoryUpdateEvent, Pos, PresenceKind, Stats,
ChatType, Content, InputKind, InventoryUpdateEvent, Pos, PresenceKind, Stats,
UtteranceKind, Vel,
},
consts::MAX_MOUNT_RANGE,
@ -309,17 +309,17 @@ impl SessionState {
InviteAnswer::TimedOut => "timed out",
};
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: _ } => {
self.hud.clear_cursor();
let i18n = global_state.i18n.read();
let msg = match result {
TradeResult::Completed => i18n.get_msg("hud-trade-result-completed"),
TradeResult::Declined => i18n.get_msg("hud-trade-result-declined"),
TradeResult::NotEnoughSpace => i18n.get_msg("hud-trade-result-nospace"),
};
self.hud.new_message(ChatType::Meta.chat_msg(msg));
self.hud
.new_message(ChatType::Meta.into_msg(Content::localized(match result {
TradeResult::Completed => "hud-trade-result-completed",
TradeResult::Declined => "hud-trade-result-declined",
TradeResult::NotEnoughSpace => "hud-trade-result-nospace",
})));
},
client::Event::InventoryUpdated(inv_event) => {
let sfx_triggers = self.scene.sfx_mgr.triggers.read();
@ -380,21 +380,13 @@ impl SessionState {
},
client::Event::Disconnect => return Ok(TickAction::Disconnect),
client::Event::DisconnectionNotification(time) => {
let i18n = global_state.i18n.read();
let message = match time {
0 => String::from(i18n.get_msg("hud-chat-goodbye")),
_ => i18n
.get_msg_ctx("hud-chat-connection_lost", &i18n::fluent_args! {
"time" => time
})
.into_owned(),
};
self.hud.new_message(ChatMsg {
chat_type: ChatType::CommandError,
message,
});
self.hud
.new_message(ChatType::CommandError.into_msg(match time {
0 => Content::localized("hud-chat-goodbye"),
_ => Content::localized_with_args("hud-chat-connection_lost", [(
"time", time,
)]),
}));
},
client::Event::Kicked(reason) => {
global_state.info_message = Some(format!(
@ -992,18 +984,12 @@ impl PlayState for SessionState {
|e| e.name.to_owned(),
)
});
let msg = global_state
.i18n
.read()
.get_msg_ctx(
self.hud.new_message(ChatType::Meta.into_msg(
Content::localized_with_args(
"hud-trade-invite_sent",
&i18n::fluent_args! {
"playername" => &name
},
)
.into_owned();
self.hud
.new_message(ChatType::Meta.chat_msg(msg));
[("playername", &name)],
),
));
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,
message: screenshot_message,
}),
// TODO: Localise
Event::ScreenshotMessage(screenshot_msg) => self
.hud
.new_message(ChatType::CommandInfo.into_plain_msg(screenshot_msg)),
Event::Zoom(delta) if self.zoom_lock => {
// 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)
{
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
Err(error) => {
self.hud.new_message(ChatType::CommandError.chat_msg(error))
// TODO: Localise
self.hud
.new_message(ChatType::CommandError.into_plain_msg(error))
},
};
},