/include/dialogue.h (af2d2d667439b36c7cbfb3e54c62939cec5a567c) (23592 bytes) (mode 100644) (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;
};

// Functions.
struct CSLFunction {
	std::vector<std::string> argNames;
	std::vector<Operation> exec;
};

// Dynamic values.
struct CSLDynamic {
	int type; // type is -1 for error, 0 for number, 1 for string, 2 for array.
	double valueNumber;
	std::string valueString;
	std::vector<std::string> valueArray;
};

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,CSLFunction> functions;
		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;
		CSLDynamic go( const std::string &id );
		void append( std::string filePath );
		bool hasScreen( const std::string &id );
		bool hasFunction( const std::string &func );
		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 );
		CSLDynamic callFunction( const std::string &func, const std::vector<std::string> &args );
		std::vector<std::string> tokenize( const std::string &str, const std::string &sep = "\t\x20()," );
		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 );
		CSLDynamic operate( Operation o );
		void transformString( std::string &str );
		Talk( const std::string &filePath = "" );
};

// Load a file, go to a screen, or call a function.
CSLDynamic Talk::go( const std::string &id ){
	// go returns 0.0 instead of nil.
	CSLDynamic zeroDynamic = { 0, 0.0, "", {} };
	if( ( id.length() >= 5 && id.substr( id.length() - 5 ) == ".json" )
		|| ( id.length() >= 4 && id.substr( id.length() - 4 ) == ".csl" ) ){
		// Load the file specified by id.
		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 if( id.find( ")" ) == std::string::npos ){
		// Go to the screen specified by id.
		screen = getScreen( id );
	}else{
		// Call a function.
		auto tokens = tokenize( id );
		if( tokens.size() > 0 ){
			std::string func = tokens[0];
			tokens.erase( tokens.begin() );
			auto result = callFunction( func, tokens );
			if( result.type >= 0 ) return result;
		}
		return zeroDynamic;
	}
	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 ){
		auto result = operate( o );
		if( result.type >= 0 ) return result;
	}
	return zeroDynamic;
}

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;
}

bool Talk::hasFunction( const std::string &func ){
	return functions.find( func ) != functions.end();
}

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 );
}

// Return whether the specified numeric variable exists.
bool Talk::hasVariable( const std::string &key ){
	return numbers.count( key ) > 0;
}

// Return a numeric value.
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 == "CLEARSCREEN" ){
		// Clears the current dialogue screen.
		screen.bg = "";
		screen.fg = "";
		screen.caption = "";
		screen.lines = {};
		screen.ids = {};
	}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 a number.
		std::string arg = key.length() >= 7 ? key.substr( 6 ) : "";
		// Screens returning non-numeric types default to 0.0.
		return go( arg ).valueNumber;
	}else if( key.length() >= 7 && key.substr( 0, 7 ) == "IFGOSUB" ){
		// :IFGOSUB condition sub
		// If condition value is nonzero, executes sub and returns a number.
		// Multiple spaces between params will break the parser, so avoid them.
		if( key.length() >= 9 ){
			std::string arg = key.substr( 8 );
			size_t space_at = arg.find_first_of( "\t\x20" );
			if( space_at != std::string::npos
				&& space_at + 1 < arg.length()
				&& getVariable( arg.substr( 0, space_at ) ) ){
				return go( arg.substr( space_at + 1 ) ).valueNumber;
			}
		}
	}else if( key.find( ")" ) != std::string::npos ){
		// Calls the specified function and returns its numeric result.
		auto tokens = tokenize( key );
		if( tokens.size() > 0 ){
			std::string func = tokens[0];
			tokens.erase( tokens.begin() );
			auto result = callFunction( func, tokens );
			if( result.type == 0 ){
				return result.valueNumber;
			}else if( result.type == 1 ){
				fprintf(
					stderr,
					"%s returned a string when a number was expected.\n",
					func.c_str()
				);
			}else if( result.type == 2 ){
				fprintf(
					stderr,
					"%s returned an array when a number was expected.\n",
					func.c_str()
				);
			}
		}
	}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 ){
				// Screen already exists.
				return &s;
			}
		}
		// Create a new screen.
		screens.push_back( {} );
		screens.back().id = id;
		return &screens.back();
	};

	auto GetFunctionPointer = [&]( const std::string &key ){
		auto it = functions.find( key );
		if( it != functions.end() ){
			// Function already exists.
			return &it->second;
		}
		// Create a new function.
		auto result = functions.insert( { key, {} } );
		if( result.second ){
			// Success.
			return &result.first->second;
		}else{
			// Failure.
			fprintf(
				stderr,
				"Failed to insert CSL function: %s\n",
				key.c_str()
			);
			return (CSLFunction*)nullptr;
		}
	};

	Screen *screen_ptr = GetScreenPointer( "init" );
	CSLFunction *function_ptr = nullptr;

	auto Lex = [&]( std::string line ){
		// Strip only leading whitespace.
		size_t start_at = line.find_first_not_of( "\t\x20" );
		if( start_at == std::string::npos ) return;
		line = line.substr( start_at );
		if( line.length() && line[0] == '#' ) return;
		if( line.length() >= 2 && line[0] == '[' ){
			// Screen declaration.
			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 ){
				screen_ptr = GetScreenPointer( line );
				function_ptr = nullptr;
			}
		}else if( line.length() >= 6 && line.substr( 0, 3 ) == "fun"
				&& ( line[3] == '\t' || line[3] == '\x20' ) ){
			// Function declaration.
			auto tokens = tokenize( line.substr( 4 ) );
			// The line should end with a single colon.
			// However, the tokenizer technically allows things like:
			// fun foo(bar):),(
			if( tokens.size() < 2 || tokens.back() != ":" ) return;
			function_ptr = GetFunctionPointer( tokens[0] );
			screen_ptr = nullptr;
			// Remove both the first and last token from the vector.
			tokens.pop_back();
			tokens.erase( tokens.begin() );
			// Set the function's arguments.
			function_ptr->argNames = tokens;
			// Clear the function's code in case of redefinition.
			function_ptr->exec.clear();
		}else if( line.length() >= 6 && line.substr( 0, 6 ) == "return"
				&& line.find_last_not_of( "\t\x20" ) == 5 ){
			// Syntax sweetener to make a naked "return" line return 0.0.
			Operation o = getOperation(
				"return",
				0.0,
				""
			);
			// Add the operation to the appropriate vector.
			if( screen_ptr ){
				screen_ptr->exec.push_back( o );
			}else if( function_ptr ){
				function_ptr->exec.push_back( o );
			}else{
				fprintf(
					stderr,
					"Failed to lex \"return\" because there was no containing block to return from.\n"
				);
			}
		}else{
			std::string key_str, val_str;
			size_t colon_at = line.find_first_of( ':' );
			if( colon_at == std::string::npos ){
				// No variable is provided to receive the return value,
				// so process the right side as a numeric expression and
				// put the result in the empty string ("") variable.
				key_str = "";
				val_str = line;
			}else{
				// A variable is provided for the return value.
				key_str = line.substr( 0, colon_at );
				val_str = line.substr( colon_at + 1 );
			}
			char* p;
			double num = std::strtod( val_str.c_str(), &p );
			Operation o = getOperation(
				key_str,
				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 appropriate vector.
			if( screen_ptr ){
				screen_ptr->exec.push_back( o );
			}else if( function_ptr ){
				function_ptr->exec.push_back( o );
			}else{
				fprintf(
					stderr,
					"Failed to lex CSL line because there was no containing block:\n%s\n",
					line.c_str()
				);
			}
		}
	};

	// 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;
}

CSLDynamic Talk::callFunction( const std::string &func, const std::vector<std::string> &args ){
	// callFunction returns 0.0 instead of nil.
	CSLDynamic zeroDynamic = { 0, 0.0, "", {} };
	// Find the function.
	auto it = functions.find( func );
	if( it == functions.end() ){
		fprintf( stderr, "CSL function not found: %s\n", func.c_str() );
		return zeroDynamic;
	}
	CSLFunction &f = it->second;
	// Verify that argument counts match.
	if( args.size() != f.argNames.size() ){
		fprintf(
			stderr,
			"%s expects %zu argument(s), got %zu\n",
			func.c_str(),
			f.argNames.size(),
			args.size()
		);
		return zeroDynamic;
	}
	// Fill the arguments.
	for( size_t i = 0; i < f.argNames.size(); i++ ){
		const std::string &argName = f.argNames[i];
		std::string argValue = args[i];
		if( argName.back() == '$' ){
			// String argument.
			// Use variable names (NOT inline values) for string arguments.
			if( argValue.back() == '$' ) argValue.pop_back();
			auto it = strings.find( argValue );
			if( it == strings.end() ){
				fprintf(
					stderr,
					"Variable %s$ was not found in call to CSL function %s\n",
					argValue.c_str(),
					func.c_str()
				);
				return zeroDynamic;
			}
			strings[argName.substr( 0, argName.length() - 1 )] = it->second;
		}else if( argName.back() == '@' ){
			// Array argument.
			// Use variable names (NOT inline values) for array arguments.
			if( argValue.back() == '@' ) argValue.pop_back();
			auto it = arrays.find( argValue );
			if( it == arrays.end() ){
				fprintf(
					stderr,
					"Variable %s@ was not found in call to CSL function %s\n",
					argValue.c_str(),
					func.c_str()
				);
				return zeroDynamic;
			}
			arrays[argName.substr( 0, argName.length() - 1 )] = it->second;
		}else{
			// Number argument.
			// Variable names and inline values work here.
			char* p;
			double num = std::strtod( argValue.c_str(), &p );
			if( *p ) num = getVariable( argValue );
			numbers[argName] = num;
		}
	}
	// Copy f.exec to a local stack to allow nested callback operations.
	std::vector<Operation> op_stack = f.exec;
	for( Operation &o : op_stack ){
		CSLDynamic result = operate( o );
		if( result.type >= 0 ) return result;
	}
	return zeroDynamic;
}

// Split str into a vector of its non-empty components that were
// separated by any of the characters in sep.
std::vector<std::string> Talk::tokenize( const std::string &str, const std::string &sep ){
	std::vector<std::string> result;
	std::string token;
	size_t start = 0, end = 0;
	while( ( end = str.find_first_of( sep, start ) ) != std::string::npos ){
		token = str.substr( start, end - start );
		if( token.length() > 0 ) result.push_back( token );
		start = end + 1;
	}
	token = str.substr( start );
	if( token.length() > 0 ) result.push_back( token );
	return result;
}

// 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
	};
}

CSLDynamic Talk::operate( Operation o ){
	CSLDynamic nilDynamic = { -1, 0.0, "", {} };
	// String assignment.
	if( o.op == '$' ){
		transformString( o.valueKey );
		strings[o.key] = o.valueKey;
		if( o.key == "return" ){
			return { 1, 0.0, o.valueKey, {} };
		}else 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 nilDynamic;
	}
	// Array assignment.
	if( o.op == '@' ){
		transformString( o.valueKey );
		// Make an empty array and get the iterator.
		const auto arr = arrays.insert( { o.key, {} } );
		auto &it = arr.first;
		bool success = arr.second;
		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 == "return" ){
			return { 2, 0.0, "", it->second };
		}else if( o.key == "lines" ){
			screen.lines = it->second;
		}else if( o.key == "ids" ){
			screen.ids = it->second;
		}
		return nilDynamic;
	}
	// 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 );
	if( o.key == "return" ) return { 0, out, "", {} };
	return nilDynamic;
}

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() == ')' ){
							// Call a function.
							auto tokens = tokenize( key );
							if( tokens.size() > 0 ){
								std::string func = tokens[0];
								tokens.erase( tokens.begin() );
								auto result =
									callFunction( func, tokens );
								if( result.type == 0 ){
									outStr +=
										stringifyNumber( result.valueNumber );
								}else if( result.type == 1 ){
									outStr += result.valueString;
								}else if( result.type == 2 ){
									outStr +=
										stringifyArray( result.valueArray );
								}
							}
						}else if( key.back() == '$' ){
							// String value.
							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() == '@' ){
							// Array value.
							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{
							// Number value.
							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 14015 649b7f0c112c3ac13287bfe88b949fec50356e4d Makefile
100644 blob 2723 b5a3f573f076ef740ca742ec9598043732e10c0e README.md
040000 tree - 6b3a1677d07517c1f83769dd7675fe6bb9d7a269 base
100755 blob 156 84cb1387849f2ca98e53e43536d00af2dfabf7d3 caveconfec
100755 blob 28 41b0ef285892c86306eaa269f366dd04cb633d21 caveconfec.bat
100644 blob 198037 a0180394c9bf29c02b7ef05916bd5573e3f37da2 confec.cpp
100644 blob 487269 29cfd3578eb40b1f039e271bcaa81af49d1b7f3c gamecontrollerdb.txt
040000 tree - 62e9d686bbab52d3d88886390b437a3ecef315de include
100755 blob 12081 ad29f012941aedfd4ee7232ed95fb68c8c5244c9 index-template.html
100755 blob 1065 a460e3c74b8fa53a6f609944ef7e41558479e73f 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