/include/dialogue.h (ee7c2a78df329c0689edc8dc808db5de9bc4f9e2) (14343 bytes) (mode 100755) (type blob)

#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


Mode Type Size Ref File
100644 blob 98 227abf3bfa53b2530dcc74495da7bd0ccdcb0775 .gitignore
100644 blob 225 9b00c2c2e7b4f0c1e338fdead65f17ba0af089c1 COPYING
100755 blob 43 45aea818a4a3202b2467509f28a481cce08834d2 Confectioner.command
100644 blob 12084 ca1d7e8e522f844c345c314ce6900efe486c41b5 Makefile
100644 blob 2723 b5a3f573f076ef740ca742ec9598043732e10c0e README.md
040000 tree - f854ec266095e18d4bee80b61191f009190533d9 base
100755 blob 156 84cb1387849f2ca98e53e43536d00af2dfabf7d3 caveconfec
100755 blob 28 41b0ef285892c86306eaa269f366dd04cb633d21 caveconfec.bat
100644 blob 195275 7dd3fffe9bb73069112dc76fcecd405fdc0c8235 confec.cpp
100644 blob 391231 cae78a24f09285accda1bd45c3361d92f4d55946 gamecontrollerdb.txt
040000 tree - a24928103d3d2c1929d85ab70447ac50d45a2384 include
100755 blob 1041 dd7c0bd7d8a6b4aeff53142375240872735d42a0 libs.cpp
100755 blob 27581 8350a63e947e8a4a55608fd090d128fef7b969a1 micropather.cpp
100644 blob 141235 f54e2d2631a628876a631456c043b77da5db78bd openjdk.pem
100755 blob 8 e9a74187b02a27b165dfa4f93bf6f060376d0ee6 steam_appid.txt
Hints:
Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://rocketgit.com/user/mse/ConfectionerEngine

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@ssh.rocketgit.com/user/mse/ConfectionerEngine

Clone this repository using git:
git clone git://git.rocketgit.com/user/mse/ConfectionerEngine

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a merge request:
... clone the repository ...
... make some changes and some commits ...
git push origin main