about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorGriffin Smith <root@gws.fyi>2019-07-19T15·54-0400
committerGriffin Smith <root@gws.fyi>2019-07-19T15·54-0400
commite2d13bd76b9af9cc2734cdcb9df605afa95cca31 (patch)
tree8a720b216dd6481fcda491ffcb082d989e2d15c6 /src
parentbc93999cf37a65d48f25e30795c85a0aef97efac (diff)
Add templates for messages
Implement a template syntax with a nom parser, and a formatter to render
templates to strings.
Diffstat (limited to 'src')
-rw-r--r--src/entities/creature.rs2
-rw-r--r--src/game.rs64
-rw-r--r--src/main.rs2
-rw-r--r--src/messages.rs127
-rw-r--r--src/messages.toml9
-rw-r--r--src/util/mod.rs2
-rw-r--r--src/util/template.rs362
7 files changed, 467 insertions, 101 deletions
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()
+        )
+    }
+}