#ifndef DIALOGUE_H
#define DIALOGUE_H
#ifndef _TM_JSON_H_INCLUDED_
# error Include tm_json.h before dialogue.h so dialogue can be loaded.
#endif
#include <stdio.h>
#include <cstdlib>
#include <ctime>
#include <cmath>
#include <functional>
#include <map>
#include <string>
#include <vector>
// Moddable fopen.
#ifndef DIALOGUE_FOPEN
#define DIALOGUE_FOPEN fopen
#endif
namespace dialogue {
// Arithmetic operations on key/value pairs.
struct Operation {
std::string key;
char op;
double valueNumber;
std::string valueKey; // Has length > 0 if value is to be derived from variable.
};
// Interaction contexts, like multiple-choice or visual novel screens.
struct Screen {
std::string id;
std::string bg;
std::string fg;
std::vector<Operation> exec;
std::string caption;
std::vector<std::string> lines;
std::vector<std::string> ids;
};
class Talk {
public:
// Set fallbackDir to alternate folder containing dialogue.
std::string fallbackDir;
std::string file;
Screen screen;
std::vector<Screen> screens;
std::map<std::string,double> numbers;
std::map<std::string,std::string> strings;
std::map<std::string,std::vector<std::string>> arrays;
std::function<double(std::string)> callback;
void go( const std::string &id );
void append( std::string filePath );
bool hasScreen( const std::string &id );
Screen getScreen( const std::string &id );
void setScreen( const Screen &scr );
bool hasVariable( const std::string &key );
double getVariable( std::string key );
void setVariable( const std::string &key, double valueNumber );
int parseJSON( std::string &text );
int parseCSL( std::string &text );
std::string stringifyNumber( double n );
std::string stringifyArray( const std::vector<std::string> &arr );
Operation getOperation( std::string key, double valueNumber, const std::string &valueKey );
void operate( Operation o );
void transformString( std::string &str );
Talk( const std::string &filePath = "" );
};
void Talk::go( const std::string &id ){
if( ( id.length() >= 5 && id.substr( id.length() - 5 ) == ".json" )
|| ( id.length() >= 4 && id.substr( id.length() - 4 ) == ".csl" ) ){
screens.clear();
std::string filePath = file.substr( 0, file.find_last_of( '/' ) + 1 ) + id;
append( filePath );
if( file == filePath ){
screen = getScreen( "init" );
}
std::srand( std::time( NULL ) );
}else{
screen = getScreen( id );
}
transformString( screen.caption );
for( std::string &s : screen.lines ){
transformString( s );
}
for( std::string &s : screen.ids ){
transformString( s );
}
// Copy exec to a local stack to allow nested callback operations.
std::vector<Operation> op_stack = screen.exec;
for( Operation &o : op_stack ){
operate( o );
}
}
void Talk::append( std::string filePath ){
FILE *stream = DIALOGUE_FOPEN( filePath.c_str(), "rb" );
if( !stream ){
// Attempt to load from the fallback directory.
std::string altPath;
size_t pos = filePath.find_last_of( '/' );
if( pos != filePath.length() - 1 ){
// File does not look like a directory.
if( pos == std::string::npos ){
altPath = fallbackDir + "/" + filePath;
}else{
altPath = fallbackDir + "/" + filePath.substr( pos + 1 );
}
stream = DIALOGUE_FOPEN( altPath.c_str(), "rb" );
}
}
if( !stream ){
screen = {
"ERROR",
"",
"",
{},
"ERROR: File \"" + filePath + "\" could not be loaded.",
{ "Go to init" },
{ "init" }
};
return;
}
std::string text = "";
char buf[4096];
while( size_t len = fread( buf, 1, sizeof( buf ), stream ) ){
text += std::string( buf, len );
}
fclose( stream );
if( filePath.length() >= 5
&& filePath.substr( filePath.length() - 5 ) == ".json" ){
if( parseJSON( text ) == 0 ) file = filePath;
}else{
if( parseCSL( text ) == 0 ) file = filePath;
}
}
bool Talk::hasScreen( const std::string &id ){
for( Screen &s : screens ){
if( s.id == id ){
return true;
}
}
return false;
}
Screen Talk::getScreen( const std::string &id ){
for( Screen &s : screens ){
if( s.id == id ){
return s;
}
}
return {
"ERROR",
"",
"",
{},
"ERROR: Dialogue screen \"" + id + "\" not found.",
{ "Go to init" },
{ "init" }
};
}
void Talk::setScreen( const Screen &scr ){
for( Screen &s : screens ){
if( s.id == scr.id ){
s = scr;
return;
}
}
screens.push_back( scr );
}
bool Talk::hasVariable( const std::string &key ){
return numbers.count( key ) > 0;
}
double Talk::getVariable( std::string key ){
// Apply string replacement to the key.
transformString( key );
if( key == "RAND" ){
// Returns a (pseudo)random number.
return std::rand();
}else if( key.length() >= 8 && key.substr( 0, 8 ) == "CALLBACK" ){
// Returns the result of the callback function.
std::string arg = key.length() >= 10 ? key.substr( 9 ) : "";
return callback( arg );
}else if( key.length() >= 6 && key.substr( 0, 6 ) == "STRLEN" ){
// Returns the length of the parameter string.
return std::max( key.length() - 7.0, 0.0 );
}else if( key.length() >= 6 && key.substr( 0, 6 ) == "ARRLEN" ){
// Returns the length of the named array if it exists, otherwise 0.
if( key.back() == '@' ) key.resize( key.length() - 1 );
if( key.length() >= 8 ){
auto it = arrays.find( key.substr( 7 ) );
if( it != arrays.end() ) return it->second.size();
}
}else if( key.length() >= 5 && key.substr( 0, 5 ) == "GOSUB" ){
// Executes the specified dialogue, then returns 0.
std::string arg = key.length() >= 7 ? key.substr( 6 ) : "";
go( arg );
}else{
// Returns the value of a variable if it exists, otherwise 0.
auto it = numbers.find( key );
if( it != numbers.end() ) return it->second;
}
return 0.0;
}
void Talk::setVariable( const std::string &key, double valueNumber ){
numbers[key] = valueNumber;
}
int Talk::parseJSON( std::string &text ){
// All code dependent on tm_json.h is contained within this function.
auto allocatedDocument = jsonAllocateDocumentEx( text.c_str(), text.size(), JSON_READER_JSON5 );
auto err = allocatedDocument.document.error;
if( err.type != JSON_OK ){
screen = {
"ERROR",
"",
"",
{},
"JSON ERROR: " +
std::string( jsonGetErrorString( err.type ) ) + " at line " +
std::to_string( err.line ) + ":" +
std::to_string( err.column ),
{ "Go to init" },
{ "init" }
};
jsonFreeDocument( &allocatedDocument );
return err.type;
}
auto viewToString = []( JsonStringView str ){
return std::string( str.data, str.size );
};
for( auto s : allocatedDocument.document.root.getObject() ){
std::vector<Operation> exec;
for( auto o : s.value["exec"].getObject() ){
exec.push_back( getOperation(
viewToString( o.name ),
o.value.getDouble(),
o.value.isString() ? viewToString( o.value.getString() ) : ""
) );
}
std::vector<std::string> lines;
for( auto l : s.value["lines"].getArray() ) lines.push_back( viewToString( l.getString() ) );
std::vector<std::string> ids;
for( auto i : s.value["ids"].getArray() ) ids.push_back( viewToString( i.getString() ) );
Screen scr = {
viewToString( s.name ),
viewToString( s.value["bg"].getString() ),
viewToString( s.value["fg"].getString() ),
exec,
viewToString( s.value["caption"].getString() ),
lines,
ids
};
// Overwrite the screen with the same id if applicable.
bool over = false;
for( auto &screen : screens ){
if( screen.id == scr.id ){
screen = scr;
over = true;
break;
}
}
if( !over ) screens.push_back( scr );
}
jsonFreeDocument( &allocatedDocument );
return 0;
}
int Talk::parseCSL( std::string &text ){
// SET CURRENT SCREEN TO "init".
// FOR LINE IN text:
// IF LINE[0] == '[':
// FIND ']' AND SET CURRENT SCREEN TO THE CONTENTS.
// ELSE:
// LEX AND ADD LINE TO exec.
auto GetScreenPointer = [&]( const std::string &id ){
for( Screen &s : screens ){
if( s.id == id ){
return &s;
}
}
screens.push_back( {} );
screens.back().id = id;
return &screens.back();
};
Screen *screen_ptr = GetScreenPointer( "init" );
auto Lex = [&]( std::string line ){
if( line.length() && line[0] == '#' ) return;
if( line.length() >= 2 && line[0] == '[' ){
line.erase( 0, 1 );
size_t bracket_at = line.find_last_of( ']' );
if( bracket_at == std::string::npos ) return;
line.erase( bracket_at );
if( line.length() > 0 && line != screen_ptr->id )
screen_ptr = GetScreenPointer( line );
}else{
size_t colon_at = line.find_first_of( ':' );
if( colon_at == std::string::npos ) return;
std::string val_str = line.substr( colon_at + 1 );
char* p;
double num = std::strtod( val_str.c_str(), &p );
Operation o = getOperation(
line.substr( 0, colon_at ),
num,
""
);
// If defining a string or array, or value is non-numeric,
// treat value as a string.
if( o.op == '$' || o.op == '@' || *p ) o.valueKey = val_str;
// Add the operation to the screen pointer.
screen_ptr->exec.push_back( o );
}
};
// Lex the text line by line.
size_t start = 0, end = 0;
while( ( end = text.find_first_of( "\r\n", start ) ) != std::string::npos ){
Lex( text.substr( start, end - start ) );
start = end + 1;
}
Lex( text.substr( start ) );
return 0;
}
// If n can be losslessly round-tripped between a double, a long long,
// and a double, return a string without a decimal. Otherwise, return a
// string with sprintf-style floating-point notation.
std::string Talk::stringifyNumber( double n ){
long long rounded = std::llround( n );
if( (double)rounded == n ){
// Number is either an integer or so weird it's an extreme corner case.
return std::to_string( rounded );
}else{
// Number is not an integer.
return std::to_string( n );
}
}
// Return a ;-separated list of strings with each string's ; characters
// escaped as: \;
std::string Talk::stringifyArray( const std::vector<std::string> &arr ){
std::string result;
for( size_t i = 0; i < arr.size(); i++ ){
// Add separating ; characters.
if( i > 0 ) result += ";";
// Replace ; characters with \; and add the fragments to result.
const std::string &str = arr[i];
size_t start = 0, end = 0;
while( ( end = str.find( ';', start ) ) != std::string::npos ){
result += str.substr( start, end - start ) + "\\;";
start = end + 1;
}
result += str.substr( start );
}
return result;
}
Operation Talk::getOperation( std::string key, double valueNumber, const std::string &valueKey ){
char op = ':';
if( key.length() > 0 && key.find_last_of( "$@=!<>?%*/+-_" ) == key.length() - 1 ){
op = key.back();
key.resize( key.length() - 1 );
}
return {
key,
op,
valueNumber,
valueKey
};
}
void Talk::operate( Operation o ){
// String assignment.
if( o.op == '$' ){
transformString( o.valueKey );
strings[o.key] = o.valueKey;
if( o.key == "bg" ){
screen.bg = o.valueKey;
}else if( o.key == "fg" ){
screen.fg = o.valueKey;
}else if( o.key == "caption" ){
screen.caption = o.valueKey;
}
return;
}
// Array assignment.
if( o.op == '@' ){
transformString( o.valueKey );
// Make an empty array and get the iterator.
const auto [it, success] = arrays.insert( { o.key, {} } );
if( !success ) it->second = {};
// Parse the array.
size_t start = 0, off = 0, end = 0;
while( ( end = o.valueKey.find( ';', start + off ) ) != std::string::npos ){
// Allow \; escaping. TODO: \\ escaping.
if( end > 0 && o.valueKey[end - 1] == '\\' ){
// Remove the \ character from the string.
o.valueKey.erase( end - 1, 1 );
// `end` now points to the character after the ;.
off = end - start;
continue;
}
it->second.push_back( o.valueKey.substr( start, end - start ) );
start = end + 1;
off = 0;
}
it->second.push_back( o.valueKey.substr( start ) );
if( o.key == "lines" ){
screen.lines = it->second;
}else if( o.key == "ids" ){
screen.ids = it->second;
}
return;
}
// Numerical operations.
double in = o.valueNumber, out = getVariable( o.key );
if( o.valueKey.length() > 0 ){
in = getVariable( o.valueKey );
}
switch( o.op ){
case ':': out = in;
break;
case '=': out = ( out == in ) ? 1.0 : 0.0;
break;
case '!': out = ( out != in ) ? 1.0 : 0.0;
break;
case '<': out = ( out < in ) ? 1.0 : 0.0;
break;
case '>': out = ( out > in ) ? 1.0 : 0.0;
break;
case '?': out = out ? in : 0.0;
break;
case '%': out = std::fmod( out, in );
break;
case '*': out *= in;
break;
case '/': out /= in;
break;
case '+': out += in;
break;
case '-': out -= in;
break;
case '_': out = std::floor( in );
}
setVariable( o.key, out );
}
void Talk::transformString( std::string &str ){
bool replacing = false;
std::string outStr = "";
size_t start = 0, i = 0;
for( i = 0; i < str.length(); i++ ){
switch( str[i] ){
case '`':
if( replacing ){
if( i - start > 1 ){
// Add the variable value to the string.
std::string key = str.substr( start + 1, i - start - 1 );
if( key.back() == '$' ){
key.resize( key.length() - 1 );
if( key == "bg" ){
outStr += screen.bg;
}else if( key == "fg" ){
outStr += screen.fg;
}else if( key == "caption" ){
outStr += screen.caption;
}else{
auto it = strings.find( key );
if( it != strings.end() )
outStr += it->second;
}
}else if( key.back() == '@' ){
key.resize( key.length() - 1 );
if( key == "lines" ){
outStr += stringifyArray( screen.lines );
}else if( key == "ids" ){
outStr += stringifyArray( screen.ids );
}else{
auto it = arrays.find( key );
if( it != arrays.end() )
outStr += stringifyArray( it->second );
}
}else{
outStr += stringifyNumber( getVariable( key ) );
}
}
start = i + 1;
}else{
if( i >= start ){
outStr += str.substr( start, i - start );
}
start = i;
}
replacing = !replacing;
}
}
if( start < str.length() ){
outStr += str.substr( start );
}
str = outStr;
}
Talk::Talk( const std::string &filePath ){
callback = []( std::string param ){
// Suppress unused parameter warnings.
param = "";
return 0.0;
};
if( filePath.length() > 0 ){
go( filePath );
}
}
} // namespace dialogue
#endif // DIALOGUE_H