Compare commits

..

2 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
15 changed files with 139 additions and 23 deletions
+2 -5
View File
@@ -7,7 +7,7 @@ For help with syntax, see [this documentation](/docs/SYNTAX.md)
## Install ## 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 ## Technical Details
@@ -103,14 +103,11 @@ Move a character with @CHARACTER to fr
> fr means front-right for instance. > fr means front-right for instance.
## TODO ## TODO
- Variables
- /about.json - /about.json
- If/Else if/Else
- PAN
- INPUT
- tokeniser check for lack of END - tokeniser check for lack of END
- Fix no closing brace edge case - Fix no closing brace edge case
- Support single quotes for strings - Support single quotes for strings
- backslashes in strings
- Brace index getter check for closing - Brace index getter check for closing
- Proper Error messages centralised??? - Proper Error messages centralised???
+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 ## Characters
See [Character documentation](/docs/CHARACTER.md) for more info See [Character documentation](/docs/CHARACTER.md) for more info
Referencing a character using the @, @NARRATOR is reserved for the Narrator.<br/> 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 Move a character with @CHARACTER to fr
Outputting can be done with @CHARACTER says "string", see more at (outputs)[##outputs]
> [!NOTE] > [!NOTE]
> fr means front-right for instance. > 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 ## Outputs
``` ```
@CHARACTER says "this string @CHARACTER says "this string
is multi-line is multi-line
and ends with a" 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 ## Variables
@@ -41,18 +76,20 @@ Variables are referenced with the \$, only integers will be supported.<br/>
Condition based: 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: 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 ## Positioning
``` ```
@CHARACTER to position @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 ## Other
``` ```
@@ -89,3 +146,4 @@ GOTO label
## Ending ## Ending
`END` to exit out of the story `END` to exit out of the story
+1
View File
@@ -461,6 +461,7 @@ version = "0.0.3"
dependencies = [ dependencies = [
"env_logger", "env_logger",
"log", "log",
"regex",
"serde", "serde",
"serde-patch", "serde-patch",
"serde_json", "serde_json",
+1
View File
@@ -25,6 +25,7 @@ unwrap_used = "warn"
[dependencies] [dependencies]
env_logger = "0.11.10" env_logger = "0.11.10"
log = "0.4.29" log = "0.4.29"
regex = "1.12.3"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde-patch = "0.2.3" serde-patch = "0.2.3"
serde_json = "1.0.149" serde_json = "1.0.149"
+5 -2
View File
@@ -71,8 +71,11 @@ pub fn character_parse(archive: &mut ZipArchive<File>)
// Serialise this to a HashMap // Serialise this to a HashMap
let characters: HashMap<String, Character> = let characters: HashMap<String, Character> =
serde_json::from_str(&file_contents) serde_json::from_str::<HashMap<String,Character>>(&file_contents)
.map_err (|err| format!("Invalid JSON in characters.json: {err}"))?; .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"); info!("Parsed characters from characters.json");
debug!("{characters:?}"); debug!("{characters:?}");
Ok(Arc::new(Mutex::new(characters))) Ok(Arc::new(Mutex::new(characters)))
+1
View File
@@ -40,6 +40,7 @@ use crate::
{ {
traits::UnwrapOrExit, traits::UnwrapOrExit,
}; };
use regex::Regex;
#[tokio::main] #[tokio::main]
async fn main() async fn main()
+3
View File
@@ -10,6 +10,7 @@ use crate::
VecDeque, VecDeque,
warn, warn,
debug, debug,
info,
mpsc::Receiver, mpsc::Receiver,
}; };
use super::keyword_parse; use super::keyword_parse;
@@ -62,6 +63,7 @@ pub fn identifier_parse
"input" => "input" =>
{ {
api::modify_data(happening_queue, "input".to_string(), String::new(), String::new(), Vec::new()); api::modify_data(happening_queue, "input".to_string(), String::new(), String::new(), Vec::new());
info!("Waiting for client input");
let input = match rx.recv() let input = match rx.recv()
{ {
Ok((_,input)) => input, Ok((_,input)) => input,
@@ -72,6 +74,7 @@ pub fn identifier_parse
} }
}; };
variables.insert(identifier.to_owned(), tokenise::Value::String(input)); variables.insert(identifier.to_owned(), tokenise::Value::String(input));
sum_index += 1;
}, },
_ => _ =>
{ {
+11 -2
View File
@@ -89,6 +89,7 @@ pub fn keyword_parse(
// Jump to a particular index based on a label eg GOTO character_check // Jump to a particular index based on a label eg GOTO character_check
"goto" => "goto" =>
{ {
info!("GOTO command, jumping there");
index += 1; index += 1;
let label = tokenise::get_keyword_token(tokens, index)?; let label = tokenise::get_keyword_token(tokens, index)?;
index = if let Some(label_index) = labels.get(&label) { *label_index } index = if let Some(label_index) = labels.get(&label) { *label_index }
@@ -98,10 +99,17 @@ pub fn keyword_parse(
index + 1 index + 1
}; };
debug!("Jumping to {index}"); 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; index += 1;
} }
} }
@@ -142,6 +150,7 @@ pub fn choice_parse
} }
}; };
api::modify_data(happening_queue, "choice".to_string(), String::new(), String::new(), choices.clone()); api::modify_data(happening_queue, "choice".to_string(), String::new(), String::new(), choices.clone());
info!("Waiting for client choice");
debug!("{choice_indeces:?}"); debug!("{choice_indeces:?}");
let choice = match rx.recv() let choice = match rx.recv()
{ {
+40 -3
View File
@@ -1,6 +1,7 @@
use crate:: use crate::
{ {
HashMap, HashMap,
Regex,
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -49,6 +50,19 @@ pub enum Comparison
LessOrEqual, 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 // TODO error reporting for all of these
pub fn get_operator_token(tokens: &[Token], index: usize) pub fn get_operator_token(tokens: &[Token], index: usize)
@@ -105,7 +119,11 @@ pub fn get_string_token(tokens: &[Token], index: usize, variables: &HashMap<Stri
{ {
match val match val
{ {
Value::String(s) => Ok(s.clone()), Value::String(s) =>
{
let string = insert_variables_into_string(s, variables);
Ok(string)
},
_ => Err("Unexpected value type".to_string()), _ => Err("Unexpected value type".to_string()),
} }
}, },
@@ -116,7 +134,11 @@ pub fn get_string_token(tokens: &[Token], index: usize, variables: &HashMap<Stri
.ok_or(format!("Variable {iden} not initialised"))?; .ok_or(format!("Variable {iden} not initialised"))?;
match val match val
{ {
Value::String(s) => Ok(s.clone()), Value::String(s) =>
{
let string = insert_variables_into_string(s, variables);
Ok(string)
},
_ => Err("Unexpected value type".to_string()), _ => Err("Unexpected value type".to_string()),
} }
}, },
@@ -125,6 +147,19 @@ pub fn get_string_token(tokens: &[Token], index: usize, variables: &HashMap<Stri
} }
} }
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> // Tokenise the story to Vec<Token>
// It can contain sub-objects (using recursive calls) // It can contain sub-objects (using recursive calls)
// TODO check for END // TODO check for END
@@ -165,7 +200,7 @@ pub fn tokenise(file_contents: &str)
{ {
let mut chars = item.chars(); let mut chars = item.chars();
chars.next(); 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 // Strings
else if item.starts_with('"') // TODO support ' else if item.starts_with('"') // TODO support '
@@ -227,6 +262,8 @@ pub fn tokenise(file_contents: &str)
Ok((tokenised_data, labels)) Ok((tokenised_data, labels))
} }
// TODO support strings that contain " in them by using a backslash
fn tokenise_string(space_seperated: &[&str], mut index: usize) fn tokenise_string(space_seperated: &[&str], mut index: usize)
-> Option<(usize, String)> -> Option<(usize, String)>
{ {
BIN
View File
Binary file not shown.
+7 -5
View File
@@ -2,11 +2,11 @@ import requests
import os import os
import time import time
import sys import sys
debug = False debug = True
try: try:
if sys.argv[1] == "debug": debug = True if sys.argv[1] == "silent": debug = False
except: except:
debug = False debug = True
# Loop and get new api # Loop and get new api
def main(): def main():
@@ -22,7 +22,9 @@ def main():
output(character, response["content"]) output(character, response["content"])
case "choice": case "choice":
user_choice = choice(response["choices"]) user_choice = choice(response["choices"])
time.sleep(0.5) continue
case "input":
get_input()
continue continue
case "end": case "end":
print("Exitting successfully") print("Exitting successfully")
@@ -46,7 +48,7 @@ def choice(choices):
def get_input(): def get_input():
api_url = "http://localhost:20264/input" api_url = "http://localhost:20264/input"
input_string = input() input_string = input("Input: ")
requests.post(api_url, json=input_string); requests.post(api_url, json=input_string);