Compare commits

...

8 Commits

Author SHA1 Message Date
deadvey ae74c07948 docs update and moved around the test stories 2026-05-26 23:00:23 +01:00
deadvey 148fb73f7f added support for instring variables and made character IDs
be always stored in lowercase
2026-05-26 21:41:36 +01:00
deadvey cc3eca857d strings in variables can now be received by get_string_token
and inputs can be directly assigned to a variable
2026-05-26 20:35:57 +01:00
deadvey f6a95f76bd Added support for assigning variables to the output of a choice 2026-05-26 17:48:36 +01:00
deadvey 59f32e975b clippy lints and moved the operator matchbox to it's own function 2026-05-26 11:54:53 +01:00
deadvey 4f7abb5f19 added support for if elif else statements by returning a boolean
from the identifier syntax parsing
2026-05-26 11:38:14 +01:00
deadvey 20369ef838 added basic variable functionality, doesn't really do anything at the
moment but you can assign and change variables.
2026-05-25 02:19:30 +01:00
deadvey 21bf659718 Began adding support for variables 2026-05-24 15:09:59 +01:00
23 changed files with 757 additions and 95 deletions
+2 -5
View File
@@ -7,7 +7,7 @@ For help with syntax, see [this documentation](/docs/SYNTAX.md)
## Install
This is not really out of development, but to run it, clone the repo, go into /server/ and use cargo run story.zip to run a file called story.zip, relative to your current location.
This is not really out of development, but to run it, clone the repo, go into /server/ and use cargo run story.zip to run a file called story.zip, absolute or relative file location.
## Technical Details
@@ -103,14 +103,11 @@ Move a character with @CHARACTER to fr
> fr means front-right for instance.
## TODO
- Variables
- /about.json
- If/Else if/Else
- PAN
- INPUT
- tokeniser check for lack of END
- Fix no closing brace edge case
- Support single quotes for strings
- backslashes in strings
- Brace index getter check for closing
- Proper Error messages centralised???
+92
View File
@@ -206,6 +206,16 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "fastrand"
version = "2.4.1"
@@ -379,11 +389,13 @@ dependencies = [
name = "happening-client"
version = "0.1.0"
dependencies = [
"home",
"macroquad",
"phf",
"reqwest",
"serde",
"serde_json",
"tokio",
]
[[package]]
@@ -403,6 +415,15 @@ version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "home"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "http"
version = "1.4.0"
@@ -724,6 +745,15 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
@@ -846,6 +876,29 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -1047,6 +1100,15 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags 2.11.1",
]
[[package]]
name = "reqwest"
version = "0.13.3"
@@ -1217,6 +1279,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "3.7.0"
@@ -1295,6 +1363,16 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.9"
@@ -1463,11 +1541,25 @@ dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
+2
View File
@@ -4,8 +4,10 @@ version = "0.1.0"
edition = "2024"
[dependencies]
home = "0.5.12"
macroquad = "0.4.15"
phf = {version="0.13.1",features=["macros"]}
reqwest = {version="0.13.3",features=["blocking","json"]}
serde = {version="1.0.228",features=["derive"]}
serde_json = "1.0.149"
tokio = {version="1.52.3",features=["full"]}
+89 -21
View File
@@ -1,14 +1,24 @@
use macroquad::prelude::*;
use reqwest::*;
use reqwest::blocking;
use std::collections::HashMap;
use phf::phf_map;
use std::{thread, time::Duration};
use std::
{
thread,
time::Duration,
collections::
{
HashMap,
HashSet,
},
process::exit,
};
use serde::
{
Serialize,
Deserialize,
};
use home::home_dir;
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct Data {
@@ -69,45 +79,92 @@ static POSITIONS: phf::Map<&'static str, [f32;2]> = phf_map! [
async fn main()
{
let mut characters: HashMap<String, (Character, Texture2D)> = HashMap::new();
let mut textures: Vec<(Texture2D, f32, f32, Color)> = Vec::new();
characters.insert("narrator".to_string(), (Character { name: "Narrator".to_string(), ..Default::default() }, Texture2D::empty()));
let mut textures: HashMap<String,(Texture2D, f32, f32, Color)> = HashMap::new();
let mut text: String = String::new();
let mut checking_for_choice: bool = false;
let mut data: Data = next_happening(); // First one should be begin
loop
{
clear_background(RED);
// Get the next character
let data = next_happening();
let character_name: &str = data.character.as_str();
// Add the character to the HashMap if it's not already
if character_name != "" && !characters.contains_key(character_name)
for (name, (texture, x, y, colour)) in &textures
{
let new_character = get_character(character_name).await;
characters.insert(character_name.to_string(), new_character);
draw_texture(&texture, *x, *y, *colour);
}
draw_multiline_text(&text, 50.0,30.0,40.0,None,WHITE);
if !is_any_key_down()
{
next_frame().await;
continue
}
let keys: HashSet<KeyCode> = get_keys_pressed();
if checking_for_choice
{
for key in &keys
{
let keycode = *key as u16;
let length: u16 = data.choices.len() as u16;
println!("key: {key:?} {keycode}");
if keycode > 48 && keycode <= length+48
{
checking_for_choice = false;
println!("Sending POST: {}",keycode-49);
let value = keycode - 49;
send_choice(value);
}
else { continue }
}
}
// Get the next character
data = next_happening();
let character_name: String = data.character.to_lowercase();
// Add the character to the HashMap if it's not already
if character_name != "" && !characters.contains_key(&character_name)
{
println!("Fetching {character_name}");
let new_character = get_character(&character_name).await;
characters.insert(character_name.clone(), new_character);
}
// Matchbox for all the commands
match data.action_type.to_lowercase().as_str()
{
"choice" =>
{
for (index, choice) in data.choices.iter().enumerate()
{
text += format!("\n{}. {}",index+1, choice).as_str();
}
checking_for_choice = true;
},
"output" =>
{
println!("SAYING");
draw_text(characters[character_name].0.name.clone(), 50.0,20.0,40.0,WHITE);
draw_text(data.content.as_str(), 50.0,40.0,30.0,WHITE);
text = format!("{}: {}", characters[&character_name].0.name.clone(),wrap_text(&data.content).as_str());
},
"to" =>
{
let position = POSITIONS.get(&data.content).cloned().unwrap();
let texture = &characters[character_name].1;
textures.push((texture.clone(), position[0], position[1], WHITE)); // Heavy
let texture = &characters[&character_name].1;
textures.insert(character_name.clone(),(texture.clone(), position[0], position[1], WHITE)); // Heavy
}
_ => println!("Unknown action"),
"begin" => (),
"end" => exit(0),
_ => println!("Unknown action, {}", data.action_type),
}
for (texture, x, y, colour) in &textures
{
draw_texture(&texture, *x, *y, *colour);
}
thread::sleep(Duration::from_millis(1000));
next_frame().await;
}
}
fn wrap_text(text: &str) -> String
{
text.chars()
.collect::<Vec<_>>()
.chunks(30)
.map(|chunk| chunk.iter().collect::<String>())
.collect::<Vec<_>>()
.join("\n")
}
fn next_happening()
-> Data
{
@@ -116,6 +173,17 @@ fn next_happening()
data
}
#[tokio::main]
async fn send_choice(index: u16)
{
let client = reqwest::Client::new();
let res = client.post("http://localhost:20264/choice")
.json(&index)
.send()
.await;
}
async fn get_character(name: &str)
-> (Character, Texture2D)
{
@@ -125,7 +193,7 @@ async fn get_character(name: &str)
let skin: Color = Color::from_rgba(skin_colour.red,skin_colour.green,skin_colour.blue,255);
let hair_colour = character.hair_color.clone();
let hair: Color = Color::from_rgba(hair_colour.red,hair_colour.green,hair_colour.blue,255);
let head_path: String = format!("../images/head/{}.png",character.head_shape);
let head_path: String = format!("/home/deadvey/.local/share/happening/images/head/{}.png",character.head_shape);
let head: Texture2D = change_colour(&mut load_image(head_path.as_str()).await.unwrap(), &skin, &hair);
(character, head)
}
+4
View File
@@ -0,0 +1,4 @@
killall happening-server
RUST_LOG=debug
happening-server $XDG_DATA_HOME/happening/stories/$1 &
cargo run .
+64 -6
View File
@@ -15,22 +15,57 @@ This is done in the about.json file,
```
## Characters
See [Character documentation](/docs/CHARACTER.md) for more info
Referencing a character using the @, @NARRATOR is reserved for the Narrator.<br/>
Customisation is done with @CHARACTER change \<feature\> into \<feature name\><br/>
Customisation is done with @CHARACTER change \<feature\> \<feature name\><br/>
Move a character with @CHARACTER to fr
Outputting can be done with @CHARACTER says "string", see more at (outputs)[##outputs]
> [!NOTE]
> fr means front-right for instance.
## Variables
Currently only strings and integers are supported as variable types (booleans are planned).<br/>
The interpreter can assume the data type so this doesn't need to be specified.<br/>
To assign new variables (or overwrite existing ones):
```
$x = 1
$an_integer = 3
$y = "hello"
$a_string = "world"
```
> [!NOTE]
> Variable names can be as long as you want and can contain any characters except whitespaces
To modify existing variables:
```
$x + 1 // Adds 1 to an integer x
$x - 1 // Subtracts 1 from an integer x
$y + " world" // Appends " world" to the end of a string y
```
Other uses:
```
$x = choice "choice 1" { // Assigns to x the choice made by the client
...
$x = input // Assigns to x the string input made by the client
```
## Outputs
```
@CHARACTER says "this string
is multi-line
and ends with a"
$x = "hello world"
@CHARACTER says $x
$name = "deadvey"
@CHARACTER says "hello $name"
```
> [!NOTE]
> Strings only support double quotes now ("") and do not support having quotes within quotes.
## Variables
@@ -41,18 +76,20 @@ Variables are referenced with the \$, only integers will be supported.<br/>
Condition based:
```
if (condition) {
if condition {
}
elif (condition) {
elif condition { // Only gets checked if the if and all previous elif statements failed
}
else
else // Always passes if all previous if and elif statements failed
}
```
> [!NOTE]
> See [conditions](##conditions)
Choice based:
@@ -69,15 +106,35 @@ or "choice 3" {
}
```
You can assign a variable to the result of a choice by doing the following:
```
$x = choice "choice 1" {
...
```
## Positioning
```
@CHARACTER to position
PAN to position
PAN position
```
## Conditions
A condition works works like this:
```
$x == "hello" // evaluates true only if x is "hello"
$x > 1 // evaulates true if x is an integer and is more than 1
$x < 1 // evaluates true if x is an integer and is less than 1
$x >= 1 // evaluates true if x is an integer and is more than or equal to 1
$x <= 1 // evaluates true if x is an integer and is less than or equal to 1
```
> [!NOTE]
> == can be used for integers or strings whereas the rest can only be used with integers
The order (variable operator value) is currently fixed and so must be layed out like this.
## Other
```
@@ -89,3 +146,4 @@ GOTO label
## Ending
`END` to exit out of the story
+2 -1
View File
@@ -457,10 +457,11 @@ dependencies = [
[[package]]
name = "happening-server"
version = "0.0.2"
version = "0.0.3"
dependencies = [
"env_logger",
"log",
"regex",
"serde",
"serde-patch",
"serde_json",
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "happening-server"
version = "0.0.2"
version = "0.0.3"
edition = "2024"
license = "GPL-3"
repository = "https://git.javalsai.tuxcord.net/deadvey/happening/"
@@ -25,6 +25,7 @@ unwrap_used = "warn"
[dependencies]
env_logger = "0.11.10"
log = "0.4.29"
regex = "1.12.3"
serde = { version = "1.0.228", features = ["derive"] }
serde-patch = "0.2.3"
serde_json = "1.0.149"
+1
View File
@@ -131,6 +131,7 @@ pub fn modify_data // TODO rename
character,
choices,
};
debug!("{new_data:?}");
queue.push_back(new_data);
drop(queue);
}
+5 -2
View File
@@ -71,8 +71,11 @@ pub fn character_parse(archive: &mut ZipArchive<File>)
// Serialise this to a HashMap
let characters: HashMap<String, Character> =
serde_json::from_str(&file_contents)
.map_err (|err| format!("Invalid JSON in characters.json: {err}"))?;
serde_json::from_str::<HashMap<String,Character>>(&file_contents)
.map_err (|err| format!("Invalid JSON in characters.json: {err}"))?
.into_iter()
.map(|(k,v)| (k.to_lowercase(), v))
.collect();
info!("Parsed characters from characters.json");
debug!("{characters:?}");
Ok(Arc::new(Mutex::new(characters)))
+11 -1
View File
@@ -40,6 +40,7 @@ use crate::
{
traits::UnwrapOrExit,
};
use regex::Regex;
#[tokio::main]
async fn main()
@@ -108,9 +109,18 @@ async fn main()
error!("Unable to read story file to string: {err}");
exit(14);
});
// Tokenise the file
let (tokens, labels) = tokenise::tokenise(&file_contents)
.unwrap_or_exit("Unable to tokenise data", 15);
debug!("{tokens:?}\n{labels:?}");
// TESTING
//debug!("{tokens:?}\n{labels:?}");
for (index,token) in tokens.iter().enumerate()
{
debug!("{index}: {token:?}");
}
// Run the syntactic parser
let characters_clone2 = Arc::clone(&characters);
let happening_stack2 = Arc::clone(&happening_stack);
// Run the parsing process for the DSL
+22 -5
View File
@@ -1,4 +1,3 @@
use std::collections::HashMap;
use crate::
{
// Internal code
@@ -11,6 +10,7 @@ use crate::
Arc,
Mutex,
VecDeque,
HashMap,
info,
debug,
warn,
@@ -18,6 +18,8 @@ use crate::
mod character_parse;
mod keyword_parse;
mod identifier_parse;
// Parse the tokens in a file
// Returns success or an error string
@@ -30,6 +32,7 @@ pub fn token_parse(
) -> Result<(),String>
{
let mut index: usize = 0;
let mut variables: HashMap<String, tokenise::Value> = HashMap::new();
info!("Client has connected");
// Run an infinite loop
loop
@@ -38,10 +41,11 @@ pub fn token_parse(
// Get the next token
match tokens.get(index)
{
// Keyword, eg IF, CHOICE or GOTO
Some(tokenise::Token::Keyword(token)) =>
{
if token.to_lowercase().as_str() == "end" { return Ok(()); }
index = keyword_parse::keyword_parse(tokens, token, index, happening_queue, labels, rx)?;
index = keyword_parse::keyword_parse(tokens, token, index, happening_queue, labels, &mut variables, rx)?;
},
// Ignore closing braces and jump over opening brace blocks
Some(tokenise::Token::Bracket((bracket,new_index))) =>
@@ -56,7 +60,7 @@ pub fn token_parse(
// Handle a character
Some(tokenise::Token::Character(character_name)) =>
{
index = match character_parse::character_parse(index+1,tokens,character_name.clone(),characters,happening_queue)
index = match character_parse::character_parse(index+1,tokens,character_name.clone(),characters,happening_queue,&variables)
{
Ok(increment) => increment,
Err((err,increment)) =>
@@ -66,9 +70,22 @@ pub fn token_parse(
},
};
}
Some(_) =>
// Identifier
Some(tokenise::Token::Identifier(name)) =>
{
warn!("Unexpected token");
index = match identifier_parse::identifier_parse(index+1,name,tokens,&mut variables,rx,happening_queue)
{
Ok((increment,_)) => increment,
Err((err,increment)) =>
{
warn!("{err}");
increment
},
};
}
Some(tok) =>
{
warn!("Unexpected token, at index {index}, expected character, keyword or identifier, got {tok:?}");
index += 1;
},
None => return Err("File unexpectedly reached termination point".to_string()),
+7 -6
View File
@@ -25,6 +25,7 @@ pub fn character_parse
character_name: String,
characters: &Arc<Mutex<HashMap::<String, character::Character>>>,
happening_queue: &Arc<Mutex<VecDeque<api::DataToSend>>>,
variables: &HashMap<String, tokenise::Value>,
) -> Result<usize,(String,usize)>
{
let mut sum_index: usize = index;
@@ -45,8 +46,8 @@ pub fn character_parse
{
info!("SAYS command with character {character_name}");
sum_index += 1;
let output = tokenise::get_string_token(tokens, sum_index)
.map_err(|err| (err, index))?;
let output = tokenise::get_string_token(tokens, sum_index, variables)
.map_err(|err| (err, sum_index))?;
debug!("Saying {output}");
api::modify_data(happening_queue, "output".to_string(), output, character_name, vec![]);
},
@@ -56,10 +57,10 @@ pub fn character_parse
{
sum_index += 1;
let feature = tokenise::get_keyword_token(tokens, sum_index)
.map_err(|err| (err, index))?;
.map_err(|err| (err, sum_index))?;
sum_index += 1;
let string = tokenise::get_string_token(tokens, sum_index)
.map_err(|err| (err, index))?;
let string = tokenise::get_string_token(tokens, sum_index,variables)
.map_err(|err| (err, sum_index))?;
info!("CHANGE command with character {character_name} feature {feature}");
let mut characters = characters.lock().unwrap_or_exit("Character Mutex was poisoned",3);
if let Some(character) = characters.get_mut(&character_name)
@@ -74,7 +75,7 @@ pub fn character_parse
{
sum_index += 1;
let content = tokenise::get_keyword_token(tokens, sum_index)
.map_err(|err| (err, index))?;
.map_err(|err| (err, sum_index))?;
api::modify_data(happening_queue, keyword.to_lowercase(), content, character_name, vec![]);
},
// Catch all condition, if the instruction is unrecognised as a
+169
View File
@@ -0,0 +1,169 @@
use crate::
{
// Internal code
tokenise,
api,
//Libs
HashMap,
Arc,
Mutex,
VecDeque,
warn,
debug,
info,
mpsc::Receiver,
};
use super::keyword_parse;
#[allow(unused_variables)]
pub fn identifier_parse
(
index: usize,
identifier: &String,
tokens: &[tokenise::Token],
variables: &mut HashMap<String, tokenise::Value>,
rx: &Receiver<(usize,String)>,
happening_queue: &Arc<Mutex<VecDeque<api::DataToSend>>>,
) -> Result<(usize,bool),(String,usize)>
{
let mut sum_index: usize = index;
let current = variables
.entry(identifier.clone())
.or_insert(tokenise::Value::Null)
.clone();
let operator = tokenise::get_operator_token(tokens, sum_index)
.map_err(|err| (err, sum_index))?;
sum_index += 1;
let result: bool = match tokenise::get_value_token(tokens, sum_index)
{
// An value that can be directly assigned to or compared against the variable
Ok(value) =>
{
sum_index += 1;
operator_match(&current,value,operator,identifier,variables)
},
// Another thing like a choice or an input
Err(_) =>
{
if operator != tokenise::Operator::Assignment // Only assignment is valid here
{
return Err((format!("Unexpected operator: {operator:?} at index {}",sum_index-1),sum_index + 1))
};
let keyword = tokenise::get_keyword_token(tokens,sum_index)
.map_err(|err| (err,sum_index))?;
match keyword.to_lowercase().as_str()
{
"choice" =>
{
let choice: String;
(sum_index, choice) = keyword_parse::choice_parse(tokens, index+1, happening_queue,rx,variables)
.map_err(|err| (err,sum_index+1))?;
variables.insert(identifier.to_owned(), tokenise::Value::String(choice));
},
"input" =>
{
api::modify_data(happening_queue, "input".to_string(), String::new(), String::new(), Vec::new());
info!("Waiting for client input");
let input = match rx.recv()
{
Ok((_,input)) => input,
Err(err) =>
{
warn!("Error receiving input from client, defaulting to choice \"\" {err}");
"".to_string()
}
};
variables.insert(identifier.to_owned(), tokenise::Value::String(input));
sum_index += 1;
},
_ =>
{
warn!("Unexpected keyword {keyword}");
},
}
false
},
};
debug!("{variables:?}");
Ok((sum_index,result))
}
fn operator_match
(
current: &tokenise::Value,
value: tokenise::Value,
operator: tokenise::Operator,
identifier: &String,
variables: &mut HashMap<String, tokenise::Value>
)
-> bool
{
// Operator match box
match operator
{
// Changing a value
tokenise::Operator::Assignment =>
{
variables.insert(identifier.to_owned(), value);
} ,
tokenise::Operator::Add =>
{
let result: tokenise::Value = match (value.clone(), current)
{
(tokenise::Value::Integer(int1),tokenise::Value::Integer(int2)) => tokenise::Value::Integer(int1 + int2),
(tokenise::Value::String(str1),tokenise::Value::String(str2)) => tokenise::Value::String(format!("{str1}{str2}")),
_ => value, // otherwise invalid
};
variables.insert(identifier.to_owned(), result);
},
tokenise::Operator::Sub =>
{
let result: tokenise::Value = match (value.clone(), current)
{
(tokenise::Value::Integer(int1),tokenise::Value::Integer(int2)) => tokenise::Value::Integer(int2 - int1),
_ => value, // otherwise invalid
};
variables.insert(identifier.to_owned(), result);
},
// Comparisons, return a boolean
tokenise::Operator::Comparison(comp) =>
{
let result = match (current, &value)
{
// Integer
(tokenise::Value::Integer(current), tokenise::Value::Integer(comparing)) =>
{
match comp
{
tokenise::Comparison::Equate => current == comparing,
tokenise::Comparison::Greater => current > comparing,
tokenise::Comparison::Less => current < comparing,
tokenise::Comparison::GreaterOrEqual => current >= comparing,
tokenise::Comparison::LessOrEqual => current <= comparing,
}
},
// String
(tokenise::Value::String(current), tokenise::Value::String(comparing)) =>
{
match comp
{
tokenise::Comparison::Equate => current == comparing,
tokenise::Comparison::Greater => current > comparing,
tokenise::Comparison::Less => current < comparing,
tokenise::Comparison::GreaterOrEqual => current >= comparing,
tokenise::Comparison::LessOrEqual => current <= comparing,
}
},
_ => {
warn!("Invalid comparison of {current:?} and {value:?}, evaluating false");
false
},
};
debug!("Comparison {current:?} comp {value:?} evaluates to {result}");
return result;
}
}
true
}
+80 -16
View File
@@ -12,6 +12,7 @@ use crate::
info,
mpsc::Receiver,
};
use super::identifier_parse;
pub fn keyword_parse(
tokens: &[tokenise::Token],
@@ -19,6 +20,7 @@ pub fn keyword_parse(
mut index: usize,
happening_queue: &Arc<Mutex<VecDeque<api::DataToSend>>>,
labels: &HashMap<String, usize>,
variables: &mut HashMap<String, tokenise::Value>,
rx: &Receiver<(usize,String)>,
)
-> Result<usize, String>
@@ -28,18 +30,54 @@ pub fn keyword_parse(
{
"choice" =>
{
let choice_indeces = choice_parse(tokens, index, happening_queue)?;
debug!("{choice_indeces:?}");
let choice = match rx.recv()
(index,_) = choice_parse(tokens, index, happening_queue, rx,variables)?;
},
"if" =>
{
Ok((choice,_)) => choice_indeces[choice],
Err(err) =>
// TODO can this go in a function?
let mut keyword = "if".to_string();
while keyword == "if" || keyword == "elif" || keyword == "else" // TODO less beefy??
{
warn!("Error receiving choice from client, defaulting to choice 0 {err}");
0
index += 1;
let mut result: bool = true;
if keyword != "else"
{
let identifier = tokenise::get_identifier_token(tokens, index)?;
(index,result) = match identifier_parse::identifier_parse(index+1, &identifier, tokens, variables,rx,happening_queue)
{
Ok((increment, result)) => (increment,result),
Err((err,increment)) =>
{
warn!("{err}");
(increment,false)
}
}
}
if result { index += 1; break; }
index = tokenise::get_closing_index(tokens,index)?;
index += 1;
keyword = match tokenise::get_keyword_token(tokens,index)
{
Ok(keyword) => keyword,
Err(_) => break,
}
}
},
"else" =>
{
index += 1;
index = tokenise::get_closing_index(tokens, index)?;
},
"elif" =>
{
loop
{
index += 1;
let Ok(new_index) = tokenise::get_closing_index(tokens, index)
else { continue; };
index = new_index;
break
}
};
index = choice;
},
"or" =>
{
@@ -51,6 +89,7 @@ pub fn keyword_parse(
// Jump to a particular index based on a label eg GOTO character_check
"goto" =>
{
info!("GOTO command, jumping there");
index += 1;
let label = tokenise::get_keyword_token(tokens, index)?;
index = if let Some(label_index) = labels.get(&label) { *label_index }
@@ -60,18 +99,32 @@ pub fn keyword_parse(
index + 1
};
debug!("Jumping to {index}");
}
},
"pan" =>
{
info!("PAN command, informing client");
index += 1;
let location = tokenise::get_keyword_token(tokens, index)?;
api::modify_data(happening_queue, "pan".to_string(), location, String::new(), Vec::new());
},
_ =>
{
warn!("Invalid command: {token}");
warn!("Invalid command: {token}, index {index}");
index += 1;
}
}
Ok(index)
}
fn choice_parse(tokens: &[tokenise::Token], mut index: usize, happening_queue: &Arc<Mutex<VecDeque<api::DataToSend>>>,)
-> Result<Vec<usize>, String>
pub fn choice_parse
(
tokens: &[tokenise::Token],
mut index: usize,
happening_queue: &Arc<Mutex<VecDeque<api::DataToSend>>>,
rx: &Receiver<(usize,String)>,
variables: &HashMap<String,tokenise::Value>,
)
-> Result<(usize,String), String>
{
let mut next_token: String = "or".to_string();
let mut choices: Vec<String> = Vec::new();
@@ -81,7 +134,7 @@ fn choice_parse(tokens: &[tokenise::Token], mut index: usize, happening_queue: &
index += 1;
choices.push
(
tokenise::get_string_token(tokens, index)?
tokenise::get_string_token(tokens, index,variables)?
);
index += 1;
choice_indeces.push(index+1);
@@ -96,6 +149,17 @@ fn choice_parse(tokens: &[tokenise::Token], mut index: usize, happening_queue: &
Err(_) => break,
}
};
api::modify_data(happening_queue, "choice".to_string(), String::new(), String::new(), choices);
Ok(choice_indeces)
api::modify_data(happening_queue, "choice".to_string(), String::new(), String::new(), choices.clone());
info!("Waiting for client choice");
debug!("{choice_indeces:?}");
let choice = match rx.recv()
{
Ok((choice,_)) => choice,
Err(err) =>
{
warn!("Error receiving choice from client, defaulting to choice 0 {err}");
0
}
};
Ok((choice_indeces[choice],choices[choice].clone()))
}
View File
+163 -10
View File
@@ -1,31 +1,76 @@
use crate::
{
HashMap,
Regex,
};
#[derive(Debug, Clone)]
pub enum Token
{
String(String),
Value(Value),
Operator(Operator),
Keyword(String), // Keywords aren't checked for validity in this stage
#[allow(dead_code)] // This is unused rn, but am going to add it later
Identifier(String),
Bracket((Bracket,usize)), // Stores the index of the matching deliminator
Character(String),
}
#[derive(Debug,Clone,PartialEq,Eq)]
pub enum Value
{
String(String),
Integer(i64),
Bool(bool),
Null,
}
#[derive(Debug,Clone,PartialEq,Eq)]
pub enum Bracket
{
Opening,
Closing,
}
pub fn get_string_token(tokens: &[Token], index: usize)
-> Result<String, String>
#[derive(Debug, Clone,PartialEq)]
pub enum Operator
{
// Changing a value
Assignment,
Add,
Sub,
Comparison(Comparison),
}
// Comparing a value
#[derive(Debug,Clone,PartialEq)]
pub enum Comparison
{
Equate,
Greater,
Less,
GreaterOrEqual,
LessOrEqual,
}
impl Value
{
pub fn as_string(&self) -> String
{
match self
{
Value::String(s) => s.clone(),
Value::Integer(i) => i.to_string(),
Value::Bool(b) => b.to_string(),
Value::Null => "".to_string(),
}
}
}
// TODO error reporting for all of these
pub fn get_operator_token(tokens: &[Token], index: usize)
-> Result<Operator, String>
{
match tokens.get(index) {
Some(Token::String(s)) => Ok(s.clone()),
Some(_) => Err("Unexpected token".to_string()),
Some(Token::Operator(op)) => Ok(op.clone()),
Some(tok) => Err(format!("Unexpected token at index {index}, expected operator, got {tok:?}")),
None => Err("File unexpectedly reached termination point".to_string()),
}
}
@@ -34,7 +79,7 @@ pub fn get_keyword_token(tokens: &[Token], index: usize)
{
match tokens.get(index) {
Some(Token::Keyword(s)) => Ok(s.clone()),
Some(_) => Err("Unexpected token".to_string()),
Some(tok) => Err(format!("Unexpected token at index {index}, expected keyword, got {tok:?}")),
None => Err("File unexpectedly reached termination point".to_string()),
}
}
@@ -43,10 +88,77 @@ pub fn get_closing_index(tokens: &[Token], index: usize)
{
match tokens.get(index) {
Some(Token::Bracket((_, index))) => Ok(*index), // TODO check for closing
Some(_) => Err("Unexpected token".to_string()),
Some(tok) => Err(format!("Unexpected token at index {index}, expected opening brace, got {tok:?}")),
None => Err("File unexpectedly reached termination point".to_string()),
}
}
pub fn get_value_token(tokens: &[Token], index: usize)
-> Result<Value, String>
{
match tokens.get(index) {
Some(Token::Value(val)) => Ok(val.clone()),
Some(tok) => Err(format!("Unexpected token at index {index}, expected value (int, string or boolean), got {tok:?}")),
None => Err("File unexpectedly reached termination point".to_string()),
}
}
pub fn get_identifier_token(tokens: &[Token], index: usize)
-> Result<String, String>
{
match tokens.get(index) {
Some(Token::Identifier(s)) => Ok(s.clone()),
Some(tok) => Err(format!("Unexpected token at index {index}, expected identifier, got {tok:?}")),
None => Err("File unexpectedly reached termination point".to_string()),
}
}
pub fn get_string_token(tokens: &[Token], index: usize, variables: &HashMap<String, Value>)
-> Result<String, String>
{
match tokens.get(index) {
// Either get string directly
Some(Token::Value(val)) =>
{
match val
{
Value::String(s) =>
{
let string = insert_variables_into_string(s, variables);
Ok(string)
},
_ => Err("Unexpected value type".to_string()),
}
},
// Or get the string that the identifier references
Some(Token::Identifier(iden)) =>
{
let val = variables.get(iden)
.ok_or(format!("Variable {iden} not initialised"))?;
match val
{
Value::String(s) =>
{
let string = insert_variables_into_string(s, variables);
Ok(string)
},
_ => Err("Unexpected value type".to_string()),
}
},
Some(tok) => Err(format!("Unexpected token at index {index}, expected string value, got {tok:?}")),
None => Err("File unexpectedly reached termination point".to_string()),
}
}
fn insert_variables_into_string(string: &str, variables: &HashMap<String, Value>)
-> String
{
let re = Regex::new(r"\$([A-Za-z0-9-_]*)").unwrap();
re.replace_all(string, |caps: &regex::Captures| {
let key = &caps[1].to_lowercase();
variables.get(key)
.map(|v| v.as_string())
.unwrap_or_else(|| caps[0].to_string()) // leave unchanged if missing
})
.to_string()
}
// Tokenise the story to Vec<Token>
// It can contain sub-objects (using recursive calls)
@@ -64,12 +176,31 @@ pub fn tokenise(file_contents: &str)
while index < space_seperated.len()
{
let mut item = space_seperated[index].to_string();
// Operator
let op: Option<Operator> = match item.as_str()
{
"=" => Some(Operator::Assignment),
"+" => Some(Operator::Add),
"-" => Some(Operator::Sub),
"==" => Some(Operator::Comparison(Comparison::Equate)),
">" => Some(Operator::Comparison(Comparison::Greater)),
"<" => Some(Operator::Comparison(Comparison::Less)),
">=" => Some(Operator::Comparison(Comparison::GreaterOrEqual)),
"<=" => Some(Operator::Comparison(Comparison::LessOrEqual)),
_ => None,
};
if let Some(op) = op
{
tokenised_data.push(Token::Operator(op));
index += 1;
continue // skip the rest of this loop
}
// Characters
if item.starts_with('@')
{
let mut chars = item.chars();
chars.next();
tokenised_data.push(Token::Character(chars.as_str().to_string()));
tokenised_data.push(Token::Character(chars.as_str().to_lowercase().to_string())); // Force character to be lowecase
}
// Strings
else if item.starts_with('"') // TODO support '
@@ -78,8 +209,28 @@ pub fn tokenise(file_contents: &str)
else { return Err("File unexpectedly ended: No closing quote".to_string()) };
index = new_index;
item = new_item;
tokenised_data.push(Token::String(item));
tokenised_data.push(Token::Value(Value::String(item)));
}
else if let Ok(value) = item.parse::<i64>()
{
tokenised_data.push(Token::Value(Value::Integer(value))); // unwrap is fine here because I checked
}
else if item == "true"
{
tokenised_data.push(Token::Value(Value::Bool(true)));
}
else if item == "false"
{
tokenised_data.push(Token::Value(Value::Bool(false)));
}
// variable/identifier
else if item.starts_with('$')
{
let mut chars = item.chars();
chars.next();
tokenised_data.push(Token::Identifier(chars.as_str().to_string()));
}
// Integer
// Labels
else if item.ends_with(':')
{
@@ -111,6 +262,8 @@ pub fn tokenise(file_contents: &str)
Ok((tokenised_data, labels))
}
// TODO support strings that contain " in them by using a backslash
fn tokenise_string(space_seperated: &[&str], mut index: usize)
-> Option<(usize, String)>
{
-14
View File
@@ -1,14 +0,0 @@
@tim says "hello world, it's a good day"
@tim change name "Timothy Fineshooter"
label:
choice "choice numero uno" {
@tim says "super sad"
}
or "choice numero duo" {
@tim says "super unsad"
}
or "choice numero tres" {
@tim says "hola mi amigos"
goto label
}
END
BIN
View File
Binary file not shown.
@@ -10,7 +10,7 @@
"pronoun_deppos": "His",
"pronoun_indpos": "His",
"pronoun_reflex": "Himself",
"head_shape": "",
"head_shape": "normal-male",
"hair_style": "",
"torso_shape": "",
"arm_shape": "",
+27
View File
@@ -0,0 +1,27 @@
$name = input
@tim says "hello"
@tim says $name
$x = 3
$x + 1
if $x == 4 {
@tim says "5"
}
elif $x < 5 {
@tim says "<5"
}
else {
@tim says "whar"
}
label:
$choice_string = choice "choice numero uno" {
@tim says "super sad"
}
or "choice numero duo" {
@tim says "super unsad"
}
or "choice numero tres" {
@tim says "hola mi amigos"
goto label
}
END
+12 -4
View File
@@ -2,11 +2,11 @@ import requests
import os
import time
import sys
debug = False
debug = True
try:
if sys.argv[1] == "debug": debug = True
if sys.argv[1] == "silent": debug = False
except:
debug = False
debug = True
# Loop and get new api
def main():
@@ -22,7 +22,9 @@ def main():
output(character, response["content"])
case "choice":
user_choice = choice(response["choices"])
time.sleep(0.5)
continue
case "input":
get_input()
continue
case "end":
print("Exitting successfully")
@@ -44,6 +46,12 @@ def choice(choices):
print("Invalid choice, defaulting to 0")
requests.post(api_url, json=choice);
def get_input():
api_url = "http://localhost:20264/input"
input_string = input("Input: ")
requests.post(api_url, json=input_string);
# Character outputs text to the user
def output(character, text):
print(character["name"], "says")