Add a generic text-prompt system
Add a generic text-prompt system to the Game, and use it to prompt the character for their name on startup. There's also a Promise type in util, which is used for the result of the prompt.
This commit is contained in:
parent
68e8ad8a0e
commit
f22bcad817
11 changed files with 488 additions and 54 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -310,6 +310,11 @@ name = "fuchsia-cprng"
|
|||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.6"
|
||||
|
@ -1217,6 +1222,7 @@ dependencies = [
|
|||
"clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"config 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"downcast-rs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"include_dir 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -1291,6 +1297,7 @@ dependencies = [
|
|||
"checksum flate2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f87e68aa82b2de08a6e037f1385455759df6e445a8df5e005b4297191dbf18aa"
|
||||
"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
|
||||
"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
|
||||
"checksum futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "45dc39533a6cae6da2b56da48edae506bb767ec07370f86f70fc062e9d435869"
|
||||
"checksum getrandom 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "e65cce4e5084b14874c4e7097f38cab54f47ee554f9194673456ea379dcc4c55"
|
||||
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
|
||||
"checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114"
|
||||
|
|
|
@ -9,13 +9,14 @@ backtrace = "0.3"
|
|||
clap = {version = "^2.33.0", features = ["yaml"]}
|
||||
config = "*"
|
||||
downcast-rs = "^1.0.4"
|
||||
futures = "0.1.28"
|
||||
include_dir = "0.2.1"
|
||||
itertools = "*"
|
||||
lazy_static = "*"
|
||||
log = "*"
|
||||
log4rs = "*"
|
||||
matches = "0.1.8"
|
||||
maplit = "^1.0.1"
|
||||
matches = "0.1.8"
|
||||
nom = "^5.0.0"
|
||||
prettytable-rs = "^0.8"
|
||||
proptest = "0.9.3"
|
||||
|
|
|
@ -92,7 +92,6 @@ impl<'de> Visitor<'de> for ColorVisitor {
|
|||
Ok(Color(Box::new(color::LightYellow)))
|
||||
}
|
||||
"magenta" => Ok(Color(Box::new(color::Magenta))),
|
||||
"magenta" => Ok(Color(Box::new(color::Magenta))),
|
||||
"red" => Ok(Color(Box::new(color::Red))),
|
||||
"white" => Ok(Color(Box::new(color::White))),
|
||||
"yellow" => Ok(Color(Box::new(color::Yellow))),
|
||||
|
|
|
@ -2,10 +2,21 @@ use super::BoxStyle;
|
|||
use super::Draw;
|
||||
use crate::display::draw_box::draw_box;
|
||||
use crate::display::utils::clone_times;
|
||||
use crate::types::{pos, BoundingBox, Position, Positioned};
|
||||
use crate::types::{pos, BoundingBox, Direction, Position, Positioned};
|
||||
use std::fmt::{self, Debug};
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub enum CursorState {
|
||||
Game,
|
||||
Prompt(Position),
|
||||
}
|
||||
|
||||
impl Default for CursorState {
|
||||
fn default() -> Self {
|
||||
CursorState::Game
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Viewport<W> {
|
||||
/// The box describing the visible part of the viewport.
|
||||
///
|
||||
|
@ -24,9 +35,12 @@ pub struct Viewport<W> {
|
|||
/// The actual screen that the viewport writes to
|
||||
pub out: W,
|
||||
|
||||
cursor_state: CursorState,
|
||||
|
||||
/// Reset the cursor back to this position after every draw
|
||||
pub cursor_position: Position,
|
||||
pub game_cursor_position: Position,
|
||||
}
|
||||
|
||||
impl<W> Viewport<W> {
|
||||
pub fn new(outer: BoundingBox, inner: BoundingBox, out: W) -> Self {
|
||||
Viewport {
|
||||
|
@ -34,7 +48,8 @@ impl<W> Viewport<W> {
|
|||
inner,
|
||||
out,
|
||||
game: outer.move_tr_corner(Position { x: 0, y: 1 }),
|
||||
cursor_position: pos(0, 0),
|
||||
cursor_state: Default::default(),
|
||||
game_cursor_position: pos(0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,7 +87,7 @@ impl<W: Write> Viewport<W> {
|
|||
}
|
||||
|
||||
fn reset_cursor(&mut self) -> io::Result<()> {
|
||||
self.cursor_goto(self.cursor_position)
|
||||
self.cursor_goto(self.game_cursor_position)
|
||||
}
|
||||
|
||||
/// Move the cursor to the given inner-relative position
|
||||
|
@ -97,23 +112,85 @@ impl<W: Write> Viewport<W> {
|
|||
/// Will overwrite any message already present, and if the given message is
|
||||
/// longer than the screen will truncate. This means callers should handle
|
||||
/// message buffering and ellipsisization
|
||||
pub fn write_message(&mut self, msg: &str) -> io::Result<()> {
|
||||
pub fn write_message(&mut self, msg: &str) -> io::Result<usize> {
|
||||
let msg_to_write = if msg.len() <= self.outer.dimensions.w as usize {
|
||||
msg
|
||||
} else {
|
||||
&msg[0..self.outer.dimensions.w as usize]
|
||||
};
|
||||
write!(
|
||||
self,
|
||||
"{}{}{}",
|
||||
self.outer.position.cursor_goto(),
|
||||
if msg.len() <= self.outer.dimensions.w as usize {
|
||||
msg
|
||||
} else {
|
||||
&msg[0..self.outer.dimensions.w as usize]
|
||||
},
|
||||
msg_to_write,
|
||||
clone_times::<_, String>(
|
||||
" ".to_string(),
|
||||
self.outer.dimensions.w - msg.len() as u16
|
||||
),
|
||||
)?;
|
||||
self.reset_cursor()?;
|
||||
Ok(msg_to_write.len())
|
||||
}
|
||||
|
||||
pub fn clear_message(&mut self) -> io::Result<()> {
|
||||
write!(
|
||||
self,
|
||||
"{}{}",
|
||||
self.outer.position.cursor_goto(),
|
||||
clone_times::<_, String>(
|
||||
" ".to_string(),
|
||||
self.outer.dimensions.w as u16
|
||||
)
|
||||
)?;
|
||||
self.reset_cursor()
|
||||
}
|
||||
|
||||
/// Write a prompt requesting text input to the message area on the screen.
|
||||
///
|
||||
/// Will overwrite any message already present, and if the given message is
|
||||
/// longer than the screen will truncate. This means callers should handle
|
||||
/// message buffering and ellipsisization
|
||||
pub fn write_prompt<'a, 'b>(&'a mut self, msg: &'b str) -> io::Result<()> {
|
||||
let len = self.write_message(msg)? + 1;
|
||||
let pos = self.outer.position + pos(len as i16, 0);
|
||||
self.cursor_state = CursorState::Prompt(pos);
|
||||
write!(self, "{}", pos.cursor_goto())?;
|
||||
self.flush()
|
||||
}
|
||||
|
||||
pub fn push_prompt_chr(&mut self, chr: char) -> io::Result<()> {
|
||||
match self.cursor_state {
|
||||
CursorState::Prompt(pos) => {
|
||||
write!(self, "{}", chr)?;
|
||||
self.cursor_state = CursorState::Prompt(pos + Direction::Right);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pop_prompt_chr(&mut self) -> io::Result<()> {
|
||||
match self.cursor_state {
|
||||
CursorState::Prompt(pos) => {
|
||||
let new_pos = pos + Direction::Left;
|
||||
write!(
|
||||
self,
|
||||
"{} {}",
|
||||
new_pos.cursor_goto(),
|
||||
new_pos.cursor_goto()
|
||||
)?;
|
||||
self.cursor_state = CursorState::Prompt(new_pos);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_prompt(&mut self) -> io::Result<()> {
|
||||
self.clear_message()?;
|
||||
self.cursor_state = CursorState::Game;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Positioned for Viewport<W> {
|
||||
|
|
|
@ -13,6 +13,8 @@ pub struct Character {
|
|||
|
||||
/// The position of the character, relative to the game
|
||||
pub position: Position,
|
||||
|
||||
pub o_name: Option<String>,
|
||||
}
|
||||
|
||||
impl Character {
|
||||
|
@ -20,6 +22,7 @@ impl Character {
|
|||
Character {
|
||||
id: None,
|
||||
position: Position { x: 0, y: 0 },
|
||||
o_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,6 +34,16 @@ impl Character {
|
|||
// TODO
|
||||
1
|
||||
}
|
||||
|
||||
pub fn name<'a>(&'a self) -> &'a str {
|
||||
self.o_name
|
||||
.as_ref()
|
||||
.expect("Character name not initialized")
|
||||
}
|
||||
|
||||
pub fn set_name(&mut self, name: String) {
|
||||
self.o_name = Some(name);
|
||||
}
|
||||
}
|
||||
|
||||
entity!(Character);
|
||||
|
|
251
src/game.rs
251
src/game.rs
|
@ -10,6 +10,8 @@ use crate::types::{
|
|||
pos, BoundingBox, Collision, Dimensions, Position, Positioned,
|
||||
PositionedMut, Ticks,
|
||||
};
|
||||
use crate::util::promise::Cancelled;
|
||||
use crate::util::promise::{promise, Complete, Promise, Promises};
|
||||
use crate::util::template::TemplateParams;
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::SeedableRng;
|
||||
|
@ -36,6 +38,75 @@ impl<'a> PositionedMut for AnEntity<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
enum PromptResolution {
|
||||
Uncancellable(Complete<String>),
|
||||
Cancellable(Complete<Result<String, Cancelled>>),
|
||||
}
|
||||
|
||||
impl PromptResolution {
|
||||
fn is_cancellable(&self) -> bool {
|
||||
use PromptResolution::*;
|
||||
match self {
|
||||
Uncancellable(_) => false,
|
||||
Cancellable(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn fulfill(&mut self, val: String) {
|
||||
use PromptResolution::*;
|
||||
match self {
|
||||
Cancellable(complete) => complete.ok(val),
|
||||
Uncancellable(complete) => complete.fulfill(val),
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&mut self) {
|
||||
use PromptResolution::*;
|
||||
match self {
|
||||
Cancellable(complete) => complete.cancel(),
|
||||
Uncancellable(complete) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of input the game is waiting to receive
|
||||
enum InputState {
|
||||
/// The initial input state of the game - we're currently waiting for direct
|
||||
/// commands.
|
||||
Initial,
|
||||
|
||||
/// A free text prompt has been shown to the user, and every character
|
||||
/// besides "escape" is interpreted as a response to that prompt
|
||||
Prompt {
|
||||
complete: PromptResolution,
|
||||
buffer: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
fn uncancellable_prompt(complete: Complete<String>) -> Self {
|
||||
InputState::Prompt {
|
||||
complete: PromptResolution::Uncancellable(complete),
|
||||
buffer: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cancellable_prompt(
|
||||
complete: Complete<Result<String, Cancelled>>,
|
||||
) -> Self {
|
||||
InputState::Prompt {
|
||||
complete: PromptResolution::Cancellable(complete),
|
||||
buffer: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InputState {
|
||||
fn default() -> Self {
|
||||
InputState::Initial
|
||||
}
|
||||
}
|
||||
|
||||
/// The full state of a running Game
|
||||
pub struct Game<'a> {
|
||||
settings: Settings,
|
||||
|
@ -45,6 +116,9 @@ pub struct Game<'a> {
|
|||
/// An iterator on keypresses from the user
|
||||
keys: Keys<StdinLock<'a>>,
|
||||
|
||||
/// The kind of input the game is waiting to receive
|
||||
input_state: InputState,
|
||||
|
||||
/// The map of all the entities in the game
|
||||
entities: EntityMap<AnEntity<'a>>,
|
||||
|
||||
|
@ -60,6 +134,9 @@ pub struct Game<'a> {
|
|||
|
||||
/// A global random number generator for the game
|
||||
rng: Rng,
|
||||
|
||||
/// A list of promises that are waiting on the game and a result
|
||||
promises: Promises<'a, Self>,
|
||||
}
|
||||
|
||||
impl<'a> Game<'a> {
|
||||
|
@ -97,9 +174,11 @@ impl<'a> Game<'a> {
|
|||
stdout,
|
||||
),
|
||||
keys: stdin.keys(),
|
||||
input_state: Default::default(),
|
||||
character_entity_id: entities.insert(Box::new(Character::new())),
|
||||
messages: Vec::new(),
|
||||
entities,
|
||||
promises: Promises::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,6 +210,12 @@ impl<'a> Game<'a> {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
fn mut_character(&mut self) -> &mut Character {
|
||||
(*self.entities.get_mut(self.character_entity_id).unwrap())
|
||||
.downcast_mut()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Draw all the game entities to the screen
|
||||
fn draw_entities(&mut self) -> io::Result<()> {
|
||||
for entity in self.entities.entities() {
|
||||
|
@ -168,7 +253,36 @@ impl<'a> Game<'a> {
|
|||
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)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prompt the user for input, returning a Future for the result of the
|
||||
/// prompt
|
||||
fn prompt(
|
||||
&mut self,
|
||||
name: &'static str,
|
||||
params: &TemplateParams<'_>,
|
||||
) -> io::Result<Promise<Self, String>> {
|
||||
let (complete, promise) = promise();
|
||||
self.input_state = InputState::uncancellable_prompt(complete);
|
||||
let message = self.message(name, params);
|
||||
self.viewport.write_prompt(&message)?;
|
||||
self.promises.push(Box::new(promise.clone()));
|
||||
Ok(promise)
|
||||
}
|
||||
|
||||
fn prompt_cancellable(
|
||||
&mut self,
|
||||
name: &'static str,
|
||||
params: &TemplateParams<'_>,
|
||||
) -> io::Result<Promise<Self, Result<String, Cancelled>>> {
|
||||
let (complete, promise) = promise();
|
||||
self.input_state = InputState::cancellable_prompt(complete);
|
||||
let message = self.message(name, params);
|
||||
self.viewport.write_prompt(&message)?;
|
||||
self.promises.push(Box::new(promise.clone()));
|
||||
Ok(promise)
|
||||
}
|
||||
|
||||
fn previous_message(&mut self) -> io::Result<()> {
|
||||
|
@ -177,7 +291,8 @@ impl<'a> Game<'a> {
|
|||
}
|
||||
self.message_idx -= 1;
|
||||
let message = &self.messages[self.message_idx];
|
||||
self.viewport.write_message(message)
|
||||
self.viewport.write_message(message)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn creature(&self, creature_id: EntityID) -> Option<&Creature> {
|
||||
|
@ -236,60 +351,116 @@ impl<'a> Game<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn flush_promises(&mut self) {
|
||||
unsafe {
|
||||
let game = self as *mut Self;
|
||||
(*game).promises.give_all(&mut *game);
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the game
|
||||
pub fn run(mut self) -> io::Result<()> {
|
||||
info!("Running game");
|
||||
self.viewport.init()?;
|
||||
self.draw_entities()?;
|
||||
self.say("global.welcome", &template_params!())?;
|
||||
self.flush()?;
|
||||
self.flush().unwrap();
|
||||
|
||||
self.prompt("character.name_prompt", &template_params!())?
|
||||
.on_fulfill(|game, char_name| {
|
||||
game.say(
|
||||
"global.welcome",
|
||||
&template_params!({
|
||||
"character" => {
|
||||
"name" => char_name,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
game.flush().unwrap();
|
||||
game.mut_character().set_name(char_name.to_string());
|
||||
});
|
||||
|
||||
loop {
|
||||
let mut old_position = None;
|
||||
use Command::*;
|
||||
match Command::from_key(self.keys.next().unwrap().unwrap()) {
|
||||
Some(Quit) => {
|
||||
info!("Quitting game due to user request");
|
||||
break;
|
||||
}
|
||||
let next_key = self.keys.next().unwrap().unwrap();
|
||||
match &mut self.input_state {
|
||||
InputState::Initial => {
|
||||
use Command::*;
|
||||
match Command::from_key(next_key) {
|
||||
Some(Quit) => {
|
||||
info!("Quitting game due to user request");
|
||||
break;
|
||||
}
|
||||
|
||||
Some(Move(direction)) => {
|
||||
use Collision::*;
|
||||
let new_pos = self.character().position + direction;
|
||||
match self.collision_at(new_pos) {
|
||||
None => {
|
||||
old_position = Some(self.character().position);
|
||||
self.entities.update_position(
|
||||
self.character_entity_id,
|
||||
new_pos,
|
||||
Some(Move(direction)) => {
|
||||
use Collision::*;
|
||||
let new_pos = self.character().position + direction;
|
||||
match self.collision_at(new_pos) {
|
||||
None => {
|
||||
old_position =
|
||||
Some(self.character().position);
|
||||
self.entities.update_position(
|
||||
self.character_entity_id,
|
||||
new_pos,
|
||||
);
|
||||
}
|
||||
Some(Combat) => {
|
||||
self.attack_at(new_pos)?;
|
||||
}
|
||||
Some(Stop) => (),
|
||||
}
|
||||
}
|
||||
|
||||
Some(PreviousMessage) => self.previous_message()?,
|
||||
|
||||
None => (),
|
||||
}
|
||||
|
||||
match old_position {
|
||||
Some(old_pos) => {
|
||||
self.tick(
|
||||
self.character().speed().tiles_to_ticks(
|
||||
(old_pos - self.character().position)
|
||||
.as_tiles(),
|
||||
),
|
||||
);
|
||||
self.viewport.clear(old_pos)?;
|
||||
self.viewport.game_cursor_position =
|
||||
self.character().position;
|
||||
self.viewport.draw(
|
||||
// TODO this clone feels unnecessary.
|
||||
&self.character().clone(),
|
||||
)?;
|
||||
}
|
||||
Some(Combat) => {
|
||||
self.attack_at(new_pos)?;
|
||||
}
|
||||
Some(Stop) => (),
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
|
||||
Some(PreviousMessage) => self.previous_message()?,
|
||||
|
||||
None => (),
|
||||
}
|
||||
|
||||
match old_position {
|
||||
Some(old_pos) => {
|
||||
self.tick(self.character().speed().tiles_to_ticks(
|
||||
(old_pos - self.character().position).as_tiles(),
|
||||
));
|
||||
self.viewport.clear(old_pos)?;
|
||||
self.viewport.cursor_position = self.character().position;
|
||||
self.viewport.draw(
|
||||
// TODO this clone feels unnecessary.
|
||||
&self.character().clone(),
|
||||
)?;
|
||||
InputState::Prompt { complete, buffer } => {
|
||||
use termion::event::Key::*;
|
||||
match next_key {
|
||||
Char('\n') => {
|
||||
info!("Prompt complete: \"{}\"", buffer);
|
||||
self.viewport.clear_prompt()?;
|
||||
complete.fulfill(buffer.clone());
|
||||
self.input_state = InputState::Initial;
|
||||
}
|
||||
Char(chr) => {
|
||||
buffer.push(chr);
|
||||
self.viewport.push_prompt_chr(chr)?;
|
||||
}
|
||||
Esc => complete.cancel(),
|
||||
Backspace => {
|
||||
buffer.pop();
|
||||
self.viewport.pop_prompt_chr()?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
|
||||
self.flush()?;
|
||||
self.flush_promises();
|
||||
debug!("{:?}", self.character());
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
@ -13,6 +13,8 @@ pub fn falses(dims: &Dimensions) -> Vec<Vec<bool>> {
|
|||
ret
|
||||
}
|
||||
|
||||
/// Randomly initialize a 2-dimensional boolean vector of the given
|
||||
/// `Dimensions`, using the given random number generator and alive chance
|
||||
pub fn rand_initialize<R: Rng + ?Sized>(
|
||||
dims: &Dimensions,
|
||||
rng: &mut R,
|
||||
|
|
|
@ -26,6 +26,7 @@ extern crate include_dir;
|
|||
extern crate nom;
|
||||
#[macro_use]
|
||||
extern crate matches;
|
||||
extern crate futures;
|
||||
|
||||
#[macro_use]
|
||||
mod util;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[global]
|
||||
welcome = "Welcome to Xanthous! It's dangerous out there, why not stay inside?"
|
||||
welcome = "Welcome to Xanthous, {{character.name}}! It's dangerous out there, why not stay inside?"
|
||||
|
||||
[combat]
|
||||
attack = "You attack the {{creature.name}}."
|
||||
|
@ -10,5 +10,8 @@ killed = [
|
|||
"The {{creature.name}} beefs it."
|
||||
]
|
||||
|
||||
[character]
|
||||
name_prompt = "What's your name?"
|
||||
|
||||
[defaults.item]
|
||||
eat = "You eat the {{item.name}}"
|
||||
eat = "You eat the {{item.name}}. {{action.result}}"
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
pub mod static_cfg;
|
||||
#[macro_use]
|
||||
pub mod template;
|
||||
pub mod promise;
|
||||
|
|
159
src/util/promise.rs
Normal file
159
src/util/promise.rs
Normal file
|
@ -0,0 +1,159 @@
|
|||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::task::{Context, Poll, Waker};
|
||||
|
||||
pub struct Promise<Env, T> {
|
||||
inner: Arc<RwLock<Inner<T>>>,
|
||||
waiters: Arc<RwLock<Vec<Box<dyn Fn(&mut Env, &T)>>>>,
|
||||
}
|
||||
|
||||
pub struct Complete<T> {
|
||||
inner: Arc<RwLock<Inner<T>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Cancelled;
|
||||
|
||||
struct Inner<T> {
|
||||
value: Option<Arc<T>>,
|
||||
waker: Option<Waker>,
|
||||
}
|
||||
|
||||
pub fn promise<Env, T>() -> (Complete<T>, Promise<Env, T>) {
|
||||
let inner = Arc::new(RwLock::new(Inner {
|
||||
value: None,
|
||||
waker: None,
|
||||
}));
|
||||
let promise = Promise {
|
||||
inner: inner.clone(),
|
||||
waiters: Arc::new(RwLock::new(Vec::new())),
|
||||
};
|
||||
let complete = Complete { inner: inner };
|
||||
(complete, promise)
|
||||
}
|
||||
|
||||
impl<T> Complete<T> {
|
||||
pub fn fulfill(&self, val: T) {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.value = Some(Arc::new(val));
|
||||
if let Some(waker) = inner.waker.take() {
|
||||
waker.wake()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Complete<Result<T, Cancelled>> {
|
||||
pub fn cancel(&mut self) {
|
||||
self.fulfill(Err(Cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, T> Complete<Result<T, E>> {
|
||||
pub fn ok(&mut self, val: T) {
|
||||
self.fulfill(Ok(val))
|
||||
}
|
||||
|
||||
pub fn err(&mut self, e: E) {
|
||||
self.fulfill(Err(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Env, T> Promise<Env, T> {
|
||||
pub fn on_fulfill<F: Fn(&mut Env, &T) + 'static>(&mut self, f: F) {
|
||||
let mut waiters = self.waiters.write().unwrap();
|
||||
waiters.push(Box::new(f));
|
||||
}
|
||||
}
|
||||
|
||||
impl<Env, T> Promise<Env, Result<T, Cancelled>> {
|
||||
pub fn on_cancel<F: Fn(&mut Env) + 'static>(&mut self, f: F) {
|
||||
self.on_err(move |env, _| f(env))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Env, E, T> Promise<Env, Result<T, E>> {
|
||||
pub fn on_ok<F: Fn(&mut Env, &T) + 'static>(&mut self, f: F) {
|
||||
self.on_fulfill(move |env, r| {
|
||||
if let Ok(val) = r {
|
||||
f(env, val)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_err<F: Fn(&mut Env, &E) + 'static>(&mut self, f: F) {
|
||||
self.on_fulfill(move |env, r| {
|
||||
if let Err(e) = r {
|
||||
f(env, e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Give<Env> {
|
||||
fn give(&self, env: &mut Env) -> bool;
|
||||
}
|
||||
|
||||
impl<Env, T> Give<Env> for Promise<Env, T> {
|
||||
fn give(&self, env: &mut Env) -> bool {
|
||||
let inner = self.inner.read().unwrap();
|
||||
if let Some(value) = &inner.value {
|
||||
let mut waiters = self.waiters.write().unwrap();
|
||||
for waiter in waiters.iter() {
|
||||
waiter(env, value);
|
||||
}
|
||||
waiters.clear();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Env, T> Clone for Promise<Env, T> {
|
||||
fn clone(&self) -> Self {
|
||||
Promise {
|
||||
inner: self.inner.clone(),
|
||||
waiters: self.waiters.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Env, P: Give<Env>> Give<Env> for &P {
|
||||
fn give(&self, env: &mut Env) -> bool {
|
||||
(*self).give(env)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Env, T> Future for Promise<Env, T> {
|
||||
type Output = Arc<T>;
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
match inner.value {
|
||||
Some(ref v) => Poll::Ready(v.clone()),
|
||||
None => {
|
||||
inner.waker = Some(cx.waker().clone());
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Promises<'a, Env> {
|
||||
ps: Vec<Box<dyn Give<Env> + 'a>>,
|
||||
}
|
||||
|
||||
impl<'a, Env> Promises<'a, Env> {
|
||||
pub fn new() -> Self {
|
||||
Promises { ps: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn push(&mut self, p: Box<dyn Give<Env> + 'a>) {
|
||||
self.ps.push(p);
|
||||
}
|
||||
|
||||
pub fn give_all(&mut self, env: &mut Env) {
|
||||
debug!("promises: {}", self.ps.len());
|
||||
self.ps.retain(|p| !p.give(env));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue