Compare commits

..

13 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
deadvey 93ca1ea34a untrack images :skull_emoji: 2026-05-20 21:53:33 +01:00
deadvey 29565949b0 Client is approx 5% done 2026-05-20 21:51:09 +01:00
deadvey 556185e095 added some TODO listing in the README and removed junk file 2026-05-19 19:38:25 +01:00
deadvey 9255c1d7fa Fixed some clippy lints 2026-05-19 19:34:56 +01:00
deadvey ee34493895 Changed the data_to_send to be a stack so many lines of code can
be pre-processed before the user interacts.
When the /happening api is called it just dequeues the front item
2026-05-19 19:23:19 +01:00
25 changed files with 3138 additions and 426 deletions
+2
View File
@@ -1,6 +1,8 @@
/server/target
/client/target
*.swp
.venv
.~*
report.odt
examples/
images/
+9 -29
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
@@ -102,34 +102,14 @@ Move a character with @CHARACTER to fr
> [!NOTE]
> fr means front-right for instance.
## Implemented stuff
### Commands
| Command | Implemented |
| --------------- | ----------- |
| END | Yes |
| CHOICE/OR/OR | Yes |
| IF/ELSE IF/ELSE | No |
| GOTO | Yes |
| PAN | No |
### Character sub-commands
| Character Command | Implemented |
| ----------------- | ----------- |
| SAYS | Yes |
| CHANGE | Yes |
| TO | Yes |
| ANIMATE | Yes |
### Other Features
| Feature | Implemented |
| ---------- | ----------- |
| Variables | No |
| about.json | No |
| INPUT | Partially |
## TODO
- /about.json
- 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???
## Error codes
+2151
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "happening-client"
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"]}
+223
View File
@@ -0,0 +1,223 @@
use macroquad::prelude::*;
use reqwest::*;
use reqwest::blocking;
use phf::phf_map;
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 {
pub action_type: String,
pub content: String,
pub character: String,
pub choices: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
#[serde(default)]
pub struct Character
{
name: String,
gender: String,
eye_color: Colour,
hair_color: Colour,
skin_color: Colour,
pronoun_subject: String,
pronoun_object: String,
pronoun_deppos: String,
pronoun_indpos: String,
pronoun_reflex: String,
head_shape: String,
hair_style: String,
torso_shape: String,
arm_shape: String,
leg_shape: String,
clothing: Clothing,
}
#[derive(Debug,Deserialize,Serialize,Clone,Default)]
#[serde(default)]
pub struct Clothing
{
top: String,
bottom: String,
shoes: String,
}
#[derive(Debug,Deserialize,Serialize,Clone,Default)]
#[serde(default)]
pub struct Colour
{
red: u8,
green: u8,
blue: u8,
}
static POSITIONS: phf::Map<&'static str, [f32;2]> = phf_map! [
"fl" => [0.0,500.0],
"bl" => [0.0,0.0],
"fr" => [500.0,500.0],
"br" => [500.0,0.0],
];
#[macroquad::main("happening")]
async fn main()
{
let mut characters: HashMap<String, (Character, Texture2D)> = HashMap::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);
for (name, (texture, x, y, colour)) in &textures
{
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");
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.insert(character_name.clone(),(texture.clone(), position[0], position[1], WHITE)); // Heavy
}
"begin" => (),
"end" => exit(0),
_ => println!("Unknown action, {}", data.action_type),
}
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
{
let data: Data = reqwest::blocking::get(format!("http://127.0.0.1:20264/happening")).unwrap().json().unwrap();
println!("{data:?}");
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)
{
let character: Character = reqwest::blocking::get(format!("http://127.0.0.1:20264/character/{name}")).unwrap().json().unwrap();
println!("{character:?}");
let skin_colour = character.skin_color.clone();
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!("/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)
}
fn change_colour(image: &mut Image, skin: &Color, hair: &Color)
-> Texture2D
{
let target = Color::from_rgba(255,0,255,255);
for y in 0..image.height() {
for x in 0..image.width() {
let pixel = image.get_pixel(x as u32, y as u32);
if pixel == target {
image.set_pixel(x as u32, y as u32, *skin);
}
}
}
let target = Color::from_rgba(0,255,255,255);
for y in 0..image.height() {
for x in 0..image.width() {
let pixel = image.get_pixel(x as u32, y as u32);
if pixel == target {
image.set_pixel(x as u32, y as u32, *hair);
}
}
}
Texture2D::from_image(&image)
}
+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"
-184
View File
@@ -1,184 +0,0 @@
use std::collections::HashMap;
use crate::
{
// Internal code
character,
api,
tokenise,
// Libraries
mpsc::Receiver,
Arc,
Mutex,
info,
debug,
warn,
type_name,
};
mod strings;
mod character_parse;
// Parse the tokens in a file
// Returns success or an error string
pub fn token_parse(
tokens: &Vec<tokenise::Token>,
characters: &Arc<Mutex<HashMap::<String, character::Character>>>,
data_to_send: &Arc<Mutex<api::DataToSend>>,
rx: &Receiver<(bool,usize,String)>,
) -> Result<(),String>
{
info!("DSL parsing begun");
let mut index: usize = 0;
if rx.recv().is_err()
{
warn!("Some issue with api");
// TODO eh?
}
info!("Client has connected");
// Run an infinite loop
'parse_loop: loop
{
// Get the next token
let token = match tokens.get(index) {
Some(tokenise::Token::String(s)) => s.clone(),
Some(_) => return Err("Unexpected token".to_string()),
None => return Err("File unexpectedly reached termination point".to_string()),
};
debug!("{index}: {token}");
// The instructions are related to characters
if token.starts_with('@')
{
let character_name: String = token.chars().skip(1).collect();
debug!("Doing something with a character: {character_name}");
// The index is incremented to after the character's instructions
index = match character_parse::character_parse(index+1, tokens, character_name, characters, data_to_send)
{
Ok(increment) => increment,
Err((err,increment)) =>
{
warn!("{err}");
increment
},
};
}
// Miscelleneous instructions
else
{
match token.to_lowercase().as_str()
{
"end" =>
{
info!("END command, exiting");
return Ok(()) // quit successfully
},
"choice" =>
{
let mut choices: Vec<String> = Vec::new();
let mut choice_indeces: Vec<usize> = Vec::new();
index += 1;
choices.push
(match tokens.get(index) {
Some(tokenise::Token::String(s)) => s.clone(),
Some(_) => return Err("Unexpected token".to_string()),
None => return Err("File unexpectedly reached termination point".to_string()),
});
choice_indeces.push(index+1);
index += 2;
let next_token = match tokens.get(index) {
Some(tokenise::Token::String(s)) => s.clone(),
Some(_) => return Err("Unexpected token".to_string()),
None => return Err("File unexpectedly reached termination point".to_string()),
};
while next_token == "or"
{
index += 1
choices.push
(match tokens.get(index) {
Some(tokenise::Token::String(s)) => s.clone(),
Some(_) => return Err("Unexpected token".to_string()),
None => return Err("File unexpectedly reached termination point".to_string()),
});
}
},
/*
"choice" =>
{
let (_,jump_points) = match choice_parse(index+1, tokens, data_to_send)
{
Ok((increment,jump_point)) => (increment,jump_point),
Err(error) => return Err(error),
};
if rx.recv().is_err() { warn!("Error sending choices to client"); }
let (_, choice, _) = match rx.recv()
{
Ok((_,choice,_)) => (None::<bool>, choice, None::<String>),
Err(err) =>
{
warn!("Error receiving choice from client, defaulting to choice 0 {err}");
(None::<bool>, 0, None::<String>)
}
};
index = jump_points[choice];
info!("CHOICE command with {} choices",jump_points.len());
debug!("{jump_points:?} {choice} {index}");
continue 'parse_loop
},
*/
"or" =>
{
info!("OR command, jumping over");
index += 2;
continue
},
_ =>
{
warn!("Invalid command: {token}");
}
}
}
if rx.recv().is_err()
{
warn!("Some issue with api");
}
}
}
// Parse the options in a choice clause and returns the idexes of the code blocks
fn choice_parse
(
index: usize,
tokens: &[&str],
data_to_send: &Arc<Mutex<api::DataToSend>>,
) -> Result<(usize, Vec<usize>), String>
{
let mut sum_index: usize = index;
let mut choices: Vec<String> = Vec::new();
let mut choice_indeces: Vec<usize> = Vec::new();
// Ensure the index is valid (the index is not beyond the vector)
// Get the initial choice
let (choice_string, counter) = strings::extract_quoted(&tokens[sum_index..])
.ok_or_else(|| "No choice string".to_string())?;
sum_index += counter;
choices.push(choice_string);
choice_indeces.push(sum_index+1);
sum_index += strings::closing_char(&tokens[sum_index..], '{','}')
.ok_or_else(|| "No closing brace".to_string())? + 1;
// Find all the alternate choices labelled with OR
// Fill out the choices vector with all the choice strings
while tokens[sum_index].to_lowercase() == "or"
{
let (choice_string, counter) = strings::extract_quoted(&tokens[sum_index+1..])
.ok_or_else(|| "No choice string".to_string())?;
sum_index += counter;
choices.push(choice_string);
choice_indeces.push(sum_index+2);
sum_index += strings::closing_char(&tokens[sum_index..], '{','}')
.ok_or_else(|| "No closing brace".to_string())? + 1;
}
debug!("{choices:?}");
// Send the choices to the Client via the API
api::modify_data(data_to_send, "choice".to_string(), String::new(), String::new(), choices);
// Return the choice indeces
Ok((sum_index + 1, choice_indeces))
}
+32 -31
View File
@@ -9,6 +9,7 @@ use crate::
HashMap,
Arc,
Mutex,
VecDeque,
config,
mpsc::Sender,
info,
@@ -18,9 +19,8 @@ use crate::
Deserialize,
};
#[derive(Debug, Deserialize, Serialize, Clone)]
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct DataToSend {
pub id: u32,
pub action_type: String,
pub content: String,
pub character: String,
@@ -32,31 +32,28 @@ pub struct DataToSend {
// tx to allow the program executor to move onto the next bit of code
pub async fn api_process
(
data_to_send: Arc<Mutex<DataToSend>>,
happening_queue: Arc<Mutex<VecDeque<DataToSend>>>,
characters: Arc<Mutex<HashMap::<String,character::Character>>>,
tx: Sender<(bool, usize, String)>,
tx: Sender<(usize, String)>,
)
{
// This data must be passed through to the api route in order to be used
let data_filter = warp::any().map(move || Arc::clone(&data_to_send));
let happening_queue_filter = warp::any().map(move || Arc::clone(&happening_queue));
let characters_filter = warp::any().map(move || Arc::clone(&characters));
let tx_filter = warp::any().map(move || tx.clone());
let tx_filter2 = tx_filter.clone();
let tx_filter3 = tx_filter.clone();
info!("Running server");
let tx_filter1 = warp::any().map(move || tx.clone());
let tx_filter2 = tx_filter1.clone();
// The server route is loaded at address:port/happening
let main = warp::path("happening")
.and(warp::get())
.and(data_filter)
.and(tx_filter)
.and(happening_queue_filter)
// Perform this code on a GET request
.map(|state: Arc<Mutex<DataToSend>>, tx_handle: Sender<(bool,usize,String)>|
.map(|queue: Arc<Mutex<VecDeque<DataToSend>>>|
{
//debug!("GET: {state:?}");
let reply = state.as_ref();
let _ = tx_handle.send((true,0,String::new()));
let mut queue = queue.lock().unwrap_or_exit("Queue Mutex was poisoned", 2);
let reply = queue.pop_front().unwrap_or_default();
drop(queue);
warp::reply::json(&reply) // Send the reply data (data_to_send formatted as JSON)
}).boxed();
let characters = warp::path("character")
@@ -90,26 +87,27 @@ pub async fn api_process
let choice = warp::path("choice")
.and(warp::post())
.and(warp::body::json())
.and(tx_filter2)
.map(|index: usize, tx_handle: Sender<(bool,usize,String)>| {
.and(tx_filter1)
.map(|index: usize, tx_handle: Sender<(usize,String)>| {
debug!("Choice: {index}");
let _ = tx_handle.send((true,index,String::new()));
let _ = tx_handle.send((index,String::new()));
let reply = "ack";
warp::reply::json(&reply)
}).boxed();
let input = warp::path("input")
.and(warp::post())
.and(warp::body::json())
.and(tx_filter3)
.map(|input: String, tx_handle: Sender<(bool, usize, String)>|
.and(tx_filter2)
.map(|input: String, tx_handle: Sender<(usize, String)>|
{
let _ = tx_handle.send((true,0,input));
let _ = tx_handle.send((0,input));
let reply = "ack";
warp::reply::json(&reply)
}).boxed();
let routes = main.or(characters).or(choice).or(input);
// Start the server
info!("Running server");
warp::serve(routes)
.run(([127, 0, 0, 1],config::API_PORT))
.await;
@@ -117,20 +115,23 @@ pub async fn api_process
// On fail, quit safely
// If successful, return nothing
pub fn modify_data
pub fn modify_data // TODO rename
(
data_to_send: &Arc<Mutex<DataToSend>>,
happening_queue: &Arc<Mutex<VecDeque<DataToSend>>>,
action_type: String,
content: String,
character_name: String,
character: String,
choices: Vec<String>,
)
{
let mut data = data_to_send.lock().unwrap_or_exit("Data to send Mutex was poisoned",2);
data.id += 1;
data.action_type = action_type;
data.content = content;
data.character = character_name;
data.choices = choices;
drop(data);
let mut queue = happening_queue.lock().unwrap_or_exit("Data to send Mutex was poisoned",2);
let new_data = DataToSend {
action_type,
content,
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)))
+27 -13
View File
@@ -11,7 +11,11 @@ use std::
env::args,
fs::File,
io::Read,
collections::HashMap,
collections::
{
HashMap,
VecDeque,
},
sync::{Arc, Mutex, mpsc},
};
use log::
@@ -36,6 +40,7 @@ use crate::
{
traits::UnwrapOrExit,
};
use regex::Regex;
#[tokio::main]
async fn main()
@@ -70,25 +75,26 @@ async fn main()
},
};
// Initialise the data strcut that will be sent out during API GET requests
let data_to_send = Arc::new(Mutex::new(api::DataToSend
{
id: 0,
let happening_stack = Arc::new(Mutex::new(
VecDeque::from([api::DataToSend{
action_type: "begin".to_owned(),
content: String::new(), // TODO send title and description
character: String::new(),
choices: vec![],
}));
}])
));
// setup the api stuff //
// Make clones of the data Arc for the two processes
let data_clone1 = Arc::clone(&data_to_send);
//let data_clone1 = Arc::clone(&data_to_send);
let happening_stack1 = Arc::clone(&happening_stack);
let characters_clone1 = Arc::clone(&characters);
let tx_clone = tx;
// Spawn a thread for warp api server
tokio::spawn(
async move {
api::api_process(data_clone1, characters_clone1, tx_clone).await;
api::api_process(happening_stack1, characters_clone1, tx_clone).await;
});
// setup the parsing stuff //
@@ -103,21 +109,29 @@ 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:?}");
let data_clone2 = Arc::clone(&data_to_send);
// 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
info!("DSL parsing begun");
match parsing::token_parse(&tokens, &labels, &characters_clone2, &data_clone2, &rx)
match parsing::token_parse(&tokens, &labels, &characters_clone2, &happening_stack2, &rx)
{
// Exit with error or success
Ok(()) =>
{
api::modify_data(&data_to_send, "end".to_string(), String::new(), String::new(), vec![]);
// TODO fix quitting instantly
let _ = rx.recv(); // Wait for the client to respond
api::modify_data(&happening_stack, "end".to_string(), String::new(), String::new(), vec![]);
let _ = rx.recv();
info!("Program exited successfully");
exit(0);
},
+34 -103
View File
@@ -1,4 +1,3 @@
use std::collections::HashMap;
use crate::
{
// Internal code
@@ -10,12 +9,17 @@ use crate::
mpsc::Receiver,
Arc,
Mutex,
VecDeque,
HashMap,
info,
debug,
warn,
};
mod character_parse;
mod keyword_parse;
mod identifier_parse;
// Parse the tokens in a file
// Returns success or an error string
@@ -23,25 +27,26 @@ pub fn token_parse(
tokens: &[tokenise::Token],
labels: &HashMap<String,usize>,
characters: &Arc<Mutex<HashMap::<String, character::Character>>>,
data_to_send: &Arc<Mutex<api::DataToSend>>,
rx: &Receiver<(bool,usize,String)>,
happening_queue: &Arc<Mutex<VecDeque<api::DataToSend>>>,
rx: &Receiver<(usize,String)>,
) -> Result<(),String>
{
let mut index: usize = 0;
if rx.recv().is_err()
{
warn!("Some issue with api");
// TODO eh?
}
let mut variables: HashMap<String, tokenise::Value> = HashMap::new();
info!("Client has connected");
// Run an infinite loop
'parse_loop: loop
loop
{
debug!("Reading {index}");
// Get the next token
let token: String = match tokens.get(index)
match tokens.get(index)
{
Some(tokenise::Token::Keyword(s)) => s.clone(),
// 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, &mut variables, rx)?;
},
// Ignore closing braces and jump over opening brace blocks
Some(tokenise::Token::Bracket((bracket,new_index))) =>
{
@@ -51,12 +56,11 @@ pub fn token_parse(
warn!("Unexpected brace block, jumping over...");
index = new_index + 1;
}
continue 'parse_loop
},
// Handle a character
Some(tokenise::Token::Character(character_name)) => // TODO add support for narrator
Some(tokenise::Token::Character(character_name)) =>
{
index = match character_parse::character_parse(index+1,tokens,character_name.clone(),characters,data_to_send)
index = match character_parse::character_parse(index+1,tokens,character_name.clone(),characters,happening_queue,&variables)
{
Ok(increment) => increment,
Err((err,increment)) =>
@@ -65,101 +69,28 @@ pub fn token_parse(
increment
},
};
if rx.recv().is_err() { warn!("Some issue with api"); }
continue 'parse_loop
}
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;
continue 'parse_loop
},
None => return Err("File unexpectedly reached termination point".to_string()),
};
debug!("{index}: {token}");
}
// The instructions are related to characters
match token.to_lowercase().as_str()
{
"end" =>
{
info!("END command, exiting");
return Ok(()) // quit successfully
},
"choice" =>
{
let choice_indeces = choice_parse(tokens, index, data_to_send)?;
debug!("{choice_indeces:?}");
if rx.recv().is_err() { warn!("Error sending choices to client"); }
let choice = match rx.recv()
{
Ok((_,choice,_)) => choice_indeces[choice],
Err(err) =>
{
warn!("Error receiving choice from client, defaulting to choice 0 {err}");
0
}
};
index = choice;
continue 'parse_loop
},
"or" =>
{
info!("OR command, jumping over");
index += 2;
let new_index = tokenise::get_closing_index(tokens, index)?;
index = new_index;
continue 'parse_loop
},
// Jump to a particular index based on a label eg GOTO character_check
"goto" =>
{
index += 1;
let label = tokenise::get_keyword_token(tokens, index)?;
index = if let Some(label_index) = labels.get(&label) { *label_index }
else
{
warn!("Label {label} does not exist");
index + 1
};
debug!("Jumping to {index}");
continue 'parse_loop
}
_ =>
{
warn!("Invalid command: {token}");
index += 1;
}
}
if rx.recv().is_err() { warn!("Some issue with api"); }
}
}
fn choice_parse(tokens: &[tokenise::Token], mut index: usize, data_to_send: &Arc<Mutex<api::DataToSend>>,)
-> Result<Vec<usize>, String>
{
let mut next_token: String = "or".to_string();
let mut choices: Vec<String> = Vec::new();
let mut choice_indeces: Vec<usize> = Vec::new();
while next_token == "or"
{
index += 1;
choices.push
(
tokenise::get_string_token(tokens, index)?
);
index += 1;
choice_indeces.push(index+1);
index = match tokenise::get_closing_index(tokens,index)
{
Ok(new_index) => new_index + 1,
Err(_) => break,
};
next_token = match tokenise::get_keyword_token(tokens, index)
{
Ok(string) => string,
Err(_) => break,
}
};
api::modify_data(data_to_send, "choice".to_string(), String::new(), String::new(), choices);
Ok(choice_indeces)
}
+12 -10
View File
@@ -9,6 +9,7 @@ use crate::
Mutex,
Arc,
HashMap,
VecDeque,
info,
warn,
debug,
@@ -23,7 +24,8 @@ pub fn character_parse
tokens: &[tokenise::Token],
character_name: String,
characters: &Arc<Mutex<HashMap::<String, character::Character>>>,
data_to_send: &Arc<Mutex<api::DataToSend>>,
happening_queue: &Arc<Mutex<VecDeque<api::DataToSend>>>,
variables: &HashMap<String, tokenise::Value>,
) -> Result<usize,(String,usize)>
{
let mut sum_index: usize = index;
@@ -44,10 +46,10 @@ 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(data_to_send, "output".to_string(), output, character_name, vec![]);
api::modify_data(happening_queue, "output".to_string(), output, character_name, vec![]);
},
// Change the property of the selected character eg @tim CHANGE name "Bill Buffins"
// will change the character with ID tim to "Bill Buffins"; a character's ID cannot change
@@ -55,17 +57,17 @@ 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)
&& character.set_field(&feature, &string)
.is_err() { warn!("Feature {feature} does not exist") }
drop(characters);
api::modify_data(data_to_send, "change".to_string(), String::new(), character_name, vec![]);
api::modify_data(happening_queue, "change".to_string(), String::new(), character_name, vec![]);
},
// These two are mainly just actions performed by the frontend client, so just tell the client to move/animate
// the character and not much other processing needed on the serverside
@@ -73,8 +75,8 @@ pub fn character_parse
{
sum_index += 1;
let content = tokenise::get_keyword_token(tokens, sum_index)
.map_err(|err| (err, index))?;
api::modify_data(data_to_send, keyword.to_lowercase(), content, character_name, vec![]);
.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
// character command
+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
}
+165
View File
@@ -0,0 +1,165 @@
use crate::
{
tokenise,
api,
HashMap,
Arc,
Mutex,
VecDeque,
warn,
debug,
info,
mpsc::Receiver,
};
use super::identifier_parse;
pub fn keyword_parse(
tokens: &[tokenise::Token],
token: &str,
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>
{
match token.to_lowercase().as_str()
{
"choice" =>
{
(index,_) = choice_parse(tokens, index, happening_queue, rx,variables)?;
},
"if" =>
{
// TODO can this go in a function?
let mut keyword = "if".to_string();
while keyword == "if" || keyword == "elif" || keyword == "else" // TODO less beefy??
{
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
}
},
"or" =>
{
info!("OR command, jumping over");
index += 2;
let new_index = tokenise::get_closing_index(tokens, index)?;
index = new_index;
},
// 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 }
else
{
warn!("Label {label} does not exist");
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}, index {index}");
index += 1;
}
}
Ok(index)
}
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();
let mut choice_indeces: Vec<usize> = Vec::new();
while next_token == "or"
{
index += 1;
choices.push
(
tokenise::get_string_token(tokens, index,variables)?
);
index += 1;
choice_indeces.push(index+1);
index = match tokenise::get_closing_index(tokens,index)
{
Ok(new_index) => new_index + 1,
Err(_) => break,
};
next_token = match tokenise::get_keyword_token(tokens, index)
{
Ok(string) => string,
Err(_) => break,
}
};
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 -9
View File
@@ -1,30 +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
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()),
}
}
@@ -33,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()),
}
}
@@ -42,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)
@@ -63,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 '
@@ -77,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(':')
{
@@ -110,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
+17 -6
View File
@@ -1,6 +1,12 @@
import requests
import os
import time
import sys
debug = True
try:
if sys.argv[1] == "silent": debug = False
except:
debug = True
# Loop and get new api
def main():
@@ -9,22 +15,21 @@ def main():
while True:
try:
response = api_get()
if response["id"] != id:
id = response["id"]
print(response)
if debug: print(response)
match response["action_type"]:
case "output":
character = get_character(response["character"])
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")
requests.post("127.0.0.1:20264", json=0);
os._exit(0)
else:
continue
except:
print("Server not up or cannot be reached")
input() # Enter to go to next loop (testing)
@@ -41,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")