Merge branch 'juliancoffee/i18n-gender-chat' into 'master'

First step to enabling grammatical genders

See merge request veloren/veloren!4301
This commit is contained in:
Illia Denysenko 2024-02-04 17:40:08 +00:00
commit a2d52d09fc
14 changed files with 674 additions and 325 deletions

View File

@ -1,7 +1,29 @@
## Player events
## Player events, $user_gender should be available
hud-chat-online_msg = [{ $name }] is online now
hud-chat-offline_msg = [{ $name }] went offline
## Buff deaths
hud-chat-goodbye = Goodbye!
hud-chat-connection_lost = Connection lost. Kicking in { $time } seconds.
## Player /tell messages, $user_gender should be available
hud-chat-tell-to = To [{ $alias }]: { $msg }
hud-chat-tell-from = From [{ $alias }]: { $msg }
# Npc /tell messages, no gender info, sadly
hud-chat-tell-to-npc = To [{ $alias }]: { $msg }
hud-chat-tell-from-npc = From [{ $alias }]: { $msg }
# Generic messages
hud-chat-message = [{ $alias }]: { $msg }
hud-chat-message-with-name = [{ $alias }] { $name }: { $msg }
hud-chat-message-in-group = ({ $group }) [{ $alias }]: { $msg }
hud-chat-message-with-name-in-group = ({ $group }) [{ $alias }] { $name }: { $msg }
## PvP Buff deaths, both $attacker_gender and $victim_gender are available
hud-chat-died_of_pvp_buff_msg =
.burning = [{ $victim }] died of: burning caused by [{ $attacker }]
.bleeding = [{ $victim }] died of: bleeding caused by [{ $attacker }]
@ -9,13 +31,9 @@ hud-chat-died_of_pvp_buff_msg =
.crippled = [{ $victim }] died of: crippled caused by [{ $attacker }]
.frozen = [{ $victim }] died of: frozen caused by [{ $attacker }]
.mysterious = [{ $victim }] died of: secret caused by [{ $attacker }]
hud-chat-died_of_buff_nonexistent_msg =
.burning = [{ $victim }] died of: burning
.bleeding = [{ $victim }] died of: bleeding
.curse = [{ $victim }] died of: curse
.crippled = [{ $victim }] died of: crippled
.frozen = [{ $victim }] died of: frozen
.mysterious = [{ $victim }] died of: secret
## PvE Buff deaths, only $victim_gender is available
hud-chat-died_of_npc_buff_msg =
.burning = [{ $victim }] died of: burning caused by { $attacker }
.bleeding = [{ $victim }] died of: bleeding caused by { $attacker }
@ -23,31 +41,47 @@ hud-chat-died_of_npc_buff_msg =
.crippled = [{ $victim }] died of: crippled caused by { $attacker }
.frozen = [{ $victim }] died of: frozen caused by { $attacker }
.mysterious = [{ $victim }] died of: secret caused by { $attacker }
## PvP deaths
## Random Buff deaths, only $victim_gender is available
hud-chat-died_of_buff_nonexistent_msg =
.burning = [{ $victim }] died of: burning
.bleeding = [{ $victim }] died of: bleeding
.curse = [{ $victim }] died of: curse
.crippled = [{ $victim }] died of: crippled
.frozen = [{ $victim }] died of: frozen
.mysterious = [{ $victim }] died of: secret
## Other PvP deaths, both $attacker_gender and $victim_gender are available
hud-chat-pvp_melee_kill_msg = [{ $attacker }] defeated [{ $victim }]
hud-chat-pvp_ranged_kill_msg = [{ $attacker }] shot [{ $victim }]
hud-chat-pvp_explosion_kill_msg = [{ $attacker }] blew up [{ $victim }]
hud-chat-pvp_energy_kill_msg = [{ $attacker }] killed [{ $victim }] with magic
hud-chat-pvp_other_kill_msg = [{ $attacker }] killed [{ $victim }]
## PvE deaths
## Other PvE deaths, only $victim_gender is available
hud-chat-npc_melee_kill_msg = { $attacker } killed [{ $victim }]
hud-chat-npc_ranged_kill_msg = { $attacker } shot [{ $victim }]
hud-chat-npc_explosion_kill_msg = { $attacker } blew up [{ $victim }]
hud-chat-npc_energy_kill_msg = { $attacker } killed [{ $victim }] with magic
hud-chat-npc_other_kill_msg = { $attacker } killed [{ $victim }]
## Other deaths
hud-chat-environmental_kill_msg = [{ $name }] died in { $environment }
## Other deaths, only $victim_gender is available
hud-chat-fall_kill_msg = [{ $name }] died from fall damage
hud-chat-suicide_msg = [{ $name }] died from self-inflicted wounds
hud-chat-default_death_msg = [{ $name }] died
## Utils
## Chat utils
hud-chat-all = All
hud-chat-you = You
hud-chat-chat_tab_hover_tooltip = Right click for settings
hud-loot-pickup-msg = {$actor} picked up { $amount ->
[one] { $item }
*[other] {$amount}x {$item}
## HUD Pickup message
hud-loot-pickup-msg = { $amount ->
[one] { $actor } picked up { $item }
*[other] { $actor } picked up {$amount}x {$item}
}
hud-chat-loot_fail = Your Inventory is full!
hud-chat-goodbye = Goodbye!
hud-chat-connection_lost = Connection lost. Kicking in { $time } seconds.

View File

@ -1,57 +1,169 @@
## Player events
hud-chat-online_msg = [{ $name }] зайшов/-ла на сервер
hud-chat-offline_msg = [{ $name }] вийшов/-ла з серверу
## Buff deaths
hud-chat-online_msg = [{ $name }] { $user_gender ->
[she] зайшла на сервер
*[he] зайшов на сервер
}
hud-chat-offline_msg = [{ $name }] { $user_gender ->
[she] вийшла з серверу
*[he] вийшов з серверу
}
hud-chat-goodbye = До побачення!
hud-chat-connection_lost = З'єднання втрачено. Відключення через { $time ->
[one] { $time } секунду
[few] { $time } секунди
*[other] { $time } секунд
}
## PvP Buff deaths
hud-chat-died_of_pvp_buff_msg =
.burning = [{ $victim }] згорів/-ла живцем через [{ $attacker }]
.bleeding = [{ $victim }] помер/-ла від кровотечі через [{ $attacker }]
.curse = [{ $victim }] помер/-ла від прокльону через [{ $attacker }]
.crippled = [{ $victim }] загинув/-ла від травм через [{ $attacker }]
.frozen = [{ $victim }] замерз/-ла на смерть через [{ $attacker }]
.mysterious = [{ $victim }] помер/-ла таємничою смертю через [{ $attacker }]
hud-chat-died_of_buff_nonexistent_msg =
.burning = [{ $victim }] згорів/-ла живцем
.bleeding = [{ $victim }] помер/-ла від кровотечі
.curse = [{ $victim }] помер/-ла від прокльону
.crippled = [{ $victim }] загинув/-ла від травм
.frozen = [{ $victim }] замерз/-ла на смерть
.mysterious = [{ $victim }] помер/-ла таємничою смертю
.burning = { $attacker_gender ->
[she] [{ $attacker }] спалила [{ $victim }] живцем
*[he] [{ $attacker }] спалив [{ $victim }] живцем
}
.bleeding = { $victim_gender ->
[she] [{$victim}] втратила занадто багато крові отримавши поранення від [{ $attacker }]
*[he] [{$victim}] втратив занадто багато крові отримавши поранення від [{ $attacker }]
}
.curse = { $victim_gender ->
[she] [{ $victim }] померла від прокляття накладеного [{ $attacker }]
*[he] [{ $victim }] помер від прокляття накладеного [{ $attacker }]
}
.crippled = { $victim_gender ->
[she] [{ $victim }] загинула від отриманих травм через [{ $attacker }]
*[he] [{ $victim }] загинув від отриманих травм через [{ $attacker }]
}
.frozen = { $victim_gender ->
[she] [{ $victim }] замерзла на смерть через [{ $attacker }]
*[he] [{ $victim }] замерз на смерть через [{ $attacker }]
}
.mysterious = { $victim_gender ->
[she] [{ $victim }] померла ... через [{ $attacker }] ... як?
*[he] [{ $victim }] помер ... через [{ $attacker }] ... як?
}
## PvE buff deaths
hud-chat-died_of_npc_buff_msg =
.burning = [{ $victim }] згорів/-ла живцем через { $attacker }
.bleeding = [{ $victim }] помер/-ла від кровотечі через { $attacker }
.curse = [{ $victim }] помер/-ла від прокльону через { $attacker }
.crippled = [{ $victim }] загинув/-ла від травм через { $attacker }
.frozen = [{ $victim }] замерз/-ла на смерть через { $attacker }
.mysterious = [{ $victim }] помер/-ла таємничою смертю через { $attacker }
.burning = { $victim_gender ->
[she] [{ $victim }] отримала через [{ $attacker }] опіки несумісні з життям
*[he] [{ $victim }] отримав через [{ $attacker }] опіки несумісні з життям
}
.bleeding = { $victim_gender ->
[she] [{ $victim }] втратила занадто багато крові отримавши поранення від [{ $attacker }]
*[he] [{ $victim }] втратив занадто багато крові отримавши поранення від [{ $attacker }]
}
.curse = { $victim_gender ->
[she] [{ $victim }] померла від прокляття накладеного [{ $attacker }]
*[he] [{ $victim }] помер від прокляття накладеного [{ $attacker }]
}
.crippled = { $victim_gender ->
[she] [{ $victim }] загинула від отриманих травм через [{ $attacker }]
*[he] [{ $victim }] загинув від отриманих травм через [{ $attacker }]
}
.frozen = { $victim_gender ->
[she] [{ $victim }] замерзла на смерть через [{ $attacker }]
*[he] [{ $victim }] замерз на смерть через [{ $attacker }]
}
.mysterious = { $victim_gender ->
[she] [{ $victim }] померла ... через [{ $attacker }] ... як?
*[he] [{ $victim }] помер ... через [{ $attacker }] ... як?
}
## Random buff deaths
hud-chat-died_of_buff_nonexistent_msg =
.burning = { $victim_gender ->
[she] [{ $victim }] отримала опіки несумісні з життям
*[he] [{ $victim }] отримав опіки несумісні з життям
}
.bleeding = { $victim_gender ->
[she] [{ $victim }] втратила занадто багато крові
*[he] [{ $victim }] втратив занадто багато крові
}
.curse = { $victim_gender ->
[she] [{ $victim }] померла від прокляття
*[he] [{ $victim }] помер від прокляття
}
.crippled = { $victim_gender ->
[she] [{ $victim }] загинула від отриманих травм
*[he] [{ $victim }] загинув від отриманих травм
}
.frozen = { $victim_gender ->
[she] [{ $victim }] замерзла на смерть
*[he] [{ $victim }] замерз на смерть
}
.mysterious = { $victim_gender ->
[she] [{ $victim }] померла ... як?
*[he] [{ $victim }] помер ... як?
}
## PvP deaths
hud-chat-pvp_melee_kill_msg = [{ $attacker }] переміг/-ла [{ $victim }]
hud-chat-pvp_ranged_kill_msg = [{ $attacker }] застрелив/-ла [{ $victim }]
hud-chat-pvp_explosion_kill_msg = [{ $attacker }] підірвав/-ла [{ $victim }]
hud-chat-pvp_energy_kill_msg = [{ $attacker }] вбив/-ла [{ $victim }] магією
hud-chat-pvp_other_kill_msg = { $attacker } вбив/-ла [{ $victim }]
hud-chat-pvp_melee_kill_msg = { $victim_gender ->
[she] [{ $victim }] вбита [{ $attacker }] у ближньому двобої
*[he] [{ $victim }] вбитий [{ $attacker }] у ближньому двобої
}
hud-chat-pvp_ranged_kill_msg = { $victim_gender ->
[she] [{ $victim }] впала вбита після влучного пострілу [{ $attacker }]
*[he] [{ $victim }] впав вбитий після влучного пострілу [{ $attacker }]
}
hud-chat-pvp_explosion_kill_msg = { $victim_gender ->
[she] [{ $victim }] розлетілась на атоми від удару [{ $attacker }]
*[he] [{ $victim }] розлетівся на атоми від удару [{ $attacker }]
}
hud-chat-pvp_energy_kill_msg = { $victim_gender ->
[she] [{ $victim }] впала вбита не встигнувши ухилитися від енергетичної атаки [{ $attacker }]
*[he] [{ $victim }] впав вбитий не встигнувши ухилитися від енергетичної атаки [{ $attacker }]
}
hud-chat-pvp_other_kill_msg = { $victim_gender ->
[she] [{ $victim }] вбита [{ $attacker }]
*[he] [{ $victim }] вбитий [{ $attacker }]
}
## PvE deaths
hud-chat-npc_melee_kill_msg = { $attacker } вбив/-ла [{ $victim }]
hud-chat-npc_ranged_kill_msg = { $attacker } застрелив/-ла [{ $victim }]
hud-chat-npc_explosion_kill_msg = { $attacker } підірвав/-ла [{ $victim }]
hud-chat-npc_energy_kill_msg = { $attacker } вбив/-ла [{ $victim }] магією
hud-chat-npc_other_kill_msg = { $attacker } вбив/-ла [{ $victim }]
hud-chat-npc_melee_kill_msg = { $victim_gender ->
[she] [{ $victim }] вбита [{ $attacker }] у ближньому двобої
*[he] [{ $victim }] вбитий [{ $attacker }] у ближньому двобої
}
hud-chat-npc_ranged_kill_msg = { $victim_gender ->
[she] [{ $victim }] впала вбита після влучного пострілу [{ $attacker }]
*[he] [{ $victim }] впав вбитий після влучного пострілу [{ $attacker }]
}
hud-chat-npc_explosion_kill_msg = { $victim_gender ->
[she] [{ $victim }] розлетілась на атоми від удару [{ $attacker }]
*[he] [{ $victim }] розлетівся на атоми від удару [{ $attacker }]
}
hud-chat-npc_energy_kill_msg = { $victim_gender ->
[she] [{ $victim }] впала вбита не встигнувши ухилитися від енергетичної атаки [{ $attacker }]
*[he] [{ $victim }] впав вбитий не встигнувши ухилитися від енергетичної атаки [{ $attacker }]
}
hud-chat-npc_other_kill_msg = { $victim_gender ->
[she] [{ $victim }] вбита [{ $attacker }]
*[he] [{ $victim }] вбитий [{ $attacker }]
}
## Other deaths
hud-chat-environmental_kill_msg = [{ $name }] помер/-ла в { $environment }
hud-chat-fall_kill_msg = [{ $name }] помер/-ла від падіння
hud-chat-suicide_msg = [{ $name }] помер/-ла від самозаподіяних ран
hud-chat-default_death_msg = [{ $name }] помер/-ла
## Utils
hud-chat-all = Усі
hud-chat-you = Ти
hud-chat-fall_kill_msg = { $victim_gender ->
[she] [{ $name }] померла від падіння
*[he] [{ $name }] помер від падіння
}
hud-chat-suicide_msg = { $victim_gender ->
[she] [{ $name }] померла від самозаподіяних ран
*[he] [{ $name }] помер від самозаподіяних ран
}
hud-chat-default_death_msg = { $victim_gender ->
[she] [{ $name }] померла
*[he] [{ $name }] помер
}
## Chat utils
hud-chat-all = Все
hud-chat-chat_tab_hover_tooltip = Правий клік для налаштування
# hud-chat-you = Ви
## HUD Pickup message
hud-loot-pickup-msg = {$actor} підняли { $amount ->
[1] { $item }
*[other] {$amount}x {$item}
}
hud-chat-loot_fail = Ваш інвентар переповнено!
hud-chat-goodbye = До побачення!
hud-chat-connection_lost = З'єднання втрачено. Перепідключення через { $time ->
[one] { $time } секунду
[few] { $time } секунди
*[other] { $time } секунд
}

View File

@ -2250,6 +2250,7 @@ impl Client {
player_info.character = match &player_info.character {
Some(character) => Some(msg::CharacterInfo {
name: character.name.to_string(),
gender: character.gender,
}),
None => {
warn!(
@ -2870,10 +2871,13 @@ impl Client {
}
/// Change player alias to "You" if client belongs to matching player
// TODO: move this to voxygen or i18n-helpers and properly localize there
// or what's better, just remove completely, it won't properly work with
// localization anyway.
pub fn personalize_alias(&self, uid: Uid, alias: String) -> String {
let client_uid = self.uid().expect("Client doesn't have a Uid!!!");
if client_uid == uid {
"You".to_string() // TODO: Localize
"You".to_string()
} else {
alias
}
@ -2884,9 +2888,10 @@ impl Client {
pub fn lookup_msg_context(&self, msg: &comp::ChatMsg) -> ChatTypeContext {
let mut result = ChatTypeContext {
you: self.uid().expect("Client doesn't have a Uid!!!"),
player_alias: HashMap::new(),
player_info: HashMap::new(),
entity_name: HashMap::new(),
};
let name_of_uid = |uid| {
let ecs = self.state.ecs();
(
@ -2897,53 +2902,52 @@ impl Client {
.find(|(_, u)| u == &uid)
.map(|(c, _)| c.name.clone())
};
let mut alias_of_uid = |uid| match self.player_list.get(uid) {
Some(player_info) => {
result.player_alias.insert(*uid, player_info.clone());
},
None => {
result
.entity_name
.insert(*uid, name_of_uid(uid).unwrap_or_else(|| "<?>".to_string()));
},
let mut add_data_of = |uid| {
match self.player_list.get(uid) {
Some(player_info) => {
result.player_info.insert(*uid, player_info.clone());
},
None => {
result
.entity_name
.insert(*uid, name_of_uid(uid).unwrap_or_else(|| "<?>".to_string()));
},
};
};
match &msg.chat_type {
comp::ChatType::Online(uid) | comp::ChatType::Offline(uid) => {
alias_of_uid(uid);
},
comp::ChatType::CommandError => (),
comp::ChatType::CommandInfo => (),
comp::ChatType::FactionMeta(_) => (),
comp::ChatType::GroupMeta(_) => (),
comp::ChatType::Online(uid) | comp::ChatType::Offline(uid) => add_data_of(uid),
comp::ChatType::Kill(kill_source, victim) => {
alias_of_uid(victim);
add_data_of(victim);
match kill_source {
KillSource::Player(attacker_uid, _) => {
alias_of_uid(attacker_uid);
add_data_of(attacker_uid);
},
KillSource::NonPlayer(_, _) => (),
KillSource::Environment(_) => (),
KillSource::FallDamage => (),
KillSource::Suicide => (),
KillSource::NonExistent(_) => (),
KillSource::Other => (),
KillSource::NonPlayer(_, _)
| KillSource::FallDamage
| KillSource::Suicide
| KillSource::NonExistent(_)
| KillSource::Other => (),
};
},
comp::ChatType::Tell(from, to) | comp::ChatType::NpcTell(from, to) => {
alias_of_uid(from);
alias_of_uid(to);
add_data_of(from);
add_data_of(to);
},
comp::ChatType::Say(uid)
| comp::ChatType::Region(uid)
| comp::ChatType::World(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::Meta => (),
| comp::ChatType::NpcSay(uid)
| comp::ChatType::Group(uid, _)
| comp::ChatType::Faction(uid, _)
| comp::ChatType::Npc(uid) => add_data_of(uid),
comp::ChatType::CommandError
| comp::ChatType::CommandInfo
| comp::ChatType::FactionMeta(_)
| comp::ChatType::GroupMeta(_)
| 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, Content},
comp::{self, body::Gender, invite::InviteKind, item::MaterialStatManifest, Content},
event::UpdateCharacterMetadata,
lod,
outcome::Outcome,
@ -35,7 +35,7 @@ pub enum ServerMsg {
Init(Box<ServerInit>),
/// Result to `ClientMsg::Register`. send ONCE
RegisterAnswer(ServerRegisterAnswer),
///Msg that can be send ALWAYS as soon as client is registered, e.g. `Chat`
/// Msg that can be send ALWAYS as soon as client is registered, e.g. `Chat`
General(ServerGeneral),
Ping(PingMsg),
}
@ -257,13 +257,14 @@ pub struct PlayerInfo {
/// used for localisation, filled by client and used by i18n code
pub struct ChatTypeContext {
pub you: Uid,
pub player_alias: HashMap<Uid, PlayerInfo>,
pub player_info: HashMap<Uid, PlayerInfo>,
pub entity_name: HashMap<Uid, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CharacterInfo {
pub name: String,
pub gender: Option<Gender>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]

View File

@ -198,6 +198,20 @@ impl<
const EXTENSION: &'static str = "ron";
}
/// Semantic gender aka body_type
///
/// Should be used for localization with extreme care.
/// For basically everything except *maybe* humanoids, it's simply wrong to
/// assume that this may be used as grammatical gender.
///
/// TODO: remove this and instead add GUI for players to choose preferred
/// gender. Read a comment for `gender_str` in voxygen/i18n-helpers/src/lib.rs.
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
pub enum Gender {
Masculine,
Feminine,
}
impl Body {
pub fn is_same_species_as(&self, other: &Body) -> bool {
match self {
@ -1360,6 +1374,17 @@ impl Body {
try_localize(self).unwrap_or_else(|| Content::localized("body-npc-speech-generic"))
}
/// Read comment on `Gender` for more
pub fn humanoid_gender(&self) -> Option<Gender> {
match self {
Body::Humanoid(b) => match b.body_type {
humanoid::BodyType::Male => Some(Gender::Masculine),
humanoid::BodyType::Female => Some(Gender::Feminine),
},
_ => None,
}
}
}
impl Component for Body {

View File

@ -57,6 +57,11 @@ impl ChatMode {
pub const fn default() -> Self { Self::World }
}
/// Enum representing death types
///
/// All variants should be strictly typed, no string content.
///
/// If it's too complicated to create an enum for death type, consult i18n team
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum KillType {
Buff(BuffKind),
@ -65,15 +70,17 @@ pub enum KillType {
Explosion,
Energy,
Other,
// Projectile(String), TODO: add projectile name when available
// Projectile(Type), TODO: add projectile name when available
}
/// Enum representing death reasons
///
/// All variants should be strictly typed, no string content.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum KillSource {
Player(Uid, KillType),
NonPlayer(String, KillType),
NonExistent(KillType),
Environment(String),
FallDamage,
Suicide,
Other,

View File

@ -54,7 +54,7 @@ pub use self::{
body::{
arthropod, biped_large, biped_small, bird_large, bird_medium, crustacean, dragon,
fish_medium, fish_small, golem, humanoid, item_drop, object, quadruped_low,
quadruped_medium, quadruped_small, ship, theropod, AllBodies, Body, BodyData,
quadruped_medium, quadruped_small, ship, theropod, AllBodies, Body, BodyData, Gender,
},
buff::{
Buff, BuffCategory, BuffChange, BuffData, BuffEffect, BuffKey, BuffKind, BuffSource, Buffs,

View File

@ -17,12 +17,14 @@ pub struct PlayerInfo {
alias: String,
}
/// Enum representing death reasons
///
/// All variants should be strictly typed, no string content.
#[derive(Clone, Serialize, Deserialize)]
pub enum KillSource {
Player(PlayerInfo, KillType),
NonPlayer(String, KillType),
NonExistent(KillType),
Environment(String),
FallDamage,
Suicide,
Other,
@ -160,7 +162,6 @@ impl ChatExporter {
},
comp::chat::KillSource::NonPlayer(str, t) => KillSource::NonPlayer(str, t),
comp::chat::KillSource::NonExistent(t) => KillSource::NonExistent(t),
comp::chat::KillSource::Environment(str) => KillSource::Environment(str),
comp::chat::KillSource::FallDamage => KillSource::FallDamage,
comp::chat::KillSource::Suicide => KillSource::Suicide,
comp::chat::KillSource::Other => KillSource::Other,

View File

@ -166,7 +166,7 @@ impl Client {
ServerMsg::RegisterAnswer(m) => PreparedMsg::new(0, &m, &self.register_stream_params),
ServerMsg::General(g) => {
match g {
//Character Screen related
// Character Screen related
ServerGeneral::CharacterDataLoadResult(_)
| ServerGeneral::CharacterListUpdate(_)
| ServerGeneral::CharacterActionError(_)
@ -176,7 +176,7 @@ impl Client {
| ServerGeneral::SpectatorSuccess(_) => {
PreparedMsg::new(1, &g, &self.character_screen_stream_params)
},
//In-game related
// In-game related
ServerGeneral::GroupUpdate(_)
| ServerGeneral::Invite { .. }
| ServerGeneral::InvitePending(_)

View File

@ -522,6 +522,8 @@ pub fn handle_possess(server: &mut Server, possessor_uid: Uid, possessee_uid: Ui
character: ecs.read_storage::<comp::Stats>().get(possessee).map(|s| {
msg::CharacterInfo {
name: s.name.clone(),
// NOTE: hack, read docs on body::Gender for more
gender: s.original_body.humanoid_gender(),
}
}),
uuid: player.uuid(),

View File

@ -724,6 +724,8 @@ impl StateExt for State {
self.notify_players(ServerGeneral::PlayerListUpdate(
PlayerListUpdate::SelectedCharacter(player_uid, CharacterInfo {
name: String::from(&stats.name),
// NOTE: hack, read docs on body::Gender for more
gender: stats.original_body.humanoid_gender(),
}),
));

View File

@ -99,6 +99,8 @@ impl<'a> System<'a> for Sys {
player_alias: player.alias.clone(),
character: stats.map(|stats| CharacterInfo {
name: stats.name.clone(),
// NOTE: hack, read docs for body::Gender for more
gender: stats.original_body.humanoid_gender(),
}),
uuid: player.uuid(),
}),

View File

@ -1,9 +1,13 @@
#![feature(let_chains)]
use std::borrow::Cow;
use common::comp::{
chat::{KillSource, KillType},
BuffKind, ChatMsg, ChatType, Content,
use common::{
comp::{
body::Gender,
chat::{KillSource, KillType},
BuffKind, ChatMsg, ChatType, Content,
},
uid::Uid,
};
use common_net::msg::{ChatTypeContext, PlayerInfo};
use i18n::Localization;
@ -16,250 +20,405 @@ pub fn localize_chat_message(
) -> (ChatType<String>, String) {
let info = lookup_fn(&msg);
let name_format = |uid: &common::uid::Uid| match info.player_alias.get(uid).cloned() {
Some(pi) => insert_alias(info.you == *uid, pi, localization),
let name_format_or_complex = |complex, uid: &Uid| match info.player_info.get(uid).cloned() {
Some(pi) => {
if complex {
insert_alias(info.you == *uid, pi, localization)
} else {
pi.player_alias
}
},
None => info
.entity_name
.get(uid)
.cloned()
.expect("client didn't proved enough info"),
.expect("client didn't provided enough info"),
};
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 {
// Some messages do suffer from complicated logic of insert_alias.
// This includes every notification-like message, like death.
let name_format = |uid: &Uid| name_format_or_complex(false, uid);
// This is a hack, kind of.
//
// Current implementation just checks if our player is humanoid, and if so,
// we take the body_type of its character and assume it as grammatical gender.
//
// In short,
// body_type of character
// -> sex of character
// -> gender of player.
// -> grammatical gender for use in messages.
//
// This is obviously, wrong, but it's good enough approximation, after all,
// players do choose their characters.
//
// In the future, we will want special GUI where players can specify their
// gender (and change it!), and we do want to handle more genders than just
// male and female.
//
// Ideally, the system should handle following (if we exclude plurals):
// - Female
// - Male
// - Neuter (or fallback Female)
// - Neuter (or fallback Male)
// - Intermediate (or fallback Female)
// - Intermediate (or fallback Male)
// and maybe more, not sure.
//
// What is supported by language and what is not, as well as maybe how to
// convert genders into strings to match, should go into _manifest.ron file
//
// So let's say language only supports male and female, we will convert all
// genders to these, using some fallbacks, and pass it.
//
// If the language can represent Female, Male and Neuter, we can pass these.
//
// Exact design of such a complex system is honestly up to discussion.
let gender_str = |uid: &Uid| {
if let Some(pi) = info.player_info.get(uid) {
match pi.character.as_ref().and_then(|c| c.gender) {
Some(Gender::Feminine) => "she".to_owned(),
Some(Gender::Masculine) => "he".to_owned(),
None => "??".to_owned(),
}
} else {
"??".to_owned()
}
};
// This is where the most fun begings.
//
// Unlike people, "items" can have their own gender, which is completely
// independent of everything, including common sense.
//
// For example, word "masculinity" can be feminine in some languages,
// as well as word "boy", and vice versa.
//
// So we can't rely on body_type, at all. And even if we did try, our
// body_type isn't even always represents animal sex, there are some
// animals that use body_type to represent their kind, like different
// types of Fox ("male" fox is forest, "female" is arctic one).
// And what about Mindflayer? They do have varied body_type, but do they
// even have concept of gender?
//
// Our use case is probably less cryptic, after all we are limited by
// mostly sentient things, but that doesn't help at all.
//
// Common example is word "spider", which can be feminine in one languages
// and masculine in other, and sometimes even neuter.
//
// Oh, and I want to add that we are talking about grammatical genders, and
// languages define their own grammar. There are languages that have more
// than three grammatical genders, there are languages that don't have
// male/female distinction and instead realy on animacy/non-animacy.
// What has an animacy and what doesn't is for language to decide.
// There are languages as well that mix these concepts and may have neuter,
// female, masculine with animacy, masculine with animacy. Some languages
// have their own scheme of things that arbitrarily picks noun-class per
// noun.
// Don't get me wrong. *All* languages do pick the gender for the word
// arbitrary as I showed at the beginning, it's just some languages have
// not just different mapping, but different gender set as well.
//
// The *only* option we have is fetch the gender per each name entry from
// localization files.
//
// I'm not 100% sure what should be the implementation of it, but I imagine
// that Stats::name() should be changed to include a way to reference where
// to grab the gender associated with this name, so translation then can
// pick right article or use right adjective/verb connected with NPC in the
// context of the message.
let _gender_str_npc = || "idk".to_owned();
let message_format = |from: &Uid, content: &Content, group: Option<&String>| {
let alias = name_format_or_complex(true, from);
let name = if let Some(pi) = info.player_info.get(from).cloned() && show_char_name {
pi.character.map(|c| c.name)
} else {
None
};
let message = localization.get_content(content);
match (group, name) {
(Some(group), None) => format!("({group}) [{alias}]: {message}"),
(None, None) => format!("[{alias}]: {message}"),
(Some(group), Some(name)) => format!("({group}) [{alias}] {name}: {message}"),
(None, Some(name)) => format!("[{alias}] {name}: {message}"),
}
let line = match group {
Some(group) => match name {
Some(name) => localization.get_msg_ctx(
"hud-chat-message-in-group-with-name",
&i18n::fluent_args! {
"group" => group,
"alias" => alias,
"name" => name,
"msg" => message,
},
),
None => {
localization.get_msg_ctx("hud-chat-message-in-group", &i18n::fluent_args! {
"group" => group,
"alias" => alias,
"msg" => message,
})
},
},
None => match name {
Some(name) => {
localization.get_msg_ctx("hud-chat-message-with-name", &i18n::fluent_args! {
"alias" => alias,
"name" => name,
"msg" => message,
})
},
None => localization.get_msg_ctx("hud-chat-message", &i18n::fluent_args! {
"alias" => alias,
"msg" => message,
}),
},
};
line.into_owned()
};
let new_msg = match &msg.chat_type {
ChatType::Online(uid) => localization
.get_msg_ctx("hud-chat-online_msg", &i18n::fluent_args! {
"user_gender" => gender_str(uid),
"name" => name_format(uid),
})
.into_owned(),
ChatType::Offline(uid) => localization
.get_msg_ctx("hud-chat-offline_msg", &i18n::fluent_args! {
"name" => name_format(uid
),
"user_gender" => gender_str(uid),
"name" => name_format(uid),
})
.into_owned(),
ChatType::CommandError => localization.get_content(msg.content()),
ChatType::CommandInfo => localization.get_content(msg.content()),
ChatType::FactionMeta(_) => localization.get_content(msg.content()),
ChatType::GroupMeta(_) => localization.get_content(msg.content()),
ChatType::CommandError
| ChatType::CommandInfo
| ChatType::Meta
| ChatType::FactionMeta(_)
| ChatType::GroupMeta(_) => localization.get_content(msg.content()),
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}]: {}",
localization.get_content(msg.content())
)
// If `from` is you, it means you're writing to someone
// and you want to see who you're writing to.
//
// Otherwise, someone writes to you, and you want to see
// who is that person that's writing to you.
let (key, person_to_show) = if info.you == *from {
("hud-chat-tell-to", to)
} else {
format!(
"From [{from_alias}]: {}",
localization.get_content(msg.content())
)
}
},
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) => 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}]: {}",
localization.get_content(msg.content())
)
} else {
format!(
"From [{from_alias}]: {}",
localization.get_content(msg.content())
)
}
},
ChatType::Meta => localization.get_content(msg.content()),
ChatType::Kill(kill_source, victim) => {
let get_buff_ident = |buff| match buff {
BuffKind::Burning => "burning",
BuffKind::Bleeding => "bleeding",
BuffKind::Cursed => "curse",
BuffKind::Crippled => "crippled",
BuffKind::Frozen => "frozen",
BuffKind::Regeneration
| BuffKind::Saturation
| BuffKind::Potion
| BuffKind::Agility
| BuffKind::CampfireHeal
| BuffKind::EnergyRegen
| BuffKind::IncreaseMaxEnergy
| BuffKind::IncreaseMaxHealth
| BuffKind::Invulnerability
| BuffKind::ProtectingWard
| BuffKind::Frenzied
| BuffKind::Hastened
| BuffKind::Fortitude
| BuffKind::Reckless
| BuffKind::Flame
| BuffKind::Frigid
| BuffKind::Lifesteal
// | BuffKind::SalamanderAspect
| BuffKind::ImminentCritical
| BuffKind::Fury
| BuffKind::Sunderer
| BuffKind::Defiance
| BuffKind::Bloodfeast
| BuffKind::Berserk => {
tracing::error!("Player was killed by a positive buff!");
"mysterious"
},
BuffKind::Wet
| BuffKind::Ensnared
| BuffKind::Poisoned
| BuffKind::Parried
| BuffKind::PotionSickness
| BuffKind::Polymorphed
| BuffKind::Heatstroke => {
tracing::error!("Player was killed by a debuff that doesn't do damage!");
"mysterious"
},
("hud-chat-tell-from", from)
};
match kill_source {
// Buff deaths
KillSource::Player(attacker, KillType::Buff(buff_kind)) => {
let buff_ident = get_buff_ident(*buff_kind);
localization
.get_msg_ctx(key, &i18n::fluent_args! {
"alias" => name_format(person_to_show),
"user_gender" => gender_str(person_to_show),
})
.into_owned()
},
ChatType::Say(uid) | ChatType::Region(uid) | ChatType::World(uid) => {
message_format(uid, msg.content(), None)
},
ChatType::Group(uid, descriptor) | ChatType::Faction(uid, descriptor) => {
message_format(uid, msg.content(), Some(descriptor))
},
ChatType::Npc(uid) | ChatType::NpcSay(uid) => message_format(uid, msg.content(), None),
ChatType::NpcTell(from, to) => {
// If `from` is you, it means you're writing to someone
// and you want to see who you're writing to.
//
// Otherwise, someone writes to you, and you want to see
// who is that person that's writing to you.
//
// Hopefully, no gendering needed, because for npc, we
// simply don't know.
let (key, person_to_show) = if info.you == *from {
("hud-chat-tell-to-npc", to)
} else {
("hud-chat-tell-from-npc", from)
};
let s = localization
.get_attr_ctx(
"hud-chat-died_of_pvp_buff_msg",
buff_ident,
&i18n::fluent_args! {
"victim" => name_format(victim),
"attacker" => name_format(attacker),
},
)
.into_owned();
Cow::Owned(s)
},
KillSource::NonPlayer(attacker_name, KillType::Buff(buff_kind)) => {
let buff_ident = get_buff_ident(*buff_kind);
let s = localization
.get_attr_ctx(
"hud-chat-died_of_npc_buff_msg",
buff_ident,
&i18n::fluent_args! {
"victim" => name_format(victim),
"attacker" => attacker_name,
},
)
.into_owned();
Cow::Owned(s)
},
KillSource::NonExistent(KillType::Buff(buff_kind)) => {
let buff_ident = get_buff_ident(*buff_kind);
let s = localization
.get_attr_ctx(
"hud-chat-died_of_buff_nonexistent_msg",
buff_ident,
&i18n::fluent_args! {
"victim" => name_format(victim),
},
)
.into_owned();
Cow::Owned(s)
},
// PvP deaths
KillSource::Player(attacker, kill_type) => {
let key = match kill_type {
KillType::Melee => "hud-chat-pvp_melee_kill_msg",
KillType::Projectile => "hud-chat-pvp_ranged_kill_msg",
KillType::Explosion => "hud-chat-pvp_explosion_kill_msg",
KillType::Energy => "hud-chat-pvp_energy_kill_msg",
KillType::Other => "hud-chat-pvp_other_kill_msg",
&KillType::Buff(_) => unreachable!("handled above"),
};
localization.get_msg_ctx(key, &i18n::fluent_args! {
"victim" => name_format(victim),
"attacker" => name_format(attacker),
})
},
// PvE deaths
KillSource::NonPlayer(attacker_name, kill_type) => {
let key = match kill_type {
KillType::Melee => "hud-chat-npc_melee_kill_msg",
KillType::Projectile => "hud-chat-npc_ranged_kill_msg",
KillType::Explosion => "hud-chat-npc_explosion_kill_msg",
KillType::Energy => "hud-chat-npc_energy_kill_msg",
KillType::Other => "hud-chat-npc_other_kill_msg",
&KillType::Buff(_) => unreachable!("handled above"),
};
localization.get_msg_ctx(key, &i18n::fluent_args! {
"victim" => name_format(victim),
"attacker" => attacker_name,
})
},
// Other deaths
KillSource::Environment(environment) => {
localization.get_msg_ctx("hud-chat-environment_kill_msg", &i18n::fluent_args! {
"name" => name_format(victim),
"environment" => environment,
})
},
KillSource::FallDamage => {
localization.get_msg_ctx("hud-chat-fall_kill_msg", &i18n::fluent_args! {
"name" => name_format(victim),
})
},
KillSource::Suicide => {
localization.get_msg_ctx("hud-chat-suicide_msg", &i18n::fluent_args! {
"name" => name_format(victim),
})
},
KillSource::NonExistent(_) | KillSource::Other => {
localization.get_msg_ctx("hud-chat-default_death_msg", &i18n::fluent_args! {
"name" => name_format(victim),
})
},
}
.into_owned()
localization
.get_msg_ctx(key, &i18n::fluent_args! {
"alias" => name_format(person_to_show),
})
.into_owned()
},
ChatType::Kill(kill_source, victim) => {
localize_kill_message(kill_source, victim, name_format, gender_str, localization)
},
};
(msg.chat_type, new_msg)
}
fn insert_alias(you: bool, info: PlayerInfo, localization: &Localization) -> String {
const YOU: &str = "hud-chat-you";
fn localize_kill_message(
kill_source: &KillSource,
victim: &Uid,
name_format: impl Fn(&Uid) -> String,
gender_str: impl Fn(&Uid) -> String,
localization: &Localization,
) -> String {
match kill_source {
// PvP deaths
KillSource::Player(attacker, kill_type) => {
let key = match kill_type {
KillType::Melee => "hud-chat-pvp_melee_kill_msg",
KillType::Projectile => "hud-chat-pvp_ranged_kill_msg",
KillType::Explosion => "hud-chat-pvp_explosion_kill_msg",
KillType::Energy => "hud-chat-pvp_energy_kill_msg",
KillType::Other => "hud-chat-pvp_other_kill_msg",
KillType::Buff(buff_kind) => {
let buff_ident = get_buff_ident(*buff_kind);
return localization
.get_attr_ctx(
"hud-chat-died_of_pvp_buff_msg",
buff_ident,
&i18n::fluent_args! {
"victim" => name_format(victim),
"victim_gender" => gender_str(victim),
"attacker" => name_format(attacker),
"attacker_gender" => gender_str(attacker),
},
)
.into_owned();
},
};
localization.get_msg_ctx(key, &i18n::fluent_args! {
"victim" => name_format(victim),
"victim_gender" => gender_str(victim),
"attacker" => name_format(attacker),
"attacker_gender" => gender_str(attacker),
})
},
// PvE deaths
KillSource::NonPlayer(attacker_name, kill_type) => {
let key = match kill_type {
KillType::Melee => "hud-chat-npc_melee_kill_msg",
KillType::Projectile => "hud-chat-npc_ranged_kill_msg",
KillType::Explosion => "hud-chat-npc_explosion_kill_msg",
KillType::Energy => "hud-chat-npc_energy_kill_msg",
KillType::Other => "hud-chat-npc_other_kill_msg",
KillType::Buff(buff_kind) => {
let buff_ident = get_buff_ident(*buff_kind);
return localization
.get_attr_ctx(
"hud-chat-died_of_npc_buff_msg",
buff_ident,
&i18n::fluent_args! {
"victim" => name_format(victim),
"victim_gender" => gender_str(victim),
"attacker" => attacker_name,
},
)
.into_owned();
},
};
localization.get_msg_ctx(key, &i18n::fluent_args! {
"victim" => name_format(victim),
"victim_gender" => gender_str(victim),
"attacker" => attacker_name,
})
},
// Other deaths
KillSource::FallDamage => {
localization.get_msg_ctx("hud-chat-fall_kill_msg", &i18n::fluent_args! {
"name" => name_format(victim),
"victim_gender" => gender_str(victim),
})
},
KillSource::Suicide => {
localization.get_msg_ctx("hud-chat-suicide_msg", &i18n::fluent_args! {
"name" => name_format(victim),
"victim_gender" => gender_str(victim),
})
},
KillSource::NonExistent(KillType::Buff(buff_kind)) => {
let buff_ident = get_buff_ident(*buff_kind);
let s = localization
.get_attr_ctx(
"hud-chat-died_of_buff_nonexistent_msg",
buff_ident,
&i18n::fluent_args! {
"victim" => name_format(victim),
"victim_gender" => gender_str(victim),
},
)
.into_owned();
Cow::Owned(s)
},
KillSource::NonExistent(_) | KillSource::Other => {
localization.get_msg_ctx("hud-chat-default_death_msg", &i18n::fluent_args! {
"name" => name_format(victim),
"victim_gender" => gender_str(victim),
})
},
}
.into_owned()
}
/// Determines .attr for `hud-chat-died-of-buff` messages
fn get_buff_ident(buff: BuffKind) -> &'static str {
match buff {
BuffKind::Burning => "burning",
BuffKind::Bleeding => "bleeding",
BuffKind::Cursed => "curse",
BuffKind::Crippled => "crippled",
BuffKind::Frozen => "frozen",
BuffKind::Regeneration
| BuffKind::Saturation
| BuffKind::Potion
| BuffKind::Agility
| BuffKind::CampfireHeal
| BuffKind::EnergyRegen
| BuffKind::IncreaseMaxEnergy
| BuffKind::IncreaseMaxHealth
| BuffKind::Invulnerability
| BuffKind::ProtectingWard
| BuffKind::Frenzied
| BuffKind::Hastened
| BuffKind::Fortitude
| BuffKind::Reckless
| BuffKind::Flame
| BuffKind::Frigid
| BuffKind::Lifesteal
// | BuffKind::SalamanderAspect
| BuffKind::ImminentCritical
| BuffKind::Fury
| BuffKind::Sunderer
| BuffKind::Defiance
| BuffKind::Bloodfeast
| BuffKind::Berserk => {
tracing::error!("Player was killed by a positive buff!");
"mysterious"
},
BuffKind::Wet
| BuffKind::Ensnared
| BuffKind::Poisoned
| BuffKind::Parried
| BuffKind::PotionSickness
| BuffKind::Polymorphed
| BuffKind::Heatstroke => {
tracing::error!("Player was killed by a debuff that doesn't do damage!");
"mysterious"
},
}
}
/// Used for inserting spacing for mod badge icon next to alias
// TODO: consider passing '$is_you' to hud-chat-message strings along with
// $spacing variable, for more flexible translations.
fn insert_alias(_replace_you: bool, info: PlayerInfo, _localization: &Localization) -> String {
// Leave space for a mod badge icon.
const MOD_SPACING: &str = " ";
match (info.is_moderator, you) {
(false, false) => info.player_alias,
(false, true) => localization.get_msg(YOU).to_string(),
(true, false) => format!("{}{}", MOD_SPACING, info.player_alias),
(true, true) => format!("{}{}", MOD_SPACING, &localization.get_msg(YOU),),
if info.is_moderator {
format!("{}{}", MOD_SPACING, info.player_alias)
} else {
info.player_alias
}
}

View File

@ -481,7 +481,7 @@ impl<'a> Widget for Chat<'a> {
.and_then(|uid| {
self.client
.lookup_msg_context(m)
.player_alias
.player_info
.get(&uid)
.map(|i| i.is_moderator)
})