Add templates for messages
Implement a template syntax with a nom parser, and a formatter to render templates to strings.
This commit is contained in:
parent
bc93999cf3
commit
e2d13bd76b
9 changed files with 549 additions and 102 deletions
80
Cargo.lock
generated
80
Cargo.lock
generated
|
@ -380,6 +380,18 @@ dependencies = [
|
|||
"spin 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
@ -492,6 +504,16 @@ dependencies = [
|
|||
"version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
@ -833,6 +855,14 @@ name = "rustc-demangle"
|
|||
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"
|
||||
|
@ -859,6 +889,19 @@ name = "scoped_threadpool"
|
|||
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"
|
||||
|
@ -934,6 +977,20 @@ name = "spin"
|
|||
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"
|
||||
|
@ -1084,6 +1141,14 @@ name = "unicode-xid"
|
|||
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"
|
||||
|
@ -1107,6 +1172,11 @@ name = "version_check"
|
|||
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"
|
||||
|
@ -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"
|
||||
|
|
|
@ -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 = "*"
|
||||
|
|
|
@ -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>,
|
||||
|
|
66
src/game.rs
66
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 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
|
||||
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>())
|
||||
.expect(
|
||||
format!("Creature ID went away: {:?}", creature_id).as_str(),
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
let damage = self.character().damage();
|
||||
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;
|
||||
|
|
|
@ -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;
|
||||
|
|
127
src/messages.rs
127
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
]
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
#[macro_use]
|
||||
pub mod static_cfg;
|
||||
#[macro_use]
|
||||
pub mod template;
|
||||
|
|
362
src/util/template.rs
Normal file
362
src/util/template.rs
Normal file
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue