diff options
author | Griffin Smith <root@gws.fyi> | 2019-07-19T15·54-0400 |
---|---|---|
committer | Griffin Smith <root@gws.fyi> | 2019-07-19T15·54-0400 |
commit | e2d13bd76b9af9cc2734cdcb9df605afa95cca31 (patch) | |
tree | 8a720b216dd6481fcda491ffcb082d989e2d15c6 | |
parent | bc93999cf37a65d48f25e30795c85a0aef97efac (diff) |
Add templates for messages
Implement a template syntax with a nom parser, and a formatter to render templates to strings.
-rw-r--r-- | Cargo.lock | 80 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/entities/creature.rs | 2 | ||||
-rw-r--r-- | src/game.rs | 64 | ||||
-rw-r--r-- | src/main.rs | 2 | ||||
-rw-r--r-- | src/messages.rs | 127 | ||||
-rw-r--r-- | src/messages.toml | 9 | ||||
-rw-r--r-- | src/util/mod.rs | 2 | ||||
-rw-r--r-- | src/util/template.rs | 362 |
9 files changed, 548 insertions, 101 deletions
diff --git a/Cargo.lock b/Cargo.lock index b0020c4669a8..9cffdfec180f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,6 +381,18 @@ dependencies = [ ] [[package]] +name = "lexical-core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "stackvector 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "static_assertions 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "libc" version = "0.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -493,6 +505,16 @@ dependencies = [ ] [[package]] +name = "nom" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lexical-core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "num-integer" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -834,6 +856,14 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "rusty-fork" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -860,6 +890,19 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "serde" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -935,6 +978,20 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "stackvector" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "static_assertions" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1085,6 +1142,14 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "unsafe-any" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1108,6 +1173,11 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "wait-timeout" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1148,6 +1218,7 @@ dependencies = [ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "log4rs 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", "maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "nom 5.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "prettytable-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "proptest 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", "proptest-derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1222,6 +1293,7 @@ dependencies = [ "checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" "checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" +"checksum lexical-core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3f8673fab7063c2cac37d299c8a1a7beb720e78f71500098e4a3c137fdf025bf" "checksum libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "6281b86796ba5e4366000be6e9e18bf35580adf9e63fbe2294aadb587613a319" "checksum linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd" "checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" @@ -1235,6 +1307,7 @@ dependencies = [ "checksum miniz_oxide_c_api 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b7fe927a42e3807ef71defb191dc87d4e24479b221e67015fe38ae2b7b447bab" "checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" "checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +"checksum nom 5.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e9761d859320e381010a4f7f8ed425f2c924de33ad121ace447367c713ad561b" "checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09" "checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" "checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" @@ -1274,10 +1347,13 @@ dependencies = [ "checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5" "checksum rust-ini 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" "checksum rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f4dccf6f4891ebcc0c39f9b6eb1a83b9bf5d747cb439ec6fba4f3b977038af" +"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" "checksum rusty-fork 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3dd93264e10c577503e926bd1430193eeb5d21b059148910082245309b424fae" "checksum ryu 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "b96a9549dc8d48f2c283938303c4b5a77aa29bfbc5b54b084fb1630408899a8f" "checksum ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997" "checksum scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" +"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" "checksum serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)" = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" "checksum serde 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)" = "32746bf0f26eab52f06af0d0aa1984f641341d06d8d673c693871da2d188c9be" "checksum serde-hjson 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0b833c5ad67d52ced5f5938b2980f32a9c1c5ef047f0b4fb3127e7a423c76153" @@ -1287,6 +1363,8 @@ dependencies = [ "checksum serde_test 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)" = "110b3dbdf8607ec493c22d5d947753282f3bae73c0f56d322af1e8c78e4c23d5" "checksum serde_yaml 0.8.9 (registry+https://github.com/rust-lang/crates.io-index)" = "38b08a9a90e5260fe01c6480ec7c811606df6d3a660415808c3c3fa8ed95b582" "checksum spin 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44363f6f51401c34e7be73db0db371c04705d35efbe9f7d6082e03a921a32c55" +"checksum stackvector 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1c4725650978235083241fab0fdc8e694c3de37821524e7534a1a9061d1068af" +"checksum static_assertions 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "c19be23126415861cb3a23e501d34a708f7f9b2183c5252d690941c2e69199d5" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum syn 0.14.9 (registry+https://github.com/rust-lang/crates.io-index)" = "261ae9ecaa397c42b960649561949d69311f08eeaea86a65696e6e46517cf741" "checksum syn 0.15.35 (registry+https://github.com/rust-lang/crates.io-index)" = "641e117d55514d6d918490e47102f7e08d096fdde360247e4a10f7a91a8478d3" @@ -1305,10 +1383,12 @@ dependencies = [ "checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86" "checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" "checksum unsafe-any 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f30360d7979f5e9c6e6cea48af192ea8fab4afb3cf72597154b8f08935bc9c7f" "checksum utf8-ranges 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9d50aa7650df78abf942826607c62468ce18d9019673d4a2ebe1865dbb96ffde" "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" +"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" "checksum wait-timeout 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" "checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" diff --git a/Cargo.toml b/Cargo.toml index 02464410ebe6..3de1dbbe8387 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ lazy_static = "*" log = "*" log4rs = "*" maplit = "^1.0.1" +nom = "^5.0.0" prettytable-rs = "^0.8" proptest = "0.9.3" proptest-derive = "*" diff --git a/src/entities/creature.rs b/src/entities/creature.rs index 55445f951b45..9fd8d23c752e 100644 --- a/src/entities/creature.rs +++ b/src/entities/creature.rs @@ -5,7 +5,7 @@ use crate::entities::{raw, EntityID}; use crate::types::Position; use std::io::{self, Write}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Creature { pub id: Option<EntityID>, pub typ: &'static CreatureType<'static>, diff --git a/src/game.rs b/src/game.rs index 48142b637645..af9b0ac938dd 100644 --- a/src/game.rs +++ b/src/game.rs @@ -8,6 +8,7 @@ use crate::types::{ pos, BoundingBox, Collision, Dimensions, Position, Positioned, PositionedMut, Ticks, }; +use crate::util::template::TemplateParams; use rand::rngs::SmallRng; use rand::SeedableRng; use std::io::{self, StdinLock, StdoutLock, Write}; @@ -145,16 +146,24 @@ impl<'a> Game<'a> { fn tick(&mut self, ticks: Ticks) {} /// Get a message from the global map based on the rng in this game - fn message(&mut self, name: &str) -> &'static str { - message(name, &mut self.rng) + fn message<'params>( + &mut self, + name: &'static str, + params: &TemplateParams<'params>, + ) -> String { + message(name, &mut self.rng, params) } /// Say a message to the user - fn say(&mut self, message_name: &str) -> io::Result<()> { - let message = self.message(message_name); + fn say<'params>( + &mut self, + message_name: &'static str, + params: &TemplateParams<'params>, + ) -> io::Result<()> { + let message = self.message(message_name, params); self.messages.push(message.to_string()); self.message_idx = self.messages.len() - 1; - self.viewport.write_message(message) + self.viewport.write_message(&message) } fn previous_message(&mut self) -> io::Result<()> { @@ -166,20 +175,45 @@ impl<'a> Game<'a> { self.viewport.write_message(message) } + fn creature(&self, creature_id: EntityID) -> Option<&Creature> { + self.entities + .get(creature_id) + .and_then(|e| e.downcast_ref::<Creature>()) + } + + fn expect_creature(&self, creature_id: EntityID) -> &Creature { + self.creature(creature_id).expect( + format!("Creature ID went away: {:?}", creature_id).as_str(), + ) + } + + fn mut_creature(&mut self, creature_id: EntityID) -> Option<&mut Creature> { + self.entities + .get_mut(creature_id) + .and_then(|e| e.downcast_mut::<Creature>()) + } + + fn expect_mut_creature(&mut self, creature_id: EntityID) -> &mut Creature { + self.mut_creature(creature_id).expect( + format!("Creature ID went away: {:?}", creature_id).as_str(), + ) + } + fn attack(&mut self, creature_id: EntityID) -> io::Result<()> { info!("Attacking creature {:?}", creature_id); - self.say("combat.attack")?; let damage = self.character().damage(); - let creature = self - .entities - .get_mut(creature_id) - .and_then(|e| e.downcast_mut::<Creature>()) - .expect( - format!("Creature ID went away: {:?}", creature_id).as_str(), - ); + let creature_name = self.expect_creature(creature_id).typ.name; + let tps = template_params!({ + "creature" => { + "name" => creature_name, + }, + }); + self.say("combat.attack", &tps)?; + + let creature = self.expect_mut_creature(creature_id); creature.damage(damage); if creature.dead() { - self.say("combat.killed")?; + self.say("combat.killed", &tps)?; info!("Killed creature {:?}", creature_id); self.remove_entity(creature_id)?; } @@ -202,7 +236,7 @@ impl<'a> Game<'a> { info!("Running game"); self.viewport.init()?; self.draw_entities()?; - self.say("global.welcome")?; + self.say("global.welcome", &template_params!())?; self.flush()?; loop { let mut old_position = None; diff --git a/src/main.rs b/src/main.rs index 636663e05045..8bad5c057fd3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,8 @@ extern crate downcast_rs; extern crate backtrace; #[macro_use] extern crate include_dir; +#[macro_use] +extern crate nom; #[macro_use] mod util; diff --git a/src/messages.rs b/src/messages.rs index 948787f1393b..aa0366e786ca 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -1,32 +1,33 @@ +use crate::util::template::Template; +use crate::util::template::TemplateParams; use rand::seq::SliceRandom; use rand::Rng; -use serde::de::MapAccess; -use serde::de::SeqAccess; -use serde::de::Visitor; use std::collections::HashMap; -use std::fmt; -use std::marker::PhantomData; #[derive(Deserialize, Debug, PartialEq, Eq)] #[serde(untagged)] enum Message<'a> { - Single(&'a str), - Choice(Vec<&'a str>), + #[serde(borrow)] + Single(Template<'a>), + Choice(Vec<Template<'a>>), } impl<'a> Message<'a> { - fn resolve<R: Rng + ?Sized>(&self, rng: &mut R) -> Option<&'a str> { + fn resolve<R: Rng + ?Sized>(&self, rng: &mut R) -> Option<&Template<'a>> { use Message::*; match self { - Single(msg) => Some(*msg), - Choice(msgs) => msgs.choose(rng).map(|msg| *msg), + Single(msg) => Some(msg), + Choice(msgs) => msgs.choose(rng), } } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Deserialize, Debug, PartialEq, Eq)] +#[serde(untagged)] enum NestedMap<'a> { + #[serde(borrow)] Direct(Message<'a>), + #[serde(borrow)] Nested(HashMap<&'a str, NestedMap<'a>>), } @@ -46,63 +47,6 @@ impl<'a> NestedMap<'a> { } } -struct NestedMapVisitor<'a> { - marker: PhantomData<fn() -> NestedMap<'a>>, -} - -impl<'a> NestedMapVisitor<'a> { - fn new() -> Self { - NestedMapVisitor { - marker: PhantomData, - } - } -} - -impl<'de> Visitor<'de> for NestedMapVisitor<'de> { - type Value = NestedMap<'de>; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str( - "A message, a list of messages, or a nested map of messages", - ) - } - - fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E> { - Ok(NestedMap::Direct(Message::Single(v))) - } - - fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> - where - A: SeqAccess<'de>, - { - let mut choices = Vec::with_capacity(seq.size_hint().unwrap_or(0)); - while let Some(choice) = seq.next_element()? { - choices.push(choice); - } - Ok(NestedMap::Direct(Message::Choice(choices))) - } - - fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> - where - A: MapAccess<'de>, - { - let mut nested = HashMap::with_capacity(map.size_hint().unwrap_or(0)); - while let Some((k, v)) = map.next_entry()? { - nested.insert(k, v); - } - Ok(NestedMap::Nested(nested)) - } -} - -impl<'de> serde::Deserialize<'de> for NestedMap<'de> { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_any(NestedMapVisitor::new()) - } -} - #[cfg(test)] mod tests { use super::*; @@ -122,13 +66,18 @@ choice = ["Say this", "Or this"] result, Ok(NestedMap::Nested(hashmap! { "global" => NestedMap::Nested(hashmap!{ - "hello" => NestedMap::Direct(Message::Single("Hello World!")), + "hello" => NestedMap::Direct(Message::Single(Template::parse("Hello World!").unwrap())), }), "foo" => NestedMap::Nested(hashmap!{ "bar" => NestedMap::Nested(hashmap!{ - "single" => NestedMap::Direct(Message::Single("Single")), + "single" => NestedMap::Direct(Message::Single( + Template::parse("Single").unwrap() + )), "choice" => NestedMap::Direct(Message::Choice( - vec!["Say this", "Or this"] + vec![ + Template::parse("Say this").unwrap(), + Template::parse("Or this").unwrap() + ] )) }) }) @@ -152,31 +101,43 @@ choice = ["Say this", "Or this"] assert_eq!( map.lookup("global.hello"), - Some(&Message::Single("Hello World!")) + Some(&Message::Single(Template::parse("Hello World!").unwrap())) ); assert_eq!( map.lookup("foo.bar.single"), - Some(&Message::Single("Single")) + Some(&Message::Single(Template::parse("Single").unwrap())) ); assert_eq!( map.lookup("foo.bar.choice"), - Some(&Message::Choice(vec!["Say this", "Or this"])) + Some(&Message::Choice(vec![ + Template::parse("Say this").unwrap(), + Template::parse("Or this").unwrap() + ])) ); } } +// static MESSAGES_RAW: &'static str = include_str!("messages.toml"); + static_cfg! { static ref MESSAGES: NestedMap<'static> = toml_file("messages.toml"); } -/// Look up a game message based on the given (dot-separated) name, with the -/// given random generator used to select from choice-based messages -pub fn message<R: Rng + ?Sized>(name: &str, rng: &mut R) -> &'static str { - MESSAGES - .lookup(name) - .and_then(|msg| msg.resolve(rng)) - .unwrap_or_else(|| { +/// Look up and format a game message based on the given (dot-separated) name, +/// with the given random generator used to select from choice-based messages +pub fn message<'a, R: Rng + ?Sized>( + name: &'static str, + rng: &mut R, + params: &TemplateParams<'a>, +) -> String { + match MESSAGES.lookup(name).and_then(|msg| msg.resolve(rng)) { + Some(msg) => msg.format(params).unwrap_or_else(|e| { + error!("Error formatting template: {}", e); + "Template Error".to_string() + }), + None => { error!("Message not found: {}", name); - "Message not found" - }) + "Template Not Found".to_string() + } + } } diff --git a/src/messages.toml b/src/messages.toml index a6b795d97ec7..d3e0e1de8a23 100644 --- a/src/messages.toml +++ b/src/messages.toml @@ -2,5 +2,10 @@ welcome = "Welcome to Xanthous! It's dangerous out there, why not stay inside?" [combat] -attack = "You attack the {{creature_name}}." -killed = "You killed the {{creature_name}}." +attack = "You attack the {{creature.name}}." +killed = [ + "You've killed the {{creature.name}}.", + "The {{creature.name}} dies.", + "The {{creature.name}} kicks it.", + "The {{creature.name}} beefs it." + ] diff --git a/src/util/mod.rs b/src/util/mod.rs index 87fd7910f3ec..c2b4eecaf5f4 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,2 +1,4 @@ #[macro_use] pub mod static_cfg; +#[macro_use] +pub mod template; diff --git a/src/util/template.rs b/src/util/template.rs new file mode 100644 index 000000000000..a3faadc31c91 --- /dev/null +++ b/src/util/template.rs @@ -0,0 +1,362 @@ +use nom::combinator::rest; +use nom::error::ErrorKind; +use nom::{Err, IResult}; +use std::collections::HashMap; +use std::fmt::{self, Display}; +use std::marker::PhantomData; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Path<'a> { + head: &'a str, + tail: Vec<&'a str>, +} + +impl<'a> Path<'a> { + fn new(head: &'a str, tail: Vec<&'a str>) -> Self { + Path { head, tail } + } +} + +impl<'a> Display for Path<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.head)?; + for part in &self.tail { + write!(f, ".{}", part)?; + } + Ok(()) + } +} + +// named!(path_ident, map_res!(is_not!(".}"), std::str::from_utf8)); +fn path_ident<'a>(input: &'a str) -> IResult<&'a str, &'a str> { + take_till!(input, |c| c == '.' || c == '}') +} + +fn path<'a>(input: &'a str) -> IResult<&'a str, Path<'a>> { + map!( + input, + tuple!( + path_ident, + many0!(complete!(preceded!(char!('.'), path_ident))) + ), + |(h, t)| Path::new(h, t) + ) +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum TemplateToken<'a> { + Literal(&'a str), + Substitution(Path<'a>), +} + +fn token_substitution<'a>( + input: &'a str, +) -> IResult<&'a str, TemplateToken<'a>> { + map!( + input, + delimited!(tag!("{{"), path, tag!("}}")), + TemplateToken::Substitution + ) +} + +fn template_token<'a>(input: &'a str) -> IResult<&'a str, TemplateToken<'a>> { + alt!( + input, + token_substitution + | map!( + alt!(complete!(take_until!("{{")) | complete!(rest)), + TemplateToken::Literal + ) + ) +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Template<'a> { + tokens: Vec<TemplateToken<'a>>, +} + +impl<'a> Template<'a> { + pub fn new(tokens: Vec<TemplateToken<'a>>) -> Self { + Template { tokens } + } +} + +pub struct TemplateVisitor<'a> { + marker: PhantomData<fn() -> Template<'a>>, +} + +impl<'a> TemplateVisitor<'a> { + pub fn new() -> Self { + TemplateVisitor { + marker: PhantomData, + } + } +} + +impl<'a> serde::de::Visitor<'a> for TemplateVisitor<'a> { + type Value = Template<'a>; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a valid template string") + } + + fn visit_borrowed_str<E: serde::de::Error>( + self, + v: &'a str, + ) -> Result<Self::Value, E> { + Template::parse(v).map_err(|_| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Str(v), + &"a valid template string", + ) + }) + } +} + +impl<'a> serde::Deserialize<'a> for Template<'a> { + fn deserialize<D: serde::Deserializer<'a>>( + deserializer: D, + ) -> Result<Self, D::Error> { + deserializer.deserialize_str(TemplateVisitor::new()) + } +} + +impl<'a> Template<'a> { + pub fn parse( + input: &'a str, + ) -> Result<Template<'a>, Err<(&'a str, ErrorKind)>> { + let (remaining, res) = template(input)?; + if remaining.len() > 0 { + unreachable!(); + } + Ok(res) + } + + pub fn format( + &self, + params: &TemplateParams<'a>, + ) -> Result<String, TemplateError<'a>> { + use TemplateToken::*; + let mut res = String::new(); + for token in &self.tokens { + match token { + Literal(s) => res.push_str(s), + Substitution(p) => match params.get(p.clone()) { + Some(s) => res.push_str(s), + None => return Err(TemplateError::MissingParam(p.clone())), + }, + } + } + Ok(res) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum TemplateError<'a> { + MissingParam(Path<'a>), +} + +impl<'a> Display for TemplateError<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use TemplateError::*; + match self { + MissingParam(path) => { + write!(f, "Missing template parameter: {}", path) + } + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum TemplateParams<'a> { + Direct(&'a str), + Nested(HashMap<&'a str, TemplateParams<'a>>), +} + +impl<'a> TemplateParams<'a> { + fn get(&self, path: Path<'a>) -> Option<&'a str> { + use TemplateParams::*; + match self { + Direct(_) => None, + Nested(m) => m.get(path.head).and_then(|next| { + if path.tail.len() == 0 { + match next { + Direct(s) => Some(*s), + _ => None, + } + } else { + next.get(Path { + head: path.tail[0], + tail: path.tail[1..].to_vec(), + }) + } + }), + } + } +} + +#[macro_export] +macro_rules! template_params { + (@count $head: expr => $hv: tt, $($rest:tt)+) => { 1 + template_params!(@count $($rest)+) }; + (@count $one:expr => $($ov: tt)*) => { 1 }; + (@inner $ret: ident, ($key: expr => {$($v:tt)*}, $($r:tt)*)) => { + $ret.insert($key, template_params!({ $($v)* })); + template_params!(@inner $ret, ($($r)*)); + }; + (@inner $ret: ident, ($key: expr => $value: expr, $($r:tt)*)) => { + $ret.insert($key, template_params!($value)); + template_params!(@inner $ret, ($($r)*)); + }; + (@inner $ret: ident, ()) => {}; + + ({ $($body: tt)* }) => {{ + let _cap = template_params!(@count $($body)*); + let mut _m = ::std::collections::HashMap::with_capacity(_cap); + template_params!(@inner _m, ($($body)*)); + TemplateParams::Nested(_m) + }}; + + ($direct:expr) => { TemplateParams::Direct($direct) }; + + () => { TemplateParams::Nested(::std::collections::HashMap::new()) }; +} + +fn template<'a>(input: &'a str) -> IResult<&'a str, Template<'a>> { + complete!( + input, + map!(many1!(complete!(template_token)), Template::new) + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_path_ident() { + assert_eq!(path_ident("foo}}"), Ok(("}}", "foo"))); + assert_eq!(path_ident("foo.bar}}"), Ok((".bar}}", "foo"))); + } + + #[test] + fn test_parse_path() { + assert_eq!(path("foo}}"), Ok(("}}", Path::new("foo", vec![])))); + assert_eq!( + path("foo.bar}}"), + Ok(("}}", Path::new("foo", vec!["bar"]))) + ); + assert_eq!( + path("foo.bar.baz}}"), + Ok(("}}", Path::new("foo", vec!["bar", "baz"]))) + ); + } + + #[test] + fn test_parse_template_token() { + assert_eq!( + template_token("foo bar"), + Ok(("", TemplateToken::Literal("foo bar"))) + ); + + assert_eq!( + template_token("foo bar {{baz}}"), + Ok(("{{baz}}", TemplateToken::Literal("foo bar "))) + ); + + assert_eq!( + template_token("{{baz}}"), + Ok(( + "", + TemplateToken::Substitution(Path::new("baz", Vec::new())) + )) + ); + + assert_eq!( + template_token("{{baz}} foo bar"), + Ok(( + " foo bar", + TemplateToken::Substitution(Path::new("baz", Vec::new())) + )) + ); + } + + #[test] + fn test_parse_template() { + assert_eq!( + template("foo bar"), + Ok(( + "", + Template { + tokens: vec![TemplateToken::Literal("foo bar")] + } + )) + ); + + assert_eq!( + template("foo bar {{baz}} qux"), + Ok(( + "", + Template { + tokens: vec![ + TemplateToken::Literal("foo bar "), + TemplateToken::Substitution(Path::new( + "baz", + Vec::new() + )), + TemplateToken::Literal(" qux"), + ] + } + )) + ); + } + + #[test] + fn test_template_params_literal() { + // trace_macros!(true); + let expected = template_params!({ + "direct" => "hi", + "other" => "here", + "nested" => { + "one" => "1", + "two" => "2", + "double" => { + "three" => "3", + }, + }, + }); + // trace_macros!(false); + assert_eq!( + TemplateParams::Nested(hashmap! { + "direct" => TemplateParams::Direct("hi"), + "other" => TemplateParams::Direct("here"), + "nested" => TemplateParams::Nested(hashmap!{ + "one" => TemplateParams::Direct("1"), + "two" => TemplateParams::Direct("2"), + "double" => TemplateParams::Nested(hashmap!{ + "three" => TemplateParams::Direct("3"), + }) + }) + }), + expected, + ) + } + + #[test] + fn test_format_template() { + assert_eq!( + "foo bar baz qux", + Template::parse("foo {{x}} {{y.z}} {{y.w.z}}") + .unwrap() + .format(&template_params!({ + "x" => "bar", + "y" => { + "z" => "baz", + "w" => { + "z" => "qux", + }, + }, + })) + .unwrap() + ) + } +} |