Correct localisation of numeric values, use coordinate conversions in rtsim

This commit is contained in:
Joshua Barretto 2023-04-12 10:34:24 +01:00
parent 0c688a9b50
commit 16e3227f78
13 changed files with 84 additions and 67 deletions

View File

@ -19,11 +19,11 @@ use std::{borrow::Cow, io};
use assets::{source::DirEntry, AssetExt, AssetGuard, AssetHandle, ReloadWatcher, SharedString}; use assets::{source::DirEntry, AssetExt, AssetGuard, AssetHandle, ReloadWatcher, SharedString};
use tracing::warn; use tracing::warn;
// Re-export because I don't like prefix // Re-export because I don't like prefix
use common::comp::Content; use common::comp::{Content, LocalizationArg};
use common_assets as assets; use common_assets as assets;
// Re-export for argument creation // Re-export for argument creation
pub use fluent::fluent_args; pub use fluent::{fluent_args, FluentValue};
pub use fluent_bundle::FluentArgs; pub use fluent_bundle::FluentArgs;
/// The reference language, aka the more up-to-date localization data. /// The reference language, aka the more up-to-date localization data.
@ -332,7 +332,7 @@ impl LocalizationGuard {
}) })
} }
/// Localize the given context. /// Localize the given content.
pub fn get_content(&self, content: &Content) -> String { pub fn get_content(&self, content: &Content) -> String {
match content { match content {
Content::Plain(text) => text.clone(), Content::Plain(text) => text.clone(),
@ -342,7 +342,14 @@ impl LocalizationGuard {
*seed, *seed,
&args &args
.iter() .iter()
.map(|(k, content)| (k, self.get_content(content))) .map(|(k, arg)| {
(k, match arg {
LocalizationArg::Content(content) => {
FluentValue::String(self.get_content(content).into())
},
LocalizationArg::Nat(n) => FluentValue::from(n),
})
})
.collect(), .collect(),
) )
.into_owned(), .into_owned(),

View File

@ -199,20 +199,56 @@ pub enum Content {
/// deterministic (but pseudorandom) localised output /// deterministic (but pseudorandom) localised output
seed: u16, seed: u16,
/// i18n arguments /// i18n arguments
args: HashMap<String, Content>, args: HashMap<String, LocalizationArg>,
}, },
} }
// TODO: Remove impl and make use of `Plain` explicit (to discourage it) // TODO: Remove impl and make use of `Plain(...)` explicit (to discourage it)
impl From<String> for Content { impl From<String> for Content {
fn from(text: String) -> Self { Self::Plain(text) } fn from(text: String) -> Self { Self::Plain(text) }
} }
// TODO: Remove impl and make use of `Plain` explicit (to discourage it) // TODO: Remove impl and make use of `Plain(...)` explicit (to discourage it)
impl<'a> From<&'a str> for Content { impl<'a> From<&'a str> for Content {
fn from(text: &'a str) -> Self { Self::Plain(text.to_string()) } fn from(text: &'a str) -> Self { Self::Plain(text.to_string()) }
} }
/// A localisation argument for localised content (see [`Content::Localized`]).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LocalizationArg {
/// The localisation argument is itself a section of content.
///
/// Note that this allows [`Content`] to recursively refer to itself. It may
/// be tempting to decide to parameterise everything, having dialogue
/// generated with a compact tree. "It's simpler!", you might say. False.
/// Over-parameterisation is an anti-pattern that hurts translators. Where
/// possible, prefer fewer levels of nesting unless doing so would
/// result in an intractably larger number of combinations. See [here](https://github.com/projectfluent/fluent/wiki/Good-Practices-for-Developers#prefer-wet-over-dry) for the
/// guidance provided by the docs for `fluent`, the localisation library
/// used by clients.
Content(Content),
/// The localisation argument is a natural number
Nat(u64),
}
// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
// discourage it)
impl From<String> for LocalizationArg {
fn from(text: String) -> Self { Self::Content(Content::Plain(text)) }
}
// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
// discourage it)
impl<'a> From<&'a str> for LocalizationArg {
fn from(text: &'a str) -> Self { Self::Content(Content::Plain(text.to_string())) }
}
// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
// discourage it)
impl From<u64> for LocalizationArg {
fn from(n: u64) -> Self { Self::Nat(n) }
}
impl Content { impl Content {
pub fn localized(key: impl ToString) -> Self { pub fn localized(key: impl ToString) -> Self {
Self::Localized { Self::Localized {
@ -222,7 +258,7 @@ impl Content {
} }
} }
pub fn localized_with_args<'a, A: Into<Content>>( pub fn localized_with_args<'a, A: Into<LocalizationArg>>(
key: impl ToString, key: impl ToString,
args: impl IntoIterator<Item = (&'a str, A)>, args: impl IntoIterator<Item = (&'a str, A)>,
) -> Self { ) -> Self {
@ -341,6 +377,8 @@ impl<G> GenericChatMsg<G> {
pub fn content(&self) -> &Content { &self.content } pub fn content(&self) -> &Content { &self.content }
pub fn into_content(self) -> Content { self.content }
pub fn set_content(&mut self, content: Content) { self.content = content; } pub fn set_content(&mut self, content: Content) { self.content = content; }
} }

View File

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

View File

@ -84,7 +84,9 @@ pub trait CoordinateConversions {
impl CoordinateConversions for Vec2<i32> { impl CoordinateConversions for Vec2<i32> {
#[inline] #[inline]
fn wpos_to_cpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e / sz as i32) } fn wpos_to_cpos(&self) -> Self {
self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e.div_euclid(sz as i32))
}
#[inline] #[inline]
fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as i32) } fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as i32) }

View File

@ -12,8 +12,7 @@ use common::{
Actor, ChunkResource, FactionId, NpcAction, NpcActivity, Personality, SiteId, VehicleId, Actor, ChunkResource, FactionId, NpcAction, NpcActivity, Personality, SiteId, VehicleId,
}, },
store::Id, store::Id,
terrain::TerrainChunkSize, terrain::CoordinateConversions,
vol::RectVolSize,
}; };
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use rand::prelude::*; use rand::prelude::*;
@ -349,12 +348,7 @@ impl Npcs {
wpos: Vec3<f32>, wpos: Vec3<f32>,
radius: f32, radius: f32,
) -> impl Iterator<Item = Actor> + '_ { ) -> impl Iterator<Item = Actor> + '_ {
let chunk_pos = wpos let chunk_pos = wpos.xy().as_().wpos_to_cpos();
.xy()
.as_::<i32>()
.map2(TerrainChunkSize::RECT_SIZE.as_::<i32>(), |e, sz| {
e.div_euclid(sz)
});
let r_sqr = radius * radius; let r_sqr = radius * radius;
LOCALITY LOCALITY
.into_iter() .into_iter()

View File

@ -16,9 +16,8 @@ use common::{
rtsim::{ChunkResource, Profession, SiteId}, rtsim::{ChunkResource, Profession, SiteId},
spiral::Spiral2d, spiral::Spiral2d,
store::Id, store::Id,
terrain::{SiteKindMeta, TerrainChunkSize}, terrain::{CoordinateConversions, SiteKindMeta, TerrainChunkSize},
time::DayPeriod, time::DayPeriod,
vol::RectVolSize,
}; };
use fxhash::FxHasher64; use fxhash::FxHasher64;
use itertools::{Either, Itertools}; use itertools::{Either, Itertools};
@ -333,7 +332,7 @@ where
let wpos_site = |wpos: Vec2<f32>| { let wpos_site = |wpos: Vec2<f32>| {
ctx.world ctx.world
.sim() .sim()
.get(wpos.as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_()) .get(wpos.as_().wpos_to_cpos())
.and_then(|chunk| chunk.sites.first().copied()) .and_then(|chunk| chunk.sites.first().copied())
}; };
@ -568,7 +567,7 @@ fn hunt_animals() -> impl Action {
} }
fn find_forest(ctx: &mut NpcCtx) -> Option<Vec2<f32>> { fn find_forest(ctx: &mut NpcCtx) -> Option<Vec2<f32>> {
let chunk_pos = ctx.npc.wpos.xy().as_() / TerrainChunkSize::RECT_SIZE.as_(); let chunk_pos = ctx.npc.wpos.xy().as_().wpos_to_cpos();
Spiral2d::new() Spiral2d::new()
.skip(ctx.rng.gen_range(1..=8)) .skip(ctx.rng.gen_range(1..=8))
.take(49) .take(49)
@ -801,8 +800,7 @@ fn chunk_path(
.filter_map(|p| Some((p, chunk_height(p)?))) .filter_map(|p| Some((p, chunk_height(p)?)))
}, },
|(p0, h0), (p1, h1)| { |(p0, h0), (p1, h1)| {
let diff = let diff = (p0 - p1).as_().cpos_to_wpos().with_z((h0 - h1) as f32);
((p0 - p1).as_() * TerrainChunkSize::RECT_SIZE.as_()).with_z((h0 - h1) as f32);
diff.magnitude_squared() diff.magnitude_squared()
}, },
@ -841,9 +839,8 @@ fn pilot() -> impl Action {
}) })
.choose(&mut ctx.rng); .choose(&mut ctx.rng);
if let Some((_id, site)) = site { if let Some((_id, site)) = site {
let start_chunk = let start_chunk = ctx.npc.wpos.xy().as_().wpos_to_cpos();
ctx.npc.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>(); let end_chunk = site.wpos.wpos_to_cpos();
let end_chunk = site.wpos / TerrainChunkSize::RECT_SIZE.as_::<i32>();
chunk_path(start_chunk, end_chunk, |chunk| { chunk_path(start_chunk, end_chunk, |chunk| {
ctx.world ctx.world
.sim() .sim()
@ -861,7 +858,7 @@ fn pilot() -> impl Action {
fn captain() -> impl Action { fn captain() -> impl Action {
// For now just randomly travel the sea // For now just randomly travel the sea
now(|ctx| { now(|ctx| {
let chunk = ctx.npc.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>(); let chunk = ctx.npc.wpos.xy().as_().wpos_to_cpos();
if let Some(chunk) = NEIGHBORS if let Some(chunk) = NEIGHBORS
.into_iter() .into_iter()
.map(|neighbor| chunk + neighbor) .map(|neighbor| chunk + neighbor)

View File

@ -6,8 +6,7 @@ use crate::{
use common::{ use common::{
comp::{self, Body}, comp::{self, Body},
rtsim::{Actor, NpcAction, NpcActivity, Personality}, rtsim::{Actor, NpcAction, NpcActivity, Personality},
terrain::TerrainChunkSize, terrain::CoordinateConversions,
vol::RectVolSize,
}; };
use rand::prelude::*; use rand::prelude::*;
use rand_chacha::ChaChaRng; use rand_chacha::ChaChaRng;
@ -180,11 +179,7 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
| common::comp::ship::Body::AirBalloon => true, | common::comp::ship::Body::AirBalloon => true,
common::comp::ship::Body::SailBoat common::comp::ship::Body::SailBoat
| common::comp::ship::Body::Galleon => { | common::comp::ship::Body::Galleon => {
let chunk_pos = let chunk_pos = wpos.xy().as_().wpos_to_cpos();
wpos.xy().as_::<i32>().map2(
TerrainChunkSize::RECT_SIZE.as_::<i32>(),
|e, sz| e.div_euclid(sz),
);
ctx.world ctx.world
.sim() .sim()
.get(chunk_pos) .get(chunk_pos)

View File

@ -2,7 +2,7 @@ use crate::{
event::{EventCtx, OnDeath, OnSetup, OnTick}, event::{EventCtx, OnDeath, OnSetup, OnTick},
RtState, Rule, RuleError, RtState, Rule, RuleError,
}; };
use common::{grid::Grid, rtsim::Actor, terrain::TerrainChunkSize, vol::RectVolSize}; use common::{grid::Grid, rtsim::Actor, terrain::CoordinateConversions};
pub struct SyncNpcs; pub struct SyncNpcs;
@ -45,7 +45,7 @@ fn on_tick(ctx: EventCtx<SyncNpcs, OnTick>) {
let data = &mut *ctx.state.data_mut(); let data = &mut *ctx.state.data_mut();
// Update vehicle grid cells // Update vehicle grid cells
for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() { for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() {
let chunk_pos = vehicle.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>(); let chunk_pos = vehicle.wpos.xy().as_().wpos_to_cpos();
if vehicle.chunk_pos != Some(chunk_pos) { if vehicle.chunk_pos != Some(chunk_pos) {
if let Some(cell) = vehicle if let Some(cell) = vehicle
.chunk_pos .chunk_pos
@ -66,14 +66,7 @@ fn on_tick(ctx: EventCtx<SyncNpcs, OnTick>) {
npc.current_site = ctx npc.current_site = ctx
.world .world
.sim() .sim()
.get( .get(npc.wpos.xy().as_().wpos_to_cpos())
npc.wpos
.xy()
.as_::<i32>()
.map2(TerrainChunkSize::RECT_SIZE.as_::<i32>(), |e, sz| {
e.div_euclid(sz)
}),
)
.and_then(|chunk| { .and_then(|chunk| {
chunk chunk
.sites .sites
@ -99,13 +92,7 @@ fn on_tick(ctx: EventCtx<SyncNpcs, OnTick>) {
} }
// Update the NPC's grid cell // Update the NPC's grid cell
let chunk_pos = npc let chunk_pos = npc.wpos.xy().as_().wpos_to_cpos();
.wpos
.xy()
.as_::<i32>()
.map2(TerrainChunkSize::RECT_SIZE.as_::<i32>(), |e, sz| {
e.div_euclid(sz)
});
if npc.chunk_pos != Some(chunk_pos) { if npc.chunk_pos != Some(chunk_pos) {
if let Some(cell) = npc if let Some(cell) = npc
.chunk_pos .chunk_pos

View File

@ -1515,6 +1515,8 @@ impl<'a> AgentData<'a> {
} }
} }
// TODO: Pass a localisation key instead of `Content` to avoid allocating if
// we're not permitted to speak.
pub fn chat_npc_if_allowed_to_speak( pub fn chat_npc_if_allowed_to_speak(
&self, &self,
msg: Content, msg: Content,

View File

@ -201,7 +201,10 @@ impl RtSim {
debug!("Saving rtsim data..."); debug!("Saving rtsim data...");
// Create the save thread if it doesn't already exist // Create the save thread if it doesn't already exist
// TODO: Use the slow job pool eventually // We're not using the slow job pool here for two reasons:
// 1) The thread is mostly blocked on IO, not compute
// 2) We need to synchronise saves to ensure monotonicity, which slow jobs
// aren't designed to allow
let (tx, _) = self.save_thread.get_or_insert_with(|| { let (tx, _) = self.save_thread.get_or_insert_with(|| {
trace!("Starting rtsim data save thread..."); trace!("Starting rtsim data save thread...");
let (tx, rx) = unbounded(); let (tx, rx) = unbounded();

View File

@ -9,9 +9,8 @@ use common::{
resources::{DeltaTime, Time, TimeOfDay}, resources::{DeltaTime, Time, TimeOfDay},
rtsim::{Actor, RtSimEntity, RtSimVehicle}, rtsim::{Actor, RtSimEntity, RtSimVehicle},
slowjob::SlowJobPool, slowjob::SlowJobPool,
terrain::{CoordinateConversions, TerrainChunkSize}, terrain::CoordinateConversions,
trade::{Good, SiteInformation}, trade::{Good, SiteInformation},
vol::RectVolSize,
LoadoutBuilder, LoadoutBuilder,
}; };
use common_ecs::{Job, Origin, Phase, System}; use common_ecs::{Job, Origin, Phase, System};
@ -236,13 +235,7 @@ impl<'a> System<'a> for Sys {
data.npcs.character_map.clear(); data.npcs.character_map.clear();
for (presence, wpos) in (&presences, &positions).join() { for (presence, wpos) in (&presences, &positions).join() {
if let PresenceKind::Character(character) = &presence.kind { if let PresenceKind::Character(character) = &presence.kind {
let chunk_pos = wpos let chunk_pos = wpos.0.xy().as_().wpos_to_cpos();
.0
.xy()
.as_::<i32>()
.map2(TerrainChunkSize::RECT_SIZE.as_::<i32>(), |e, sz| {
e.div_euclid(sz)
});
data.npcs data.npcs
.character_map .character_map
.entry(chunk_pos) .entry(chunk_pos)

View File

@ -874,7 +874,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),
msg.content().clone(), msg.into_content(),
)) ))
} }
}, },

View File

@ -384,8 +384,7 @@ impl SessionState {
.new_message(ChatType::CommandError.into_msg(match time { .new_message(ChatType::CommandError.into_msg(match time {
0 => Content::localized("hud-chat-goodbye"), 0 => Content::localized("hud-chat-goodbye"),
_ => Content::localized_with_args("hud-chat-connection_lost", [( _ => Content::localized_with_args("hud-chat-connection_lost", [(
"time", "time", time,
time.to_string(),
)]), )]),
})); }));
}, },
@ -988,7 +987,7 @@ impl PlayState for SessionState {
self.hud.new_message(ChatType::Meta.into_msg( self.hud.new_message(ChatType::Meta.into_msg(
Content::localized_with_args( Content::localized_with_args(
"hud-trade-invite_sent", "hud-trade-invite_sent",
[("playername", name.as_str())], [("playername", name)],
), ),
)); ));
client.send_invite(uid, InviteKind::Trade) client.send_invite(uid, InviteKind::Trade)