Files
fs2open.github.com/code/parse/parselo.cpp
T
Goober5000andClaude Opus 4.7 ba7aa70794 Fix 32-bit truncation in the flagset u64 round-trip
Mission flags are saved to the mission file as the full 64-bit flagset value, but three issues prevented anything larger than 32 bits from being saved as intended:

 * `stuff_flagset` parsed the value with `stuff_long`, where `atol` clamps anything above `LONG_MAX`
 * `flagset::from_u64` cast the incoming value to `unsigned long`, truncating bits 32-63
 * `flagset::to_u64` used `bitset::to_ulong` where it should use `bitset::to_ullong`

Fix these issues and add `static_assert` in the flagset functions to defend against future issues.  Also use the proper UINT64_T_ARG where it should be used.

This was first triggered by `Limited_support_rearm_pool`, the first mission flag to occupy bit 31.  Only mission flags are affected; the other flagset serialization paths were audited and are safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-08-30 16:46:11 -04:00

5078 lines
124 KiB
C++

/*
* Copyright (C) Volition, Inc. 1999. All rights reserved.
*
* All source code herein is the property of Volition, Inc. You may not sell
* or otherwise commercially exploit the source or things you created based on the
* source.
*
*/
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <cstdarg>
#include <csetjmp>
#include <cctype>
#include "cmdline/cmdline.h"
#include "globalincs/version.h"
#include "localization/fhash.h"
#include "localization/localize.h"
#include "mission/missionparse.h"
#include "parse/encrypt.h"
#include "parse/md5_hash.h"
#include "parse/parselo.h"
#include "parse/sexp.h"
#include "ship/ship.h"
#include "weapon/weapon.h"
#include "mod_table/mod_table.h"
#include "utils/encoding.h"
#include "utils/unicode.h"
#include "utils/string_utils.h"
#include <utf8.h>
using namespace parse;
#define ERROR_LENGTH 64
#define RS_MAX_TRIES 5
#define SHARP_S (char)-33
// to know that a modular table is currently being parsed
bool Parsing_modular_table = false;
char Current_filename[MAX_PATH_LEN];
char Current_filename_sub[MAX_PATH_LEN]; //Last attempted file to load, don't know if ex or not.
char Error_str[ERROR_LENGTH];
int Warning_count, Error_count;
int fred_parse_flag = 0;
int Token_found_flag;
char *Parse_text = nullptr;
char *Parse_text_raw = nullptr;
char *Mp = NULL, *Mp_save = NULL;
const char *token_found;
SCP_vector<Bookmark> Bookmarks; // Stack of all our previously paused parsing
// text allocation stuff
void allocate_parse_text(size_t size);
static size_t Parse_text_size = 0;
static const SCP_unordered_map<SCP_string, SCP_string> retail_hashes = {
{"strings.tbl", "84ab6e5392d7c54752a61161aac9f9fd"},
{"weapons.tbl", "ca2c7f305b1f36988c2bb8c371ab2027"}
};
// Return true if this character is white space, else false.
bool is_white_space(char ch)
{
return ((ch == ' ') || (ch == '\t') || (ch == EOLN) || (ch == CARRIAGE_RETURN));
}
// Return true if this character is white space, else false.
bool is_white_space(unicode::codepoint_t cp)
{
return ((cp == UNICODE_CHAR(' ')) || (cp == UNICODE_CHAR('\t')) || (cp == (unicode::codepoint_t)EOLN) || (cp == (unicode::codepoint_t)CARRIAGE_RETURN));
}
// Returns the length of the string up to but excluding any white space. This could be the entire string if the string contains no white space.
// Equivalently, returns the position of the first white space character, or the string length if no white space is found.
size_t find_white_space(const char *str)
{
return strcspn(str, " \t\n\r");
}
// Returns true if this character is gray space, else false (gray space is white space except for EOLN).
bool is_gray_space(char ch)
{
return ((ch == ' ') || (ch == '\t'));
}
// Returns true if this character is gray space, else false (gray space is white space except for EOLN).
bool is_gray_space(unicode::codepoint_t cp)
{
return cp == UNICODE_CHAR(' ') || cp == UNICODE_CHAR('\t');
}
// Returns the length of the string up to but excluding any white space. This could be the entire string if the string contains no white space.
// Equivalently, returns the position of the first white space character, or the string length if no white space is found.
size_t find_gray_space(const char *str)
{
return strcspn(str, " \t");
}
bool is_parenthesis(char ch)
{
return ((ch == '(') || (ch == ')'));
}
// Advance global Mp (mission pointer) past all current white space.
// Leaves Mp pointing at first non white space character.
void ignore_white_space(const char **pp)
{
if (pp == nullptr)
pp = const_cast<const char**>(&Mp);
while ((**pp != '\0') && is_white_space(**pp))
(*pp)++;
}
void ignore_gray_space(const char **pp)
{
if (pp == nullptr)
pp = const_cast<const char**>(&Mp);
while ((**pp != '\0') && is_gray_space(**pp))
(*pp)++;
}
// Truncate *str, eliminating all trailing white space.
// Eg: "abc " becomes "abc"
// "abc abc " becomes "abc abc"
// "abc \t" becomes "abc"
void drop_trailing_white_space(char *str)
{
auto len = strlen(str);
if (len == 0)
{
// Nothing to do here
return;
}
auto i = len - 1;
while (i != INVALID_SIZE && is_white_space(str[i]))
{
--i;
}
str[i + 1] = '\0';
}
// Ditto for SCP_string
void drop_trailing_white_space(SCP_string &str)
{
if (str.empty())
{
// Nothing to do here
return;
}
auto i = str.size() - 1;
while (i != INVALID_SIZE && is_white_space(str[i]))
{
--i;
}
str.resize(i + 1);
}
// Eliminate any leading whitespace in str
void drop_leading_white_space(char *str)
{
auto len = strlen(str);
size_t first = 0;
// find first non-whitespace
while ((first < len) && is_white_space(str[first]))
first++;
// quick out
if (first == 0)
return;
memmove(str, str+first, len-first);
str[len-first] = 0;
}
// Ditto for SCP_string
void drop_leading_white_space(SCP_string &str)
{
auto len = str.length();
size_t first = 0;
// find first non-whitespace
while ((first < len) && is_white_space(str[first]))
first++;
// quick out
if (first == 0)
return;
// Assign the found substring to the string
str = str.substr(first, len - first);
}
// eliminates all leading and trailing white space from a string. Returns pointer passed in.
char *drop_white_space(char *str)
{
drop_trailing_white_space(str);
drop_leading_white_space(str);
return str;
}
// ditto for SCP_string
void drop_white_space(SCP_string &str)
{
drop_trailing_white_space(str);
drop_leading_white_space(str);
}
// Advances Mp past current token.
void skip_token()
{
ignore_white_space();
while ((*Mp != '\0') && !is_white_space(*Mp))
Mp++;
}
// Display a diagnostic message if Verbose is set.
// (Verbose is set if -v command line switch is present.)
void diag_printf(const char *format, ...)
{
#ifndef NDEBUG
SCP_string buffer;
va_list args;
va_start(args, format);
vsprintf(buffer, format, args);
va_end(args);
nprintf(("Parse", "%s", buffer.c_str()));
#endif
}
// Grab and return (a pointer to) a bunch of tokens, terminating at
// ERROR_LENGTH chars, or end of line.
char *next_tokens(bool terminate_before_parenthesis_or_comma)
{
int count = 0;
char *pstr = Mp;
char ch;
while (((ch = *pstr++) != EOLN) && (ch != '\0') && (count < ERROR_LENGTH-1))
Error_str[count++] = ch;
if (terminate_before_parenthesis_or_comma && (Error_str[count-1] == ',' || Error_str[count - 1] == ')'))
--count;
Error_str[count] = 0;
return Error_str;
}
// Return the line number given by the current mission pointer, ie Mp.
// A very slow function (scans all processed text), but who cares how long
// an error reporting function takes?
int get_line_num()
{
int count = 1;
bool inquote = false;
int incomment = false;
int multiline = false;
char *p = Parse_text;
char *stoploc = Mp;
// if there is no parse text, then we have some ad-hoc text such as provided in an evaluateSEXP call or in the debug console
if (Parse_text == nullptr)
return count;
while (p < stoploc)
{
if (*p == '\0') {
Warning(LOCATION, "Unexpected end-of-file while looking for line number!");
break;
}
if ( !incomment && (*p == '\"') )
inquote = !inquote;
if ( !incomment && !inquote && (*p == COMMENT_CHAR) )
incomment = true;
if ( !incomment && (*p == '/') && (*(p+1) == '*') ) {
multiline = true;
incomment = true;
}
if ( incomment )
stoploc++;
if ( multiline && (*(p-1) == '*') && (*p == '/') ) {
multiline = false;
incomment = false;
}
if (*p++ == EOLN) { // in the process of parsing, all line endings are normalized to single-character EOLN
if ( !multiline && incomment )
incomment = false;
count++;
}
}
return count;
}
// Call this function to display an error message.
// error_level == 0 means this is just a warning.
// == 1 means this is an error message.
// == 2 means this is a release warning.
// Prints line number and other useful information.
void error_display(int error_level, const char *format, ...)
{
char type[8];
SCP_string error_text;
va_list args;
if (error_level == 0 || error_level == 2) {
strcpy_s(type, "Warning");
Warning_count++;
} else if (error_level == 1) {
strcpy_s(type, "Error");
Error_count++;
} else {
Warning(LOCATION, "Invalid error level %d passed to error_display!", error_level);
error_level = 1;
}
va_start(args, format);
vsprintf(error_text, format, args);
va_end(args);
nprintf((type, "%s(line %i): %s: %s\n", Current_filename, get_line_num(), type, error_text.c_str()));
if (error_level == 0 || Cmdline_noparseerrors)
Warning(LOCATION, "%s(line %i):\n%s: %s", Current_filename, get_line_num(), type, error_text.c_str());
else if (error_level == 2)
ReleaseWarning(LOCATION, "%s(line %i):\n%s: %s", Current_filename, get_line_num(), type, error_text.c_str());
else
Error(LOCATION, "%s(line %i):\n%s: %s", Current_filename, get_line_num(), type, error_text.c_str());
}
// Advance Mp to the next eoln character.
void advance_to_eoln(const char *more_terminators)
{
char terminators[128];
Assert((more_terminators == NULL) || (strlen(more_terminators) < 125));
terminators[0] = EOLN;
terminators[1] = 0;
if (more_terminators != NULL)
strcat_s(terminators, more_terminators);
while (strchr(terminators, *Mp) == NULL)
Mp++;
}
// Advance Mp to the next white space (ignoring white space inside of " marks)
void advance_to_next_white()
{
bool in_quotes = false;
while ((*Mp != EOLN) && (*Mp != '\0')) {
if (*Mp == '\"')
in_quotes = !in_quotes;
if (!in_quotes && is_white_space(*Mp))
break;
if (!in_quotes && is_parenthesis(*Mp))
break;
Mp++;
}
}
// If the parser is at an eoln, move past it
bool skip_eoln()
{
auto old_Mp = Mp;
if (*Mp == '\r')
Mp++;
if (*Mp == '\n')
Mp++;
return old_Mp != Mp;
}
// Search for specified string, skipping everything up to that point. Returns 1 if found,
// 0 if string wasn't found (and hit end of file), or -1 if not found, but end of checking
// block was reached.
int skip_to_string(const char *pstr, const char *end)
{
ignore_white_space();
auto len = strlen(pstr);
size_t len2 = 0;
if (end)
len2 = strlen(end);
while ((*Mp != '\0') && strnicmp(pstr, Mp, len) != 0) {
if (end && *Mp == '#')
return 0;
if (end && !strnicmp(end, Mp, len2))
return -1;
advance_to_eoln(NULL);
ignore_white_space();
}
if (!Mp || *Mp == '\0')
return 0;
Mp += strlen(pstr);
return 1;
}
// Goober5000
// Advance to start of pstr. Return 0 is successful, otherwise return !0
int skip_to_start_of_string(const char *pstr, const char *end)
{
ignore_white_space();
auto len = strlen(pstr);
size_t endlen;
if(end)
endlen = strlen(end);
else
endlen = 0;
while ( (*Mp != '\0') && strnicmp(pstr, Mp, len) != 0 ) {
if (end && *Mp == '#')
return 0;
if (end && !strnicmp(end, Mp, endlen))
return 0;
advance_to_eoln(NULL);
ignore_white_space();
}
if (!Mp || *Mp == '\0')
return 0;
return 1;
}
// Advance to start of either pstr1 or pstr2. Return 0 is successful, otherwise return !0
int skip_to_start_of_string_either(const char *pstr1, const char *pstr2, const char *end)
{
size_t len1, len2, endlen;
ignore_white_space();
len1 = strlen(pstr1);
len2 = strlen(pstr2);
if(end)
endlen = strlen(end);
else
endlen = 0;
while ( (*Mp != '\0') && strnicmp(pstr1, Mp, len1) != 0 && strnicmp(pstr2, Mp, len2) != 0 ) {
if (end && *Mp == '#')
return 0;
if (end && !strnicmp(end, Mp, endlen))
return 0;
advance_to_eoln(NULL);
ignore_white_space();
}
if (!Mp || *Mp == '\0')
return 0;
return 1;
}
int skip_to_start_of_string_one_of(const SCP_vector<SCP_string>& pstr, const char* end) {
size_t endlen;
ignore_white_space();
if (end)
endlen = strlen(end);
else
endlen = 0;
while (*Mp != '\0') {
bool foundStart = false;
for (const SCP_string& pstr_i : pstr) {
if (strnicmp(pstr_i.c_str(), Mp, pstr_i.size()) == 0) {
foundStart = true;
break;
}
}
if (foundStart)
break;
if (end && *Mp == '#')
return 0;
if (end && !strnicmp(end, Mp, endlen))
return 0;
advance_to_eoln(NULL);
ignore_white_space();
}
if (!Mp || *Mp == '\0')
return 0;
return 1;
}
// Find a required string.
// If not found, display an error message, but try up to RS_MAX_TRIES times
// to find the string. (This is the groundwork for ignoring non-understood
// lines.
// If unable to find the required string after RS_MAX_TRIES tries, then
// abort using longjmp to parse_abort.
int required_string(const char *pstr)
{
int count = 0;
ignore_white_space();
while (strnicmp(pstr, Mp, strlen(pstr)) != 0 && (count < RS_MAX_TRIES)) {
error_display(1, "Missing required token: [%s]. Found [%.32s] instead.\n", pstr, next_tokens());
advance_to_eoln(NULL);
ignore_white_space();
count++;
}
if (count == RS_MAX_TRIES) {
throw parse::ParseException("Required string not found");
}
Mp += strlen(pstr);
diag_printf("Found required string [%s]\n", token_found = pstr);
return 1;
}
bool check_for_eof_raw()
{
return (*Mp == '\0');
}
bool check_for_eof()
{
ignore_white_space();
return check_for_eof_raw();
}
/**
Returns true if it finds a newline character precded by any amount of grayspace.
*/
bool check_for_eoln()
{
ignore_gray_space();
return (*Mp == EOLN);
}
// similar to optional_string, but just checks if next token is a match.
// It doesn't advance Mp except to skip past white space.
bool check_for_string(const char *pstr)
{
ignore_white_space();
return check_for_string_raw(pstr);
}
// like check for string, but doesn't skip past any whitespace
bool check_for_string_raw(const char *pstr)
{
return (strnicmp(pstr, Mp, strlen(pstr)) == 0);
}
int string_lookup(const char* str1, const SCP_vector<SCP_string>& strlist, const char* description, bool say_errors, bool print_list)
{
return string_lookup(SCP_string(str1), strlist, description, say_errors, print_list);
}
int string_lookup(const SCP_string& str1, const SCP_vector<SCP_string>& strlist, const char* description, bool say_errors, bool print_list)
{
for (size_t i = 0; i < strlist.size(); i++)
if (lcase_equal(str1, strlist[i]))
return static_cast<int>(i);
if (say_errors)
{
const char* suffix;
SCP_string list;
if (print_list)
{
list = ":\n";
for (const auto &item: list)
{
list += " ";
list += item;
list += "\n";
}
suffix = list.c_str();
}
else
suffix = ".\n";
error_display(0, "Unable to find \"%s\" in %s list%s", str1.c_str(), description ? description : "unnamed", suffix);
}
return -1;
}
// Find an optional string.
// If found, return 1, else return 0.
// If found, point past string, else don't update pointer.
int optional_string(const char *pstr)
{
ignore_white_space();
if (!strnicmp(pstr, Mp, strlen(pstr))) {
Mp += strlen(pstr);
return 1;
}
return 0;
}
int optional_string_either(const char *str1, const char *str2, bool advance)
{
ignore_white_space();
if ( !strnicmp(str1, Mp, strlen(str1)) ) {
if(advance)
Mp += strlen(str1);
return 0;
} else if ( !strnicmp(str2, Mp, strlen(str2)) ) {
if (advance)
Mp += strlen(str2);
return 1;
}
return -1;
}
// generic parallel to required_string_one_of
int optional_string_one_of(int arg_count, ...)
{
Assertion(arg_count > 0, "optional_string_one_of() called with arg_count of %d; get a coder!\n", arg_count);
int idx, found = -1;
char *pstr;
va_list vl;
ignore_white_space();
va_start(vl, arg_count);
for (idx = 0; idx < arg_count; idx++)
{
pstr = va_arg(vl, char*);
if ( !strnicmp(pstr, Mp, strlen(pstr)) )
{
Mp += strlen(pstr);
found = idx;
break;
}
}
va_end(vl);
return found;
}
int required_string_fred(const char *pstr, const char *end)
{
char *backup = Mp;
token_found = pstr;
if (fred_parse_flag)
return 0;
ignore_white_space();
while (*Mp != '\0' && strnicmp(pstr, Mp, strlen(pstr)) != 0) {
if ((*Mp == '#') || (end && !strnicmp(end, Mp, strlen(end)))) {
Mp = NULL;
break;
}
advance_to_eoln(NULL);
ignore_white_space();
}
if (!Mp || *Mp == '\0') {
diag_printf("Required string [%s] not found\n", pstr);
Mp = backup;
Token_found_flag = 0;
return 0;
}
Mp += strlen(pstr);
diag_printf("Found required string [%s]\n", pstr);
Token_found_flag = 1;
return 1;
}
// attempt to find token in buffer. It might not exist, however, in which case we don't need
// to do anything. If it is found, then we advance the pointer to just after the token. To
// further complicate things, we should only search to a certain point, since we don't want
// a token that belongs to another section which might match the token we want. Thus, we
// also pass in an ending token, which marks the point we should stop looking at.
int optional_string_fred(const char *pstr, const char *end, const char *end2)
{
char *mp_save = Mp;
token_found = pstr;
if (fred_parse_flag)
return 0;
ignore_white_space();
while ((*Mp != '\0') && strnicmp(pstr, Mp, strlen(pstr)) != 0) {
if ((*Mp == '#') || (end && !strnicmp(end, Mp, strlen(end))) ||
(end2 && !strnicmp(end2, Mp, strlen(end2)))) {
Mp = NULL;
break;
}
advance_to_eoln(NULL);
ignore_white_space();
}
if (!Mp || *Mp == '\0') {
diag_printf("Optional string [%s] not found\n", pstr);
Mp = mp_save;
Token_found_flag = 0;
return 0;
}
Mp += strlen(pstr);
diag_printf("Found optional string [%s]\n", pstr);
Token_found_flag = 1;
return 1;
}
/**
* @brief Checks for one of two required strings
*
* @retval 0 for str1 match
* @retval 1 for str2 match
* @throws parse::ParseException If neither strings were found
*
* @details Advances the Mp until a string is found or exceeds RS_MAX_TRIES. Once a string is found, Mp is located at
* the start of the found string.
*/
int required_string_either(const char *str1, const char *str2, bool advance)
{
ignore_white_space();
for (int count = 0; count < RS_MAX_TRIES; ++count) {
if (strnicmp(str1, Mp, strlen(str1)) == 0) {
if (advance)
Mp += strlen(str1);
diag_printf("Found required string [%s]\n", token_found = str1);
return 0;
} else if (strnicmp(str2, Mp, strlen(str2)) == 0) {
if (advance)
Mp += strlen(str2);
diag_printf("Found required string [%s]\n", token_found = str2);
return 1;
}
error_display(1, "Required token = [%s] or [%s], found [%.32s].\n", str1, str2, next_tokens());
advance_to_eoln(NULL);
ignore_white_space();
}
throw parse::ParseException("Required string not found");
}
/**
* @brief Checks for one of any of the given required strings.
*
* @returns The index number of the found string, if it was found
* @returns -1 if a string was not found
*
* @details By ngld, with some tweaks by MageKing17.
*/
int required_string_one_of(int arg_count, ...)
{
Assertion(arg_count > 0, "required_string_one_of() called with arg_count of %d; get a coder!\n", arg_count);
int count = 0;
int idx;
char *expected;
SCP_string message = "";
va_list vl;
ignore_white_space();
while (count < RS_MAX_TRIES) {
va_start(vl, arg_count);
for (idx = 0; idx < arg_count; idx++) {
expected = va_arg(vl, char*);
if (strnicmp(expected, Mp, strlen(expected)) == 0) {
diag_printf("Found required string [%s]", token_found = expected);
va_end(vl);
return idx;
}
}
va_end(vl);
if (message.empty()) {
va_start(vl, arg_count);
message = "Required token = ";
for (idx = 0; idx < arg_count; idx++) {
message += "[";
message += va_arg(vl, char*);
message += "]";
if (arg_count == 2 && idx == 0) {
message += " or ";
} else if (idx == arg_count - 2) {
message += ", or ";
} else if (idx < arg_count - 2) {
message += ", ";
}
}
va_end(vl);
}
error_display(1, "%s, found [%.32s]\n", message.c_str(), next_tokens());
advance_to_eoln(NULL);
ignore_white_space();
count++;
}
return -1;
}
int required_string_one_of_fred(int arg_count, ...)
{
Assertion(arg_count > 0, "required_string_one_of_fred() called with arg_count of %d; get a coder!\n", arg_count);
va_list vl;
int idx;
char* expected;
ignore_white_space();
while (*Mp != '\0') {
va_start(vl, arg_count);
for (idx = 0; idx < arg_count; idx++) {
expected = va_arg(vl, char*);
if (strnicmp(expected, Mp, strlen(expected)) == 0) {
diag_printf("Found required string [%s]\n", token_found = expected);
va_end(vl);
return idx;
}
}
va_end(vl);
advance_to_eoln(nullptr);
ignore_white_space();
}
// EOF reached without finding any token
if (*Mp == '\0') {
SCP_string message = "Unable to find any required token: ";
va_start(vl, arg_count);
for (idx = 0; idx < arg_count; idx++) {
expected = va_arg(vl, char*);
message += "[";
message += expected;
message += "]";
if (arg_count == 2 && idx == 0) {
message += " or ";
} else if (idx == arg_count - 2) {
message += ", or ";
} else if (idx < arg_count - 2) {
message += ", ";
}
}
va_end(vl);
diag_printf("%s\n", message.c_str());
}
return -1;
}
int required_string_either_fred(const char *str1, const char *str2)
{
ignore_white_space();
while (*Mp != '\0') {
if (!strnicmp(str1, Mp, strlen(str1))) {
// Mp += strlen(str1);
diag_printf("Found required string [%s]\n", token_found = str1);
return fred_parse_flag = 0;
} else if (!strnicmp(str2, Mp, strlen(str2))) {
// Mp += strlen(str2);
diag_printf("Found required string [%s]\n", token_found = str2);
return fred_parse_flag = 1;
}
advance_to_eoln(NULL);
ignore_white_space();
}
if (*Mp == '\0')
diag_printf("Unable to find either required token [%s] or [%s]\n", str1, str2);
return -1;
}
// Copy characters from instr to outstr until eoln is found, or until max
// characters have been copied (including terminator).
void copy_to_eoln(char *outstr, const char *more_terminators, const char *instr, int max)
{
int count = 0;
char ch;
char terminators[128];
Assert((more_terminators == NULL) || (strlen(more_terminators) < 125));
terminators[0] = EOLN;
terminators[1] = 0;
if (more_terminators != NULL)
strcat_s(terminators, more_terminators);
while (((ch = *instr++) != 0) && (strchr(terminators, ch) == NULL) && (count < max)) {
*outstr++ = ch;
count++;
}
if (count >= max)
error_display(0, "Token too long: [%s]. Length = " SIZE_T_ARG ". Max is %i.\n", next_tokens(), strlen(next_tokens()), max);
*outstr = 0;
}
// Ditto for SCP_string.
void copy_to_eoln(SCP_string &outstr, const char *more_terminators, const char *instr)
{
char ch;
char terminators[128];
Assert((more_terminators == NULL) || (strlen(more_terminators) < 125));
terminators[0] = EOLN;
terminators[1] = 0;
if (more_terminators != NULL)
strcat_s(terminators, more_terminators);
outstr = "";
while (((ch = *instr++) != 0) && (strchr(terminators, ch) == NULL)) {
outstr.append(1, ch);
}
}
// Copy characters from instr to outstr until next white space is found, or until max
// characters have been copied (including terminator).
void copy_to_next_white(char *outstr, const char *instr, int max)
{
int count = 0;
bool in_quotes = false;
char ch;
while (((ch = *instr++)>0) && (ch != EOLN) && (ch != '\0') && (count < max)) {
if ( ch == '\"' ) {
in_quotes = !in_quotes;
continue;
}
if ( !in_quotes && is_white_space(ch) ) // not in quotes, white space terminates string
break;
if ( !in_quotes && is_parenthesis(ch) ) // not in quotes, parentheses are important for parsing so we don't want to copy them
break;
*outstr++ = ch;
count++;
}
if (count >= max)
error_display(0, "Token too long: [%s]. Length = " SIZE_T_ARG ". Max is %i.\n", next_tokens(), strlen(next_tokens()), max);
*outstr = 0;
}
// Ditto for SCP_string.
void copy_to_next_white(SCP_string &outstr, const char *instr)
{
bool in_quotes = false;
char ch;
outstr = "";
while (((ch = *instr++)>0) && (ch != EOLN) && (ch != '\0')) {
if ( ch == '\"' ) {
in_quotes = !in_quotes;
continue;
}
if ( !in_quotes && is_white_space(ch) ) // not in quotes, white space terminates string
break;
if ( !in_quotes && is_parenthesis(ch) ) // not in quotes, parentheses are important for parsing so we don't want to copy them
break;
outstr.append(1, ch);
}
}
//Returns a null-terminated character string allocated with vm_malloc() with the data
char* alloc_text_until(const char* instr, const char* endstr)
{
Assert(instr && endstr);
auto foundstr = stristr(instr, endstr);
if(foundstr == NULL)
{
error_display(1, "Looking for [%s], but never found it.\n", endstr);
throw parse::ParseException("End string not found");
}
else
{
if ( (foundstr - instr) <= 0 ) {
Int3(); // since this really shouldn't ever happen
return NULL;
}
char* rstr = NULL;
rstr = (char*) vm_malloc((foundstr - instr + 1)*sizeof(char));
if(rstr != NULL) {
strncpy(rstr, instr, foundstr-instr);
rstr[foundstr-instr] = '\0';
} else {
Error(LOCATION, "Could not allocate enough memory in alloc_text_until");
}
return rstr;
}
}
// Copy text until a certain string is matched.
// For example, this is used to copy mission notes, scanning until $END NOTES:
// is found.
void copy_text_until(char *outstr, const char *instr, const char *endstr, int max_chars)
{
Assert(outstr && instr && endstr);
auto foundstr = stristr(instr, endstr);
if (foundstr == NULL) {
error_display(1, "Looking for [%s], but never found it.\n", endstr);
throw parse::ParseException("End string not found");
}
if (foundstr - instr + strlen(endstr) < (uint) max_chars) {
strncpy(outstr, instr, foundstr - instr);
outstr[foundstr - instr] = 0;
} else {
error_display(1, "Too much text (" SIZE_T_ARG " chars, %i allowed) before %s\n",
foundstr - instr + strlen(endstr), max_chars, endstr);
throw parse::ParseException("Too much text found");
}
diag_printf("Here's the partial wad of text:\n%.30s\n", outstr);
}
// Ditto for SCP_string.
void copy_text_until(SCP_string &outstr, const char *instr, const char *endstr)
{
Assert(instr && endstr);
auto foundstr = stristr(instr, endstr);
if (foundstr == NULL) {
error_display(1, "Looking for [%s], but never found it.\n", endstr);
throw parse::ParseException("End string not found");
}
outstr.assign(instr, foundstr - instr);
diag_printf("Here's the partial wad of text:\n%.30s\n", outstr.c_str());
}
// stuffs a string into a buffer. Can get a string between " marks and stops
// when whitespace is encounted -- not to end of line
void stuff_string_white(char *outstr, int len)
{
if(!len)
len = NAME_LENGTH-1;
ignore_white_space();
copy_to_next_white(outstr, Mp, len);
advance_to_next_white();
}
// ditto for SCP_string
void stuff_string_white(SCP_string &outstr)
{
ignore_white_space();
copy_to_next_white(outstr, Mp);
advance_to_next_white();
}
// Goober5000
void stuff_string_until(char *outstr, const char *endstr, int len)
{
if(!len)
len = NAME_LENGTH-1;
ignore_gray_space();
copy_text_until(outstr, Mp, endstr, len);
Mp += strlen(outstr);
drop_trailing_white_space(outstr);
}
// Goober5000
void stuff_string_until(SCP_string &outstr, const char *endstr)
{
ignore_gray_space();
copy_text_until(outstr, Mp, endstr);
Mp += outstr.length();
drop_trailing_white_space(outstr);
}
//WMC
//Used for allocating large blocks, eg of Python code
//Returns a null-terminated string allocated with vm_malloc(),
//or NULL on failure
//Does depth checks for the start and end strings
//extra_chars indicates extra malloc space that should be allocated.
char* alloc_block(const char* startstr, const char* endstr, int extra_chars)
{
Assert(startstr != NULL && endstr != NULL);
Assert(stricmp(startstr, endstr));
char* rval = NULL;
auto elen = strlen(endstr);
auto slen = strlen(startstr);
size_t flen = 0;
//Skip the opening thing and any extra stuff
required_string(startstr);
ignore_white_space();
//Allocate it
char* pos = Mp;
//Depth checking
int level = 1;
while(*pos != '\0')
{
if(!strnicmp(pos, startstr, slen))
{
level++;
}
else if(!strnicmp(pos, endstr, elen))
{
level--;
}
if(level<=0)
{
break;
}
pos++;
}
//Check that we left the file
if(level > 0)
{
error_display(1, "Unclosed pair of \"%s\" and \"%s\"", startstr, endstr);
throw parse::ParseException("End string not found");
}
else
{
//Set final length for faster calcs
flen = pos-Mp;
//Allocate the memory
//WMC - Don't forget the null character that's added later on.
rval = (char*) vm_malloc((flen + extra_chars + 1)*sizeof(char));
//Copy the text (if memory was allocated)
if(rval != NULL) {
strncpy(rval, Mp, flen);
rval[flen] = '\0';
} else {
return NULL;
}
}
//Skip the copied stuff
Mp += flen;
required_string(endstr);
return rval;
}
// Karajorma - Stuffs the provided char array with either the contents of a quoted string or the name of a string
// variable. Returns PARSING_FOUND_STRING if a string was found or PARSING_FOUND_VARIABLE if a variable was present.
int get_string_or_variable (char *str)
{
int result = -1;
ignore_white_space();
// Variable
if (*Mp == SEXP_VARIABLE_CHAR)
{
auto saved_Mp = Mp;
Mp++;
stuff_string_white(str);
int sexp_variable_index = get_index_sexp_variable_name(str);
// We only want String variables
if (sexp_variable_index >= 0)
result = PARSING_FOUND_VARIABLE;
else
{
Mp = saved_Mp;
stuff_string_white(str);
error_display(1, "Expected \"%s\" to be a variable", str);
}
}
// Quoted string
else if (*Mp == '"')
{
get_string(str);
result = PARSING_FOUND_STRING;
}
else
{
get_string(str);
error_display(1, "Invalid entry \"%s\" found in get_string_or_variable. Must be a quoted string or a string variable name.", str);
}
return result;
}
// ditto for SCP_string
int get_string_or_variable (SCP_string &str)
{
int result = -1;
ignore_white_space();
// Variable
if (*Mp == SEXP_VARIABLE_CHAR)
{
auto saved_Mp = Mp;
Mp++;
stuff_string_white(str);
int sexp_variable_index = get_index_sexp_variable_name(str);
// We only want String variables
if (sexp_variable_index >= 0)
result = PARSING_FOUND_VARIABLE;
else
{
Mp = saved_Mp;
stuff_string_white(str);
error_display(1, "Expected \"%s\" to be a variable", str.c_str());
}
}
// Quoted string
else if (*Mp == '"')
{
get_string(str);
result = PARSING_FOUND_STRING;
}
else
{
get_string(str);
error_display(1, "Invalid entry \"%s\" found in get_string_or_variable. Must be a quoted string or a string variable name.", str.c_str());
}
return result;
}
/**
* Stuff a string (" chars ") into *str, return length.
* Accepts an optional max length parameter. If it is omitted or negative, then no max length is enforced.
*/
int get_string(char *str, int max)
{
auto len = strcspn(Mp + 1, "\"");
if (max >= 0 && len >= (size_t)max)
error_display(0, "String too long. Length = " SIZE_T_ARG ". Max is %i.\n", len, max);
strncpy(str, Mp + 1, len);
str[len] = 0;
Mp += len + 2;
return (int)len;
}
/**
* Stuff a string (" chars ") into str.
*/
void get_string(SCP_string &str)
{
auto len = strcspn(Mp + 1, "\"");
str.assign(Mp + 1, len);
Mp += len + 2;
}
// Stuff a string into a string buffer.
// Supports various FreeSpace primitive types. If 'len' is supplied, it will override
// the default string length if using the F_NAME case.
void stuff_string(char *outstr, int type, int len, const char *terminators)
{
char read_str[PARSE_BUF_SIZE] = "";
int read_len = PARSE_BUF_SIZE;
int final_len = len - 1;
int tag_id;
// make sure we have enough room
Assert( final_len > 0 );
// make sure it's zero'd out
memset( outstr, 0, len );
switch (type) {
case F_RAW:
case F_LNAME:
case F_NAME:
case F_DATE:
case F_FILESPEC:
case F_PATHNAME:
case F_MESSAGE:
ignore_gray_space();
copy_to_eoln(read_str, terminators, Mp, read_len);
drop_trailing_white_space(read_str);
advance_to_eoln(terminators);
break;
case F_TRIMMED:
ignore_white_space();
copy_to_eoln(read_str, terminators, Mp, read_len);
drop_trailing_white_space(read_str);
advance_to_eoln(terminators);
break;
case F_NOTES:
ignore_white_space();
copy_text_until(read_str, Mp, "$End Notes:", read_len);
Mp += strlen(read_str);
required_string("$End Notes:");
break;
// F_MULTITEXTOLD keeping for backwards compatability with old missions
// can be deleted once all missions are using new briefing format
case F_MULTITEXTOLD:
ignore_white_space();
copy_text_until(read_str, Mp, "$End Briefing Text:", read_len);
Mp += strlen(read_str);
required_string("$End Briefing Text:");
break;
case F_MULTITEXT:
ignore_white_space();
copy_text_until(read_str, Mp, "$end_multi_text", read_len);
Mp += strlen(read_str);
drop_trailing_white_space(read_str);
required_string("$end_multi_text");
break;
default:
Error(LOCATION, "Unhandled string type %d in stuff_string!", type);
}
if (type == F_FILESPEC) {
// Make sure that the passed string looks like a good filename
if (strlen(read_str) == 0) {
// Empty file name is probably not valid!
error_display(0, "A file name was expected but no name was supplied! This is probably a mistake.");
}
}
// now we want to do any final localization
if(type != F_RAW && type != F_LNAME)
{
lcl_ext_localize(read_str, outstr, final_len, &tag_id);
// if the hash localized text hash table is active and we have a valid external string - hash it
if(fhash_active() && (tag_id > -2)){
fhash_add_str(outstr, tag_id);
}
}
else
{
if ( strlen(read_str) > (uint)final_len )
error_display(0, "Token too long: [%s]. Length = " SIZE_T_ARG ". Max is %i.\n", read_str, strlen(read_str), final_len);
strncpy(outstr, read_str, final_len);
}
diag_printf("Stuffed string = [%.30s]\n", outstr);
}
// Stuff a string into a string buffer.
// Supports various FreeSpace primitive types.
void stuff_string(SCP_string &outstr, int type, const char *terminators)
{
SCP_string read_str;
int tag_id;
// make sure it's zero'd out
outstr = "";
switch (type) {
case F_RAW:
case F_LNAME:
case F_NAME:
case F_DATE:
case F_FILESPEC:
case F_PATHNAME:
case F_MESSAGE:
ignore_gray_space();
copy_to_eoln(read_str, terminators, Mp);
drop_trailing_white_space(read_str);
advance_to_eoln(terminators);
break;
case F_TRIMMED:
ignore_white_space();
copy_to_eoln(read_str, terminators, Mp);
drop_trailing_white_space(read_str);
advance_to_eoln(terminators);
break;
case F_NOTES:
ignore_white_space();
copy_text_until(read_str, Mp, "$End Notes:");
Mp += read_str.length();
required_string("$End Notes:");
break;
// F_MULTITEXTOLD keeping for backwards compatability with old missions
// can be deleted once all missions are using new briefing format
case F_MULTITEXTOLD:
ignore_white_space();
copy_text_until(read_str, Mp, "$End Briefing Text:");
Mp += read_str.length();
required_string("$End Briefing Text:");
break;
case F_MULTITEXT:
ignore_white_space();
copy_text_until(read_str, Mp, "$end_multi_text");
Mp += read_str.length();
drop_trailing_white_space(read_str);
required_string("$end_multi_text");
break;
default:
Error(LOCATION, "Unhandled string type %d in stuff_string!", type);
}
if (type == F_FILESPEC) {
// Make sure that the passed string looks like a good filename
if (read_str.empty()) {
// Empty file name is not valid!
error_display(1, "A file name was expected but no name was supplied!\n");
}
}
// now we want to do any final localization
if(type != F_RAW && type != F_LNAME)
{
lcl_ext_localize(read_str, outstr, &tag_id);
// if the hash localized text hash table is active and we have a valid external string - hash it
if(fhash_active() && (tag_id > -2)){
fhash_add_str(outstr.c_str(), tag_id);
}
}
else
{
outstr = std::move(read_str);
}
diag_printf("Stuffed string = [%.30s]\n", outstr.c_str());
}
// stuff a string, but only until the end of a line. don't ignore leading whitespace. close analog of fgets()/cfgets()
void stuff_string_line(char *outstr, int len)
{
char read_str[PARSE_BUF_SIZE] = "";
int read_len = PARSE_BUF_SIZE;
int final_len = len - 1;
int tag_id;
Assert( final_len > 0 );
// read in a line
copy_to_eoln(read_str, "\n", Mp, read_len);
drop_trailing_white_space(read_str);
advance_to_eoln("");
Mp++;
// now we want to do any final localization
lcl_ext_localize(read_str, outstr, final_len, &tag_id);
// if the hash localized text hash table is active and we have a valid external string - hash it
if(fhash_active() && (tag_id > -2)){
fhash_add_str(outstr, tag_id);
}
diag_printf("Stuffed string = [%.30s]\n", outstr);
}
// ditto for SCP_string
void stuff_string_line(SCP_string &outstr)
{
SCP_string read_str;
int tag_id;
// read in a line
copy_to_eoln(read_str, "\n", Mp);
drop_trailing_white_space(read_str);
advance_to_eoln("");
Mp++;
// now we want to do any final localization
lcl_ext_localize(read_str, outstr, &tag_id);
// if the hash localized text hash table is active and we have a valid external string - hash it
if(fhash_active() && (tag_id > -2)){
fhash_add_str(outstr.c_str(), tag_id);
}
diag_printf("Stuffed string = [%.30s]\n", outstr.c_str());
}
// Exactly the same as stuff string only Malloc's the buffer.
// Supports various FreeSpace primitive types. If 'len' is supplied, it will override
// the default string length if using the F_NAME case.
char *stuff_and_malloc_string(int type, const char *terminators)
{
SCP_string tmp_result;
stuff_string(tmp_result, type, terminators);
drop_white_space(tmp_result);
if (tmp_result.empty())
return NULL;
return vm_strdup(tmp_result.c_str());
}
void stuff_malloc_string(char **dest, int type, const char *terminators)
{
Assert(dest != NULL); //wtf?
char *new_val = stuff_and_malloc_string(type, terminators);
if(new_val != NULL)
{
if((*dest) != NULL) {
vm_free(*dest);
}
(*dest) = new_val;
}
}
// Like stuff_and_malloc_string but for std::unique_ptr<char[]>
void stuff_string(std::unique_ptr<char[]> &outstr, int type, bool null_if_empty, const char *terminators)
{
SCP_string tmp_result;
stuff_string(tmp_result, type, terminators);
outstr = util::unique_copy(tmp_result.c_str(), null_if_empty);
}
// Like stuff_and_malloc_string but for SCP_vm_unique_ptr<char>
void stuff_string(SCP_vm_unique_ptr<char> &outstr, int type, bool null_if_empty, const char *terminators)
{
SCP_string tmp_result;
stuff_string(tmp_result, type, terminators);
outstr = util::vm_unique_copy(tmp_result.c_str(), null_if_empty);
}
// After reading a multitext string, you can call this function to convert any newlines into
// spaces, so it's a one paragraph string (i.e. as in MS-Word).
void compact_multitext_string(char *str)
{
auto p_dest = str;
auto p_src = str;
while (*p_src)
{
char ch = *p_src;
// skip CR
// convert LF to space
// copy characters backwards if any CRs previously encountered
if (ch != '\r')
{
if (ch == '\n')
*p_dest = ' ';
else if (p_dest != p_src)
*p_dest = *p_src;
p_dest++;
}
p_src++;
}
if (p_dest != p_src)
*p_dest = 0;
}
// ditto for SCP_string
void compact_multitext_string(SCP_string &str)
{
auto p_dest = str.begin();
auto p_src = str.begin();
while (p_src != str.end())
{
char ch = *p_src;
// skip CR
// convert LF to space
// copy characters backwards if any CRs previously encountered
if (ch != '\r')
{
if (ch == '\n')
*p_dest = ' ';
else if (p_dest != p_src)
*p_dest = *p_src;
p_dest++;
}
p_src++;
}
if (p_dest != p_src)
str.erase(p_dest);
}
// Converts a character from Windows-1252 to CP437.
int maybe_convert_foreign_character(int ch)
{
// time to do some special foreign character conversion
switch (ch) {
case -57:
ch = 128;
break;
case -4:
ch = 129;
break;
case -23:
ch = 130;
break;
case -30:
ch = 131;
break;
case -28:
ch = 132;
break;
case -32:
ch = 133;
break;
case -27:
ch = 134;
break;
case -25:
ch = 135;
break;
case -22:
ch = 136;
break;
case -21:
ch = 137;
break;
case -24:
ch = 138;
break;
case -17:
ch = 139;
break;
case -18:
ch = 140;
break;
case -20:
ch = 141;
break;
case -60:
ch = 142;
break;
case -59:
ch = 143;
break;
case -55:
ch = 144;
break;
case -26:
ch = 145;
break;
case -58:
ch = 146;
break;
case -12:
ch = 147;
break;
case -10:
ch = 148;
break;
case -14:
ch = 149;
break;
case -5:
ch = 150;
break;
case -7:
ch = 151;
break;
case -1:
ch = 152;
break;
case -42:
ch = 153;
break;
case -36:
ch = 154;
break;
case -94:
ch = 155;
break;
case -93:
ch = 156;
break;
case -91:
ch = 157;
break;
case -125:
ch = 159;
break;
case -31:
ch = 160;
break;
case -19:
ch = 161;
break;
case -13:
ch = 162;
break;
case -6:
ch = 163;
break;
case -15:
ch = 164;
break;
case -47:
ch = 165;
break;
case -86:
ch = 166;
break;
case -70:
ch = 167;
break;
case -65:
ch = 168;
break;
case -84:
ch = 170;
break;
case -67:
ch = 171;
break;
case -68:
ch = 172;
break;
case -95:
ch = 173;
break;
case -85:
ch = 174;
break;
case -69:
ch = 175;
break;
case -33:
ch = 225;
break;
case -75:
ch = 230;
break;
case -79:
ch = 241;
break;
case -9:
ch = 246;
break;
case -80:
ch = 248;
break;
case -73:
ch = 250;
break;
case -78:
ch = 253;
break;
case -96:
ch = 255;
break;
}
return ch;
}
// Goober5000
// Yarn - The capacity of out must be at least the value returned by
// get_converted_string_length(in) (plus one if add_null is true).
// Returns the number of characters written to out.
size_t maybe_convert_foreign_characters(const char *in, char *out, bool add_null)
{
if (Fred_running) {
size_t len = strlen(in);
if (add_null) {
strcpy(out, in);
return len + 1;
} else {
strncpy(out, in, len);
return len;
}
} else {
auto inp = in;
auto outp = out;
while (*inp != '\0') {
if (*inp == SHARP_S) {
*outp++ = 's';
*outp++ = 's';
} else if (Lcl_pl) {
*outp++ = *inp;
} else {
*outp++ = (char) maybe_convert_foreign_character(*inp);
}
inp++;
}
if (add_null) {
*outp++ = '\0';
}
return outp - out;
}
}
// Goober5000
SCP_string maybe_convert_foreign_characters(const SCP_string &text)
{
if (Fred_running)
return text;
SCP_string new_text;
for (auto ch: text)
{
if (ch == SHARP_S)
new_text += "ss";
else if (!Lcl_pl)
new_text += i2ch(maybe_convert_foreign_character(ch));
else
new_text += ch;
}
return new_text;
}
// Yarn - Returns what the length of the text will be after it's processed by
// maybe_convert_foreign_characters, not including the null terminator.
size_t get_converted_string_length(const char *text)
{
if (Fred_running) {
return strlen(text);
} else {
size_t count = 0;
auto s = strchr(text, SHARP_S);
while (s != nullptr) {
count++;
s = strchr(s + 1, SHARP_S);
}
return strlen(text) + count;
}
}
// Yarn - Returns what the length of the text will be after it's processed by
// maybe_convert_foreign_characters.
size_t get_converted_string_length(const SCP_string &text)
{
if (Fred_running) {
return text.size();
} else {
size_t count = 0;
for (auto ii = text.begin(); ii != text.end(); ++ii) {
if (*ii == SHARP_S) {
count++;
}
}
return text.size() + count;
}
}
// Goober5000
bool get_number_before_separator(int &number, int &number_chars, const char *text, char separator)
{
char buf[8];
const char *ch = text;
int len = 0;
while (true)
{
// didn't find separator
if (*ch == '\0' || len == 8)
return false;
// found separator
if (*ch == separator)
break;
// found nondigit
if (!isdigit(*ch))
return false;
// copying in progress
buf[len] = *ch;
len++;
ch++;
}
// got an integer
buf[len] = '\0';
number = atoi(buf);
number_chars = len;
return true;
}
// Goober5000
bool get_number_before_separator(int &number, int &number_chars, const SCP_string &text, SCP_string::iterator text_pos, char separator)
{
char buf[8];
SCP_string::iterator ch = text_pos;
int len = 0;
while (true)
{
// didn't find separator
if (ch == text.end() || len == 8)
return false;
// found separator
if (*ch == separator)
break;
// found nondigit
if (!isdigit(*ch))
return false;
// copying in progress
buf[len] = *ch;
len++;
++ch;
}
// got an integer
buf[len] = '\0';
number = atoi(buf);
number_chars = len;
return true;
}
bool matches_version_specific_tag(const char *line_start, bool &compatible_version, int &tag_len)
{
// special version-specific comment
// formatted like e.g. ;;FSO 3.7.0;;
// Should now support anything from ;;FSO 3;; to ;;FSO 3.7.3.20151106;; -MageKing17
if (strnicmp(line_start, ";;FSO ", 6) != 0)
return false;
int major, minor, build, revis;
int s_num = scan_fso_version_string(line_start, &major, &minor, &build, &revis);
if (s_num == 0)
return false;
// hack for releases
if (s_num == 4 && FS_VERSION_REVIS < 1000) {
s_num = 3;
}
const char *ch = line_start + 6;
while ((*ch) != ';') {
Assertion((*ch) != '\0', "String that was already guaranteed to end with semicolons did not end with semicolons; it's possible we have fallen into an alternate universe. Failing string: [%s]\n", line_start);
ch++;
}
ch++;
Assertion((*ch) == ';', "String that was guaranteed to have double semicolons did not; it's possible we have fallen into an alternate universe. Failing string: [%s]\n", line_start);
ch++;
tag_len = (int)(ch - line_start);
compatible_version = true;
// check whether major, minor, and build line up with this version
if (major > FS_VERSION_MAJOR)
{
compatible_version = false;
}
else if (major == FS_VERSION_MAJOR && s_num > 1)
{
if (minor > FS_VERSION_MINOR)
{
compatible_version = false;
}
else if (minor == FS_VERSION_MINOR && s_num > 2)
{
if (build > FS_VERSION_BUILD)
{
compatible_version = false;
}
else if (build == FS_VERSION_BUILD && s_num > 3)
{
if (revis > FS_VERSION_REVIS)
{
compatible_version = false;
}
}
}
}
// true for tag match
return true;
}
// Strip comments from a line of input.
// Goober5000 - rewritten for the second time
void strip_comments(char *line, bool &in_quote, bool &in_multiline_comment_a, bool &in_multiline_comment_b)
{
char *writep = line;
char *readp = line;
// copy all characters from read to write, unless they're commented
while (*readp != '\r' && *readp != '\n' && *readp != '\0')
{
// only check for comments if not quoting
if (!in_quote)
{
bool compatible_version;
int tag_len;
// see what sort of comment characters we recognize
if (!strncmp(readp, "/*", 2))
{
// comment styles are mutually exclusive
if (!in_multiline_comment_b)
in_multiline_comment_a = true;
}
else if (!strncmp(readp, "!*", 2))
{
// comment styles are mutually exclusive
if (!in_multiline_comment_a)
in_multiline_comment_b = true;
}
else if (!strncmp(readp, "*/", 2))
{
if (in_multiline_comment_a)
{
in_multiline_comment_a = false;
readp += 2;
continue;
}
}
else if (!strncmp(readp, "*!", 2))
{
if (in_multiline_comment_b)
{
in_multiline_comment_b = false;
readp += 2;
continue;
}
}
// special version-specific comment
// formatted like e.g. ;;FSO 3.7.0;;
else if (matches_version_specific_tag(readp, compatible_version, tag_len))
{
// comment passes, so advance pass the tag and keep reading
if (compatible_version)
{
readp += tag_len;
continue;
}
// comment does not pass, so ignore the line
else
{
break;
}
}
// standard comment
else if (*readp == ';')
{
break;
}
}
// maybe toggle quoting
if (*readp == '\"')
in_quote = !in_quote;
// if not inside a comment, copy the characters
if (!in_multiline_comment_a && !in_multiline_comment_b)
{
if (writep != readp)
*writep = *readp;
writep++;
}
// read the next character
readp++;
}
// if we moved any characters, or if we haven't reached the end of the string, then mark end-of-line and terminate string
if (writep != readp || *readp != '\0')
{
writep[0] = EOLN;
writep[1] = '\0';
}
}
// Reads one line of text from the input, returning the number of input chars read. Also sets the line ending type if found;
// and if there is a mismatch, displays a warning.
int parse_get_line(char *lineout, int max_line_len, const char *textin, int input_len, int line_num, LineEndingType &file_line_ending_type, bool &warned_for_this_file)
{
auto found_line_ending = LineEndingType::UNKNOWN;
char prev_c = '\0';
int num_chars_written = 0;
for (int num_chars_read = 1; num_chars_read <= input_len; ++num_chars_read)
{
char c = *textin++;
if (c == '\0' || c == EOF) // hard stop
{
input_len = num_chars_read;
break;
}
else if (c == EOLN)
{
if (prev_c == CARRIAGE_RETURN)
found_line_ending = LineEndingType::CRLF;
else
found_line_ending = LineEndingType::LF;
}
else if (c == CARRIAGE_RETURN)
{
if (*textin != EOLN)
found_line_ending = LineEndingType::CR;
}
else
{
if (num_chars_written == max_line_len)
{
num_chars_read--; // back out the character we just read, since we can't write it
// terminate the string and return
*lineout = '\0';
return num_chars_read;
}
*lineout++ = c;
num_chars_written++;
}
if (found_line_ending != LineEndingType::UNKNOWN)
{
if (file_line_ending_type == LineEndingType::UNKNOWN)
file_line_ending_type = found_line_ending;
else if (found_line_ending != file_line_ending_type && !warned_for_this_file)
{
// we can't use error_display() here because we're in the middle of reading the file
Warning(LOCATION, "In %s, an inconsistent line ending was detected on line %d. Please check the file for line ending errors.", Current_filename_sub, line_num);
warned_for_this_file = true;
}
// ugh, if we're at the max length, we can't write the newline, so back out the newline we read
if (num_chars_written == max_line_len)
{
if (found_line_ending == LineEndingType::CRLF)
num_chars_read -= 2;
else
num_chars_read--;
}
else
{
*lineout++ = EOLN; // normalize line endings to single-character EOLN
num_chars_written++;
}
// terminate the string and return
*lineout = '\0';
return num_chars_read;
}
prev_c = c;
}
// we read the entire input without reaching a newline
*lineout = 0;
return input_len;
}
// Read mission text, stripping comments.
// When a comment is found, it is removed. If an entire line
// consisted of a comment, a blank line is left in the input file.
// Goober5000 - added ability to read somewhere other than Parse_text
void read_file_text(const char *filename, int mode, char *processed_text, char *raw_text)
{
Assertion(filename, "Filename must not be null!");
if (!filename)
throw parse::FileOpenException("Filename must not be null!");
// copy the filename
strcpy_s(Current_filename_sub, filename);
// if we are paused then processed_text and raw_text must not be NULL!!
if ( !Bookmarks.empty() && ((processed_text == NULL) || (raw_text == NULL)) ) {
Error(LOCATION, "ERROR: Neither processed_text nor raw_text may be NULL when parsing is paused!!\n");
}
// read the raw text
read_raw_file_text(filename, mode, raw_text);
if (processed_text == NULL)
processed_text = Parse_text;
if (raw_text == NULL)
raw_text = Parse_text_raw;
// process it (strip comments)
process_raw_file_text(processed_text, raw_text);
}
// Goober5000
void read_file_text_from_default(const default_file& file, char *processed_text, char *raw_text)
{
// we have no filename, so copy a substitute
strcpy_s(Current_filename_sub, "internal default file ");
strcat_s(Current_filename_sub, file.filename);
// if we are paused then processed_text and raw_text must not be NULL!!
if ( !Bookmarks.empty() && ((processed_text == NULL) || (raw_text == NULL)) ) {
Error(LOCATION, "ERROR: Neither \"processed_text\" nor \"raw_text\" may be NULL when parsing is paused!!\n");
}
// make sure to do this before anything else
allocate_parse_text(file.size + 1);
// if we have no raw buffer, set it as the default raw text area
if (raw_text == NULL)
raw_text = Parse_text_raw;
auto text = reinterpret_cast<const char*>(file.data);
// copy text in the array (but only if the raw text and the array are not the same)
if (raw_text != file.data)
{
// Copy the file contents into the array and null-terminate it
// We have to make sure to adjust the size if the size of a char is more than 1
strncpy(raw_text, text, file.size / sizeof(char));
raw_text[file.size / sizeof(char)] = '\0';
}
if (processed_text == NULL)
processed_text = Parse_text;
// process the text
process_raw_file_text(processed_text, raw_text);
}
void stop_parse()
{
Assert( Bookmarks.empty() );
if (Parse_text != nullptr) {
vm_free(Parse_text);
Parse_text = nullptr;
}
if (Parse_text_raw != nullptr) {
vm_free(Parse_text_raw);
Parse_text_raw = nullptr;
}
Parse_text_size = 0;
}
void allocate_parse_text(size_t size)
{
Assert( size > 0 );
// Make sure that there is space for the terminating null character
size += 1;
if (size <= Parse_text_size) {
// Make sure that a new parsing session does not use uninitialized data.
memset( Parse_text, 0, sizeof(char) * Parse_text_size );
memset( Parse_text_raw, 0, sizeof(char) * Parse_text_size);
return;
}
static ubyte parse_atexit = 0;
if (!parse_atexit) {
atexit(stop_parse);
parse_atexit = 1;
}
if (Parse_text != nullptr) {
vm_free(Parse_text);
Parse_text = nullptr;
}
if (Parse_text_raw != nullptr) {
vm_free(Parse_text_raw);
Parse_text_raw = nullptr;
}
Parse_text = (char *) vm_malloc(sizeof(char) * size, memory::quiet_alloc);
Parse_text_raw = (char *) vm_malloc(sizeof(char) * size, memory::quiet_alloc);
if ( (Parse_text == nullptr) || (Parse_text_raw == nullptr) ) {
Error(LOCATION, "Unable to allocate enough memory for Parse_text! Aborting...\n");
}
memset( Parse_text, 0, sizeof(char) * size );
memset( Parse_text_raw, 0, sizeof(char) * size);
Parse_text_size = size;
}
// Goober5000
void read_raw_file_text(const char *filename, int mode, char *raw_text)
{
CFILE *mf;
int file_is_encrypted;
Assertion(filename, "Filename must not be null!");
if (!filename)
throw parse::FileOpenException("Filename must not be null!");
mf = cfopen(filename, "rb", mode);
if (mf == NULL)
{
nprintf(("Error", "Wokka! Error opening file (%s)!\n", filename));
throw parse::FileOpenException("Failed to open file");
}
// read the entire file in
int file_len = cfilelength(mf);
if(!file_len) {
nprintf(("Error", "Oh noes!! File is empty! (%s)!\n", filename));
throw parse::ParseException("File is empty");
}
// For the possible Latin1 -> UTF-8 conversion we need to reallocate the raw_text at some point and we can only do
// that if we have control over the raw_text pointer which is only the case if it's null.
auto can_reallocate = raw_text == nullptr;
if (raw_text == nullptr) {
// allocate, or reallocate, memory for Parse_text and Parse_text_raw based on size we need now
allocate_parse_text((size_t) (file_len + 1));
// NOTE: this always has to be done *after* the allocate_mission_text() call!!
raw_text = Parse_text_raw;
}
// read first 10 bytes to determine if file is encrypted
cfread(raw_text, MIN(file_len, 10), 1, mf);
file_is_encrypted = is_encrypted(raw_text);
cfseek(mf, 0, CF_SEEK_SET);
file_len = util::check_encoding_and_skip_bom(mf, filename);
if ( file_is_encrypted )
{
int unscrambled_len;
char *scrambled_text;
scrambled_text = (char*)vm_malloc(file_len+1);
Assert(scrambled_text);
cfread(scrambled_text, file_len, 1, mf);
// unscramble text
unencrypt(scrambled_text, file_len, raw_text, &unscrambled_len);
file_len = unscrambled_len;
vm_free(scrambled_text);
}
else
{
cfread(raw_text, file_len, 1, mf);
}
//WMC - Slap a NULL character on here for the odd error where we forgot a #End
raw_text[file_len] = '\0';
if (Unicode_text_mode) {
// Validate the UTF-8 encoding
auto invalid = utf8::find_invalid(raw_text, raw_text + file_len);
if (invalid != raw_text + file_len) {
auto isLatin1 = util::guessLatin1Encoding(raw_text, (size_t) file_len);
// We do the additional can_reallocate check here since we need control over raw_text to reencode the file
if (isLatin1 && can_reallocate) {
// Some retail files are known to be safe to convert to utf-8 so validate that here and only warn otherwise
bool downgrade_warning = false;
// Compare filename by lowercase string
SCP_string key = filename;
SCP_tolower(key);
// Check for a hash
auto it = retail_hashes.find(key);
if (it != retail_hashes.end()) {
const auto hash = md5_hash(raw_text, strlen(raw_text));
if (it->second == hash) {
downgrade_warning = true;
} else {
mprintf(("Found Latin-1 encoded retail file %s with non-matching hash '%s'\n", filename, hash.c_str()));
}
}
// Log if a retail file was converted. Warn otherwise.
if (downgrade_warning) {
mprintf(("Found Latin-1 encoded retail file %s. The file will be automatically converted to UTF-8.\n", filename));
} else {
Warning(LOCATION, "Found Latin-1 encoded file %s. This file will be automatically converted to UTF-8 but "
"it may cause parsing issues with some files if they contained invalid data.\n"
"To silence this warning you must convert the files to UTF-8, e.g. by using a program like "
"iconv.",
filename);
}
// SDL2 has iconv functionality so we use that to convert from Latin1 to UTF-8
// We need the raw_text as fallback so we first need to copy the current
SCP_string input_str = raw_text;
SCP_string buffer;
bool success = unicode::convert_encoding(buffer, raw_text, unicode::Encoding::Encoding_iso8859_1, unicode::Encoding::Encoding_utf8);
if (Parse_text_size < buffer.length()) {
allocate_parse_text(buffer.length());
}
if (success) {
strncpy(Parse_text_raw, buffer.c_str(), buffer.length());
}
else {
Warning(LOCATION, "File reencoding failed!\n"
"You will probably encounter encoding issues.");
// Copy the original data back to the mission text pointer so that we don't loose any data here
strcpy(Parse_text_raw, input_str.c_str());
}
} else {
Warning(LOCATION, "Found invalid UTF-8 encoding in file %s at position " PTRDIFF_T_ARG "!\n"
"This may cause parsing errors and should be fixed!", filename, invalid - raw_text);
}
}
}
cfclose(mf);
}
// Goober5000, based partly on above iconv usage
void coerce_to_utf8(SCP_string &buffer, const char *str)
{
auto len = strlen(str);
// Validate the UTF-8 encoding
auto invalid = utf8::find_invalid(str, str + len);
if (invalid == str + len)
{
// turns out this is valid UTF-8
buffer.assign(str);
return;
}
bool isLatin1 = util::guessLatin1Encoding(str, len);
// we can convert it
if (isLatin1)
{
unicode::convert_encoding(buffer, str, unicode::Encoding::Encoding_iso8859_1, unicode::Encoding::Encoding_utf8);
return;
}
// unknown encoding, so just truncate
buffer.assign(str, invalid - str);
Warning(LOCATION, "Truncating non-UTF-8 string '%s' to '%s'!\n", str, buffer.c_str());
}
// Goober5000
void process_raw_file_text(char* processed_text, char* raw_text)
{
SCP_string parse_exception_1402;
unicode::convert_encoding(parse_exception_1402, "1402, \"Sie haben IPX-Protokoll als Protokoll ausgew\xE4hlt, aber dieses Protokoll ist auf Ihrer Maschine nicht installiert.\".\"\n", unicode::Encoding::Encoding_iso8859_1);
SCP_string parse_exception_1117;
unicode::convert_encoding(parse_exception_1117, "1117, \"\\r\\n\"Aucun web browser trouva. Del\xE0 isn't on emm\xE9nagea ou if \\r\\non est emm\xE9nagea, ca isn't set pour soient la default browser.\\r\\n\\r\\n\"\n", unicode::Encoding::Encoding_iso8859_1);
SCP_string parse_exception_1337;
unicode::convert_encoding(parse_exception_1337, "1337, \"(fr)Loading\"\n", unicode::Encoding::Encoding_iso8859_1);
SCP_string parse_exception_3966;
unicode::convert_encoding(parse_exception_3966, "3966, \"Es sieht so aus, als habe Staffel Kappa Zugriff auf die GTVA-Zugangscodes f\xFCr das System gehabt. Das ist ein ernstes Sicherheitsleck. Ihre IFF-Kennung erschien als \"verb\xFCndet\", so da\xDF sie sich dem Konvoi ungehindert n\xE4hern konnten. Zum Gl\xFC\x63k flogen Sie und Alpha 2 Geleitschutz und lie\xDF\x65n den Schwindel auffliegen, bevor Kappa ihren Befehl ausf\xFChren konnte.\"\n", unicode::Encoding::Encoding_iso8859_1);
char* mp;
char* mp_raw;
char outbuf[PARSE_BUF_SIZE];
bool in_quote = false;
bool in_multiline_comment_a = false;
bool in_multiline_comment_b = false;
int raw_text_len = static_cast<int>(strlen(raw_text));
if (processed_text == NULL)
processed_text = Parse_text;
if (raw_text == NULL)
raw_text = Parse_text_raw;
Assert(processed_text != NULL);
Assert(raw_text != NULL);
mp = processed_text;
mp_raw = raw_text;
// strip comments from raw text, reading into file_text
int num_chars_read = 0;
int remaining_raw_len = raw_text_len;
int parsed_line_num = 1;
auto file_line_ending_type = LineEndingType::UNKNOWN;
bool warned_for_this_file = false;
while ((num_chars_read = parse_get_line(outbuf, PARSE_BUF_SIZE-1, mp_raw, remaining_raw_len, parsed_line_num, file_line_ending_type, warned_for_this_file)) != 0) {
mp_raw += num_chars_read;
remaining_raw_len -= num_chars_read;
parsed_line_num++;
// stupid hacks to make retail data work with fixed parser, per Mantis #3072
if (!strcmp(outbuf, parse_exception_1402.c_str())) {
int offset = Unicode_text_mode ? 1 : 0;
outbuf[121 + offset] = ' ';
outbuf[122 + offset] = ' ';
}
else if (!strcmp(outbuf, parse_exception_1117.c_str())) {
char* ch = &outbuf[11];
do {
*ch = *(ch + 1);
++ch;
} while (*ch);
}
else if (!strcmp(outbuf, parse_exception_1337.c_str())) {
outbuf[3] = '6';
}
else if (!strcmp(outbuf, parse_exception_3966.c_str())) {
int offset = Unicode_text_mode ? 1 : 0;
outbuf[171 + offset] = '\'';
outbuf[181 + offset * 2] = '\'';
}
strip_comments(outbuf, in_quote, in_multiline_comment_a, in_multiline_comment_b);
if (Unicode_text_mode) {
// In unicode mode we simply assume that the text is already properly encoded in UTF-8
// Also, since we don't know how big mp actually is since we get the pointer from the outside we can't use one of
// the "safe" strcpy variants here...
strcpy(mp, outbuf);
mp += strlen(outbuf);
} else {
mp += maybe_convert_foreign_characters(outbuf, mp, false);
}
}
// Make sure the string is terminated properly
*mp = *mp_raw = '\0';
/*
while (cfgets(outbuf, PARSE_BUF_SIZE, mf) != NULL) {
if (strlen(outbuf) >= PARSE_BUF_SIZE-1)
error_display(0, "Input string too long. Max is %i characters.\n%.256s\n", PARSE_BUF_SIZE, outbuf);
// If you hit this assert, it is probably telling you the obvious. The file
// you are trying to read is truly too large. Look at *filename to see the file name.
Assert(mp_raw - file_text_raw + strlen(outbuf) < PARSE_TEXT_SIZE);
strcpy_s(mp_raw, outbuf);
mp_raw += strlen(outbuf);
in_comment = strip_comments(outbuf, in_comment);
strcpy_s(mp, outbuf);
mp += strlen(outbuf);
}
*mp = *mp_raw = EOF_CHAR;
*/
}
void debug_show_mission_text()
{
char *mp = Parse_text;
char ch;
while ((ch = *mp++) != '\0')
printf("%c", ch);
}
// Goober5000
void read_file_bytes(const char *filename, int mode, char *raw_bytes)
{
CFILE *mf;
Assertion(filename, "Filename must not be null!");
if (!filename)
throw parse::FileOpenException("Filename must not be null!");
// copy the filename
strcpy_s(Current_filename_sub, filename);
// if we are paused then raw_bytes must not be NULL!!
if ( !Bookmarks.empty() && (raw_bytes == nullptr) ) {
Error(LOCATION, "ERROR: raw_bytes may not be NULL when parsing is paused!!\n");
}
mf = cfopen(filename, "rb", mode);
if (mf == nullptr)
{
nprintf(("Error", "Wokka! Error opening file (%s)!\n", filename));
throw parse::FileOpenException("Failed to open file");
}
// read the entire file in
int file_len = cfilelength(mf);
if(!file_len) {
nprintf(("Error", "Oh noes!! File is empty! (%s)!\n", filename));
throw parse::ParseException("File is empty");
}
if (raw_bytes == nullptr) {
// allocate, or reallocate, memory for Parse_text and Parse_text_raw based on size we need now
allocate_parse_text((size_t) (file_len + 1));
// NOTE: this always has to be done *after* the allocate_mission_text() call!!
raw_bytes = Parse_text_raw;
}
cfread(raw_bytes, file_len, 1, mf);
//WMC - Slap a NULL character on here for the odd error where we forgot a #End
// Goober5000 - for binary files, the equivalent is an EOF
raw_bytes[file_len] = EOF;
cfclose(mf);
}
// Returns whether the first character encountered in str that is not whitespace is the character to look for.
// If so, and if after_ch is non-nullptr, it will be set to point to the first character after the character to look for.
bool check_first_non_whitespace_char(const char *str, char char_to_look_for, char **after_ch)
{
auto active_ch = str;
while (true)
{
if (*active_ch == '\0')
return false;
if (!is_white_space(*active_ch))
break;
active_ch++;
}
if (*active_ch == char_to_look_for)
{
if (after_ch != nullptr)
*after_ch = const_cast<char*>(active_ch + 1); // this is ugly, but strtof and strtod do the same thing
return true;
}
return false;
}
// Do the same thing for grayspace
bool check_first_non_grayspace_char(const char *str, char char_to_look_for, char **after_ch)
{
auto active_ch = str;
while (true)
{
if (*active_ch == '\0')
return false;
if (!is_gray_space(*active_ch))
break;
active_ch++;
}
if (*active_ch == char_to_look_for)
{
if (after_ch != nullptr)
*after_ch = const_cast<char*>(active_ch + 1); // this is ugly, but strtof and strtod do the same thing
return true;
}
return false;
}
bool unexpected_numeric_char(char ch)
{
return (ch != '\0') && (ch != ',') && (ch != ')') && !is_white_space(ch);
}
// Stuff a floating point value pointed at by Mp.
// Advances past float characters.
int stuff_float(float *f, bool optional)
{
char *str_start = Mp;
char *str_end;
// since strtof ignores white space anyway, might as well make it explicit
ignore_white_space();
auto result = strtof(Mp, &str_end);
bool success = false, comma = false;
int retval = 0;
// no float found?
if (result == 0.0f && str_end == Mp)
{
if (!optional)
error_display(1, "Expected float, found [%.32s].\n", next_tokens());
}
else
{
*f = result;
success = true;
}
if (success)
Mp = str_end;
// if an unexpected character is part of the number, warn about it
if (success && unexpected_numeric_char(*Mp))
{
error_display(0, "Expected float, found [%.32s].\n", next_tokens(true));
// Rather than back up to str_start, do what retail did and continue
// merrily parsing along at the next character. (Optional numbers
// will still back up to str_start - c.f. a few lines down.)
if (optional)
success = false;
}
if (check_first_non_grayspace_char(Mp, ',', &Mp))
comma = true;
if (optional && !success)
Mp = str_start;
if (success)
{
retval = 2;
diag_printf("Stuffed float: %f\n", *f);
}
else if (optional)
retval = comma ? 1 : 0;
else
skip_token();
return retval;
}
// Stuff an integer value pointed at by Mp.
// Advances past integer characters.
int stuff_int(int *i, bool optional)
{
char *str_start = Mp;
// since atoi ignores white space anyway, might as well make it explicit
ignore_white_space();
// this is a bit cumbersome
size_t span;
if (*Mp == '+' || *Mp == '-')
{
span = strspn(Mp + 1, "0123456789");
// account for the sign symbol, but not if it's the only valid character
if (span > 0)
++span;
}
else
span = strspn(Mp, "0123456789");
auto result = atoi(Mp);
bool success = false, comma = false;
int retval = 0;
// no int found?
if (result == 0 && span == 0)
{
if (!optional)
error_display(1, "Expected int, found [%.32s].\n", next_tokens());
}
else
{
*i = result;
success = true;
}
if (success)
Mp += span;
// if an unexpected character is part of the number, warn about it
if (success && unexpected_numeric_char(*Mp))
{
error_display(0, "Expected int, found [%.32s].\n", next_tokens(true));
// Rather than back up to str_start, do what retail did and continue
// merrily parsing along at the next character. (Optional numbers
// will still back up to str_start - c.f. a few lines down.)
if (optional)
success = false;
}
if (check_first_non_grayspace_char(Mp, ',', &Mp))
comma = true;
if (optional && !success)
Mp = str_start;
if (success)
{
retval = 2;
diag_printf("Stuffed int: %d\n", *i);
}
else if (optional)
retval = comma ? 1 : 0;
else
skip_token();
return retval;
}
// Stuff a long value pointed at by Mp.
// Advances past integer characters.
int stuff_long(long *l, bool optional)
{
char *str_start = Mp;
// since atol ignores white space anyway, might as well make it explicit
ignore_white_space();
// this is a bit cumbersome
size_t span;
if (*Mp == '+' || *Mp == '-')
{
span = strspn(Mp + 1, "0123456789");
// account for the sign symbol, but not if it's the only valid character
if (span > 0)
++span;
}
else
span = strspn(Mp, "0123456789");
auto result = atol(Mp);
bool success = false, comma = false;
int retval = 0;
// no long found?
if (result == 0 && span == 0)
{
if (!optional)
error_display(1, "Expected long, found [%.32s].\n", next_tokens());
}
else
{
*l = result;
success = true;
}
if (success)
Mp += span;
// if an unexpected character is part of the number, warn about it
if (success && unexpected_numeric_char(*Mp))
{
error_display(0, "Expected long, found [%.32s].\n", next_tokens(true));
// Rather than back up to str_start, do what retail did and continue
// merrily parsing along at the next character. (Optional numbers
// will still back up to str_start - c.f. a few lines down.)
if (optional)
success = false;
}
if (check_first_non_grayspace_char(Mp, ',', &Mp))
comma = true;
if (optional && !success)
Mp = str_start;
if (success)
{
retval = 2;
diag_printf("Stuffed long: %ld\n", *l);
}
else if (optional)
retval = comma ? 1 : 0;
else
skip_token();
return retval;
}
// Stuff an unsigned 64-bit value pointed at by Mp.
// Advances past integer characters.
int stuff_uint64(std::uint64_t *l, bool optional)
{
char *str_start = Mp;
// since strtoull ignores white space anyway, might as well make it explicit
ignore_white_space();
// this is a bit cumbersome
size_t span;
if (*Mp == '+')
{
span = strspn(Mp + 1, "0123456789");
// account for the sign symbol, but not if it's the only valid character
if (span > 0)
++span;
}
else
span = strspn(Mp, "0123456789");
// don't call strtoull unless we found digits, because it will happily parse
// (and wrap around) a negative number
std::uint64_t result = (span > 0) ? strtoull(Mp, nullptr, 10) : 0;
bool success = false, comma = false;
int retval = 0;
// no number found?
if (span == 0)
{
if (!optional)
error_display(1, "Expected unsigned integer, found [%.32s].\n", next_tokens());
}
else
{
*l = result;
success = true;
}
if (success)
Mp += span;
// if an unexpected character is part of the number, warn about it
if (success && unexpected_numeric_char(*Mp))
{
error_display(0, "Expected unsigned integer, found [%.32s].\n", next_tokens(true));
// Rather than back up to str_start, do what retail did and continue
// merrily parsing along at the next character. (Optional numbers
// will still back up to str_start - c.f. a few lines down.)
if (optional)
success = false;
}
if (check_first_non_grayspace_char(Mp, ',', &Mp))
comma = true;
if (optional && !success)
Mp = str_start;
if (success)
{
retval = 2;
diag_printf("Stuffed uint64: " UINT64_T_ARG "\n", *l);
}
else if (optional)
retval = comma ? 1 : 0;
else
skip_token();
return retval;
}
int stuff_float_optional(float *f)
{
return stuff_float(f, true);
}
int stuff_int_optional(int *i)
{
return stuff_int(i, true);
}
// Stuff an integer value pointed at by Mp. If a variable is found instead, stuff the value of that variable and record the
// index of the variable in the following slot.
void stuff_int_or_variable(int *i, int *var_index, bool need_positive_value)
{
if (*Mp == SEXP_VARIABLE_CHAR)
{
int value = -1;
SCP_string str;
auto saved_Mp = Mp;
Mp++;
stuff_string(str, F_NAME);
int index = get_index_sexp_variable_name(str);
if (index > -1 && index < MAX_SEXP_VARIABLES)
{
if (Sexp_variables[index].type & SEXP_VARIABLE_NUMBER)
{
value = atoi(Sexp_variables[index].text);
}
else
{
error_display(1, "Invalid variable type \"%s\" found in mission. Variable must be a number variable!", str.c_str());
}
}
else
{
Mp = saved_Mp;
stuff_string(str, F_NAME);
error_display(1, "Invalid variable name \"%s\" found.", str.c_str());
}
// zero negative values if requested
if (need_positive_value && value < 0)
{
value = 0;
}
// Record the value of the index for FreeSpace
*i = value;
// Record the index itself because we may need it later.
*var_index = index;
}
else
{
stuff_int(i);
// Since we have a numerical value we don't have a SEXP variable index to add for next slot.
*var_index = NOT_SET_BY_SEXP_VARIABLE;
}
}
//Stuffs boolean value.
//Passes things off to stuff_boolean(bool)
void stuff_boolean(int *i, bool a_to_eol)
{
bool tempb;
stuff_boolean(&tempb, a_to_eol);
if(tempb)
*i = 1;
else
*i = 0;
}
void stuff_boolean_flag(int *i, int flag, bool a_to_eol)
{
bool temp;
stuff_boolean(&temp, a_to_eol);
if(temp)
*i |= flag;
else
*i &= ~(flag);
}
// Stuffs a boolean value pointed at by Mp.
// YES/NO (supporting 1/0 now as well)
// Now supports localization :) -WMC
void stuff_boolean(bool *b, bool a_to_eol)
{
char token[NAME_LENGTH];
stuff_string_white(token);
if(a_to_eol)
advance_to_eoln(NULL);
if (!parse_boolean(token, b))
{
*b = false;
error_display(0, "Boolean '%s' type unknown; assuming 'no/false'", token);
}
diag_printf("Stuffed bool: %s\n", (b) ? NOX("true") : NOX("false"));
}
// Parses a token into a boolean value, if the token is recognized. If so, the boolean parameter is assigned the value and the function returns true;
// if not, the boolean parameter is not assigned and the function returns false.
bool parse_boolean(const char *token, bool *b)
{
Assertion(token != nullptr && b != nullptr, "Parameters must not be NULL!");
if(isdigit(token[0]))
{
if(token[0] != '0')
*b = true;
else
*b = false;
return true;
}
else
{
if(!stricmp(token, "yes")
|| !stricmp(token, "true")
|| !stricmp(token, "ja") //German
|| !stricmp(token, "Oui") //French
|| !stricmp(token, "si") //Spanish
|| !stricmp(token, "ita vero") //Latin
|| !stricmp(token, "HIja'") || !stricmp(token, "HISlaH")) //Klingon
{
*b = true;
return true;
}
else if(!stricmp(token, "no")
|| !stricmp(token, "false")
|| !stricmp(token, "nein") //German
|| !stricmp(token, "Non") //French
//I don't know spanish for "no"
//But according to altavista, spanish for "No" is "no"
//Go figure.
|| !stricmp(token, "minime") //Latin
|| !stricmp(token, "ghobe'")) //Klingon
{
*b = false;
return true;
}
}
// token not recognized
return false;
}
// Stuff an integer value (cast to a ubyte) pointed at by Mp.
// Advances past integer characters.
void stuff_ubyte(ubyte *i)
{
int temp;
stuff_int(&temp);
*i = (ubyte)temp;
}
template <typename T, typename F>
void stuff_token_list(SCP_vector<T> &list, F stuff_one_token, const char *type_as_string, bool skip_comma = true)
{
list.clear();
ignore_white_space();
if (*Mp != '(')
{
error_display(1, "Reading %s list. Found [%c]. Expected '('.\n", type_as_string, *Mp);
throw parse::ParseException("Syntax error");
}
Mp++;
while (!check_first_non_whitespace_char(Mp, ')', &Mp))
{
ignore_white_space();
T item;
if (stuff_one_token(&item))
list.push_back(std::move(item));
if (skip_comma)
check_first_non_grayspace_char(Mp, ',', &Mp);
}
}
template <typename T, typename F>
size_t stuff_token_list(T *listp, size_t list_max, F stuff_one_token, const char *type_as_string, bool skip_comma = true)
{
SCP_vector<T> list;
stuff_token_list(list, stuff_one_token, type_as_string, skip_comma);
if (list_max < list.size())
{
error_display(0, "Too many items in %s list. Found " SIZE_T_ARG "; max is " SIZE_T_ARG ". List has been truncated.", type_as_string, list.size(), list_max);
list.resize(list_max);
}
size_t i = 0;
for (const auto &item : list)
listp[i++] = item;
Assert(i == list.size());
return i;
}
// If this data is going to be parsed multiple times (like for mission load), then the dest variable
// needs to be set to zero in between parses, otherwise we keep bad data.
// For tbm files, it must not be reset.
void parse_string_flag_list(int &dest, flag_def_list defs[], size_t defs_size)
{
SCP_vector<SCP_string> slp;
stuff_string_list(slp);
for (auto &str : slp)
{
for (size_t j = 0; j < defs_size; j++)
{
if (!stricmp(str.c_str(), defs[j].name))
dest |= defs[j].def;
}
}
}
size_t stuff_bool_list(bool *blp, size_t max_bools)
{
return stuff_token_list(blp, max_bools, [](bool *b)->bool {
stuff_boolean(b, false);
return true;
}, "bool");
}
void stuff_string_list(SCP_vector<SCP_string> &slp)
{
stuff_token_list(slp, [](SCP_string *buf)->bool {
if (*Mp != '\"') {
error_display(0, "Missing quotation marks in string list.");
// Since this is a bad token, skip characters until we find a comma, parenthesis, or EOLN
advance_to_eoln(",)");
return false;
}
*buf = "";
get_string(*buf);
return true;
}, "string");
}
size_t stuff_string_list(char slp[][NAME_LENGTH], size_t max_strings)
{
SCP_vector<SCP_string> list;
stuff_string_list(list);
if (max_strings < list.size())
{
error_display(0, "Too many items in %s list. Found " SIZE_T_ARG "; max is " SIZE_T_ARG ". List has been truncated.", "string", list.size(), max_strings);
list.resize(max_strings);
}
for (size_t i = 0; i < list.size(); ++i)
{
if (list[i].size() >= NAME_LENGTH)
{
Warning(LOCATION, "'%s' is too long and will be truncated. Max length is %d.", list[i].c_str(), NAME_LENGTH - 1);
list[i].resize(NAME_LENGTH - 1);
}
strcpy_s(slp[i], list[i].c_str());
}
return list.size();
}
const char* get_lookup_type_name(ParseLookupType lookup_type)
{
switch (lookup_type)
{
case ParseLookupType::RAW_INTEGER_TYPE:
return "Untyped integer list";
case ParseLookupType::SHIP_TYPE:
return "Ships";
case ParseLookupType::SHIP_INFO_TYPE:
return "Ship Classes";
case ParseLookupType::WEAPON_LIST_TYPE:
return "Weapon Types";
case ParseLookupType::WEAPON_POOL_TYPE:
return "Weapon Pool";
case ParseLookupType::FIREBALL_INFO_TYPE:
return "Fireball Types";
case ParseLookupType::MISSION_LOADOUT_SHIP_LIST:
return "Mission Loadout Ships";
case ParseLookupType::MISSION_LOADOUT_WEAPON_LIST:
return "Mission Loadout Weapons";
case ParseLookupType::CAMPAIGN_LOADOUT_SHIP_LIST:
return "Campaign Loadout Ships";
case ParseLookupType::CAMPAIGN_LOADOUT_WEAPON_LIST:
return "Campaign Loadout Weapons";
default:
return "Unknown lookup type, tell a coder!";
}
}
// use a functor here so that we don't need to re-roll the parsing function for both variants of stuff_int_lists
struct StuffIntListParser
{
ParseLookupType lookup_type;
bool warn_on_lookup_failure;
StuffIntListParser(ParseLookupType _lookup_type, bool _warn_on_lookup_failure)
: lookup_type(_lookup_type), warn_on_lookup_failure(_warn_on_lookup_failure)
{}
bool operator()(int* buf) {
if (*Mp == '"') {
int num = 0;
bool valid_negative = false;
SCP_string str;
get_string(str);
switch (lookup_type) {
case ParseLookupType::SHIP_TYPE:
num = ship_name_lookup(str.c_str()); // returns index of Ship[] entry with name
if (num < 0 && warn_on_lookup_failure)
error_display(0, "Unable to find ship %s in stuff_int_list!", str.c_str());
break;
case ParseLookupType::SHIP_INFO_TYPE:
num = ship_info_lookup(str.c_str()); // returns index of Ship_info[] entry with name
if (num < 0 && warn_on_lookup_failure)
error_display(0, "Unable to find ship class %s in stuff_int_list!", str.c_str());
break;
case ParseLookupType::WEAPON_POOL_TYPE:
num = weapon_info_lookup(str.c_str());
if (num < 0 && warn_on_lookup_failure)
error_display(0, "Unable to find weapon class %s in stuff_int_list!", str.c_str());
break;
case ParseLookupType::WEAPON_LIST_TYPE:
num = weapon_info_lookup(str.c_str());
if (str.empty())
valid_negative = true;
else if (num < 0 && warn_on_lookup_failure)
error_display(0, "Unable to find weapon class %s in stuff_int_list!", str.c_str());
break;
case ParseLookupType::FIREBALL_INFO_TYPE:
num = fireball_info_lookup(str.c_str());
if (num < 0 && warn_on_lookup_failure)
error_display(0, "Unable to find fireball type %s in stuff_int_list!", str.c_str());
break;
case ParseLookupType::RAW_INTEGER_TYPE:
num = atoi(str.c_str());
valid_negative = true;
break;
default:
UNREACHABLE("Unsupported lookup_type %d in stuff_int_list", static_cast<int>(lookup_type));
break;
}
if (num < 0 && !valid_negative)
return false;
*buf = num;
} else {
stuff_int(buf);
}
return true;
}
};
// Stuffs an integer list.
// This is of the form ( i* )
// where i is an integer.
// For example, (1) () (1 2 3) ( 1 ) are legal integer lists.
size_t stuff_int_list(int *ilp, size_t max_ints, ParseLookupType lookup_type, bool warn_on_lookup_failure)
{
return stuff_token_list(ilp, max_ints, StuffIntListParser(lookup_type, warn_on_lookup_failure), get_lookup_type_name(lookup_type));
}
// Stuffs an integer list.
// This is of the form ( i* )
// where i is an integer.
// For example, (1) () (1 2 3) ( 1 ) are legal integer lists.
void stuff_int_list(SCP_vector<int> &ilp, ParseLookupType lookup_type, bool warn_on_lookup_failure)
{
stuff_token_list(ilp, StuffIntListParser(lookup_type, warn_on_lookup_failure), get_lookup_type_name(lookup_type));
}
// Karajorma/Goober5000 - Stuffs a loadout list by parsing a list of ship or weapon choices.
// Unlike stuff_int_list it can deal with variables
void stuff_loadout_list(SCP_vector<loadout_row> &list, ParseLookupType lookup_type)
{
stuff_token_list(list, [&](loadout_row *buf)->bool {
SCP_string str;
int variable_found = get_string_or_variable(str);
// if we've got a variable get the variable index and copy its value into str so that regardless of whether we found
// a variable or not it now holds the name of the ship or weapon we're interested in.
if (variable_found) {
Assert(lookup_type != ParseLookupType::CAMPAIGN_LOADOUT_SHIP_LIST);
buf->index_sexp_var = get_index_sexp_variable_name(str);
if (buf->index_sexp_var < 0) {
error_display(1, "Invalid SEXP variable name \"%s\" found in stuff_loadout_list.", str.c_str());
}
str = Sexp_variables[buf->index_sexp_var].text;
}
switch (lookup_type) {
case ParseLookupType::MISSION_LOADOUT_SHIP_LIST:
case ParseLookupType::CAMPAIGN_LOADOUT_SHIP_LIST:
buf->index = ship_info_lookup(str.c_str());
break;
case ParseLookupType::MISSION_LOADOUT_WEAPON_LIST:
case ParseLookupType::CAMPAIGN_LOADOUT_WEAPON_LIST:
buf->index = weapon_info_lookup(str.c_str());
break;
default:
UNREACHABLE("Unsupported lookup_type %d in stuff_loadout_list", static_cast<int>(lookup_type));
return false;
}
bool skip_this_entry = false;
// Complain if this isn't a valid ship or weapon and we are loading a mission. Campaign files can be loaded containing
// no ships from the current tables (when swapping mods) so don't report that as an error.
if (buf->index < 0 && (lookup_type == ParseLookupType::MISSION_LOADOUT_SHIP_LIST || lookup_type == ParseLookupType::MISSION_LOADOUT_WEAPON_LIST)) {
error_display(0, "Invalid type \"%s\" found in loadout of mission file...skipping", str.c_str());
skip_this_entry = true;
// increment counter for release FRED builds.
Num_unknown_loadout_classes++;
}
else if ((Game_mode & GM_MULTIPLAYER) && (lookup_type == ParseLookupType::MISSION_LOADOUT_WEAPON_LIST) && (Weapon_info[buf->index].maximum_children_spawned > 300)){
Warning(LOCATION, "Weapon '%s' has more than 300 possible spawned weapons over its lifetime! This can cause issues for Multiplayer.", Weapon_info[buf->index].name);
}
if (!skip_this_entry) {
// similarly, complain if this is a valid ship or weapon class that the player can't use
if ((lookup_type == ParseLookupType::MISSION_LOADOUT_SHIP_LIST) && (!(Ship_info[buf->index].flags[Ship::Info_Flags::Player_ship])) ) {
error_display(0, "Ship type \"%s\" found in loadout of mission file. This class is not marked as a player ship...skipping", str.c_str());
skip_this_entry = true;
}
else if ((lookup_type == ParseLookupType::MISSION_LOADOUT_WEAPON_LIST) && (!(Weapon_info[buf->index].wi_flags[Weapon::Info_Flags::Player_allowed])) ) {
nprintf(("Warning", "Warning: Weapon type %s found in loadout of mission file. This class is not marked as a player allowed weapon...skipping\n", str.c_str()));
if ( !Is_standalone )
error_display(0, "Weapon type \"%s\" found in loadout of mission file. This class is not marked as a player allowed weapon...skipping", str.c_str());
skip_this_entry = true;
}
}
// Loadout counts are only needed for missions
if (lookup_type == ParseLookupType::MISSION_LOADOUT_SHIP_LIST || lookup_type == ParseLookupType::MISSION_LOADOUT_WEAPON_LIST)
{
ignore_white_space();
// Now read in the number of this type available. The number must be positive
stuff_int_or_variable(&buf->count, &buf->count_sexp_var, true);
}
return !skip_this_entry;
}, get_lookup_type_name(lookup_type));
}
//Stuffs an float list like stuff_int_list.
size_t stuff_float_list(float* flp, size_t max_floats)
{
return stuff_token_list(flp, max_floats, [](float *f)->bool {
stuff_float(f);
return true;
}, "float", false); // don't skip the comma in stuff_token_list because stuff_float also skips one
}
// ditto the above, but a vector of floats...
void stuff_float_list(SCP_vector<float>& flp)
{
stuff_token_list(flp, [](float* buf)->bool {
stuff_float(buf);
return true;
}, "float", false); // don't skip the comma in stuff_token_list because stuff_float also skips one
}
// Stuff a vec2d struct, which is 2 floats.
void stuff_vec2d(vec2d* vp)
{
stuff_float(&vp->x);
stuff_float(&vp->y);
}
// Stuff a vec3d struct, which is 3 floats.
void stuff_vec3d(vec3d *vp)
{
stuff_float(&vp->xyz.x);
stuff_float(&vp->xyz.y);
stuff_float(&vp->xyz.z);
}
void stuff_angles_deg_phb(angles* ap) {
stuff_float(&ap->p);
stuff_float(&ap->h);
stuff_float(&ap->b);
ap->p = fl_radians(ap->p);
ap->h = fl_radians(ap->h);
ap->b = fl_radians(ap->b);
}
void stuff_parenthesized_vec2d(vec2d* vp)
{
ignore_white_space();
if (*Mp != '(') {
error_display(1, "Reading parenthesized vec2d. Found [%c]. Expected '('.\n", *Mp);
throw parse::ParseException("Syntax error");
}
else {
Mp++;
stuff_vec2d(vp);
ignore_white_space();
if (*Mp != ')') {
error_display(1, "Reading parenthesized vec2d. Found [%c]. Expected ')'.\n", *Mp);
throw parse::ParseException("Syntax error");
}
Mp++;
}
}
void stuff_parenthesized_vec3d(vec3d *vp)
{
ignore_white_space();
if (*Mp != '(') {
error_display(1, "Reading parenthesized vec3d. Found [%c]. Expected '('.\n", *Mp);
throw parse::ParseException("Syntax error");
} else {
Mp++;
stuff_vec3d(vp);
ignore_white_space();
if (*Mp != ')') {
error_display(1, "Reading parenthesized vec3d. Found [%c]. Expected ')'.\n", *Mp);
throw parse::ParseException("Syntax error");
}
Mp++;
}
}
// Stuffs vec3d list. *vlp is an array of vec3ds.
// This is of the form ( (vec3d)* )
// (where * is a kleene star, not a pointer indirection)
// For example, ( (1 2 3) (2 3 4) (2 3 5) )
// is a list of three vec3ds.
size_t stuff_vec3d_list(vec3d *vlp, size_t max_vecs)
{
return stuff_token_list(vlp, max_vecs, [](vec3d *buf)->bool {
stuff_parenthesized_vec3d(buf);
return true;
}, "vec3d");
}
// ditto the above, but a vector of vec3ds...
void stuff_vec3d_list(SCP_vector<vec3d> &vec_list)
{
stuff_token_list(vec_list, [](vec3d *buf)->bool {
stuff_parenthesized_vec3d(buf);
return true;
}, "vec3d");
}
// Stuff a matrix, which is 3 vec3ds.
void stuff_matrix(matrix *mp)
{
stuff_vec3d(&mp->vec.rvec);
stuff_vec3d(&mp->vec.uvec);
stuff_vec3d(&mp->vec.fvec);
// Make sure this matrix is well-behaved.
vm_fix_matrix(mp);
}
// Find a required string (*id), then stuff the text of type f_type that
// follows it at *addr. *strlist[] contains the strings it should try to
// match.
void find_and_stuff(const char *id, int *addr, int f_type, const char *strlist[], size_t max, const char *description)
{
char token[128];
bool checking_ship_classes = (stricmp(id, "$class:") == 0);
// Goober5000 - don't say errors when we're checking classes because 1) we have more checking to do; and 2) we will say a redundant error later
required_string(id);
stuff_string(token, f_type, sizeof(token));
*addr = string_lookup(token, strlist, max, description, !checking_ship_classes);
// Goober5000 - handle certain FSPort idiosyncracies with ship classes
if (*addr < 0 && checking_ship_classes)
{
int idx = ship_info_lookup(token);
if (idx >= 0)
*addr = string_lookup(Ship_info[idx].name, strlist, max, description, false);
else
*addr = -1;
}
}
void find_and_stuff_optional(const char *id, int *addr, int f_type, const char * const *strlist, size_t max, const char *description)
{
char token[128];
if(optional_string(id))
{
stuff_string(token, f_type, sizeof(token));
*addr = string_lookup(token, strlist, max, description, true);
}
}
// Mp points at a string.
// Find the string in the list of strings *strlist[].
// Returns the index of the match, -1 if none.
int match_and_stuff(int f_type, const char * const *strlist, int max, const char *description)
{
char token[128];
stuff_string(token, f_type, sizeof(token));
return string_lookup(token, strlist, max, description, false);
}
void find_and_stuff_or_add(const char *id, int *addr, int f_type, char *strlist[], int *total,
int max, const char *description)
{
char token[128];
*addr = -1;
required_string(id);
stuff_string(token, f_type, sizeof(token));
if (*total)
*addr = string_lookup(token, strlist, *total, description, false);
if (*addr == -1) // not in list, so lets try and add it.
{
Assert(*total < max);
strcpy(strlist[*total], token);
*addr = (*total)++;
}
}
// pause current parsing so that some else can be parsed without interfering
// with the currently parsing file
void pause_parse()
{
Bookmark Mark;
Mark.filename = Current_filename;
Mark.Mp = Mp;
Mark.Warning_count = Warning_count;
Mark.Error_count = Error_count;
Bookmarks.push_back(std::move(Mark));
}
// unpause parsing to continue with previously parsing file
void unpause_parse()
{
Assert( !Bookmarks.empty() );
if (Bookmarks.empty())
return;
Bookmark Mark = Bookmarks.back();
Mp = Mark.Mp;
Warning_count = Mark.Warning_count;
Error_count = Mark.Error_count;
strcpy_s(Current_filename, Mark.filename.c_str());
Bookmarks.pop_back();
}
void reset_parse(char *text)
{
if (text != NULL) {
Mp = text;
} else {
Mp = Parse_text;
}
Warning_count = 0;
Error_count = 0;
strcpy_s(Current_filename, Current_filename_sub);
}
// Display number of warnings and errors at the end of a parse.
void display_parse_diagnostics()
{
nprintf(("Parse", "\nParse complete.\n"));
nprintf(("Parse", "%i errors. %i warnings.\n", Error_count, Warning_count));
}
// Splits a string into 2 lines if the string is wider than max_pixel_w pixels. A null
// terminator is placed where required to make the first line <= max_pixel_w. The remaining
// text is returned (leading whitespace removed). If the line doesn't need to be split,
// NULL is returned.
char *split_str_once(char *src, int max_pixel_w, float scale)
{
char *brk = nullptr;
char *word_brk = nullptr; // if we fail, break here (even if its in the middle of a word)
bool last_was_white = false;
Assert(src);
if (max_pixel_w <= 0)
return src; // if there's no width, skip everything else
int w;
gr_get_string_size(&w, nullptr, src, scale);
if ( (w <= max_pixel_w) && !strstr(src, "\n") ) {
return nullptr; // string doesn't require a cut
}
size_t i;
size_t len = strlen(src);
for (i=0; i<len; i++) {
gr_get_string_size(&w, nullptr, src, scale, i + 1);
if (w <= max_pixel_w) {
if (src[i] == '\n') { // reached natural end of line
src[i] = 0;
return src + i + 1;
}
}
else if (word_brk == nullptr) {
word_brk = src + i;
}
if (is_white_space(src[i])) {
if (!last_was_white) {
// only update the line break if:
// a) we don't have a line break yet;
// b) we're still within the required real estate
// (basically we want the latest line break that doesn't go off the edge of the screen,
// but if the *first* line break is off the end of the screen, we want that)
if (brk == nullptr || w <= max_pixel_w) {
brk = src + i;
}
}
last_was_white = true;
} else {
last_was_white = false;
}
}
// if we are over max pixel width and weren't able to come up with a good non-word
// split then just split the word
if ( (w > max_pixel_w) && ((i == 0) || !brk) ) {
return word_brk;
}
if (!brk) {
brk = src + i;
}
*brk = 0;
src = brk + 1;
while (is_white_space(*src))
src++;
if (!*src)
return nullptr; // end of the string anyway
if (*src == '\n')
src++;
return src;
}
#define SPLIT_STR_BUFFER_SIZE 512
// --------------------------------------------------------------------------------------
// split_str()
//
// A general function that will split a string into several lines. Lines are allowed up
// to max_pixel_w pixels. Breaks are found in white space.
//
// Supports \n's in the strings!
//
// parameters: src => source string to be broken up
// max_pixel_w => max width of line in pixels
// n_chars => output array that will hold number of characters in each line
// p_str => output array of pointers to start of lines within src
// max_lines => limit of number of lines to break src up into
// ignore_char => OPTIONAL parameter (default val -1). Ignore words starting with this character
// This is useful when you want to ignore embedded control information that starts
// with a specific character, like $ or #
//
// returns: number of lines src is broken into
// -1 is returned when an error occurs
//
int split_str(const char *src, int max_pixel_w, int *n_chars, const char **p_str, int max_lines, int max_line_length, unicode::codepoint_t ignore_char, bool strip_leading_whitespace)
{
char buffer[SPLIT_STR_BUFFER_SIZE];
const char *breakpoint = NULL;
int sw, new_line = 1, line_num = 0, last_was_white = 0;
int ignore_until_whitespace, buf_index;
// check our assumptions..
Assert(src != NULL);
Assert(n_chars != NULL);
Assert(p_str != NULL);
Assert(max_lines > 0);
Assert(max_pixel_w > 0);
Assertion(max_line_length > 0, "Max line length should be >0, not %d; get a coder!\n", max_line_length);
memset(buffer, 0, sizeof(buffer));
buf_index = 0;
ignore_until_whitespace = 0;
// get rid of any leading whitespace
while (strip_leading_whitespace && is_white_space(*src))
src++;
new_line = 1;
p_str[0] = NULL;
// iterate through chars in line, keeping track of most recent "white space" location that can be used
// as a line splitting point if necessary
unicode::codepoint_range range(src);
auto end_iter = std::end(range);
auto iter = std::begin(range);
for (; iter != end_iter; ++iter) {
auto cp = *iter;
if (line_num >= max_lines)
return line_num; // time to bail out
// starting a new line of text, init stuff for that
if (new_line) {
p_str[line_num] = NULL;
if (strip_leading_whitespace && is_gray_space(cp))
continue;
p_str[line_num] = iter.pos();
breakpoint = NULL;
new_line = 0;
}
// maybe skip leading whitespace
if (ignore_until_whitespace) {
if ( is_white_space(cp) )
ignore_until_whitespace = 0;
continue;
}
// if we have a newline, split the line here
if (cp == UNICODE_CHAR('\n')) {
n_chars[line_num] = (int)(iter.pos() - p_str[line_num]); // track length of line
line_num++;
if (line_num < max_lines) {
p_str[line_num] = NULL;
}
new_line = 1;
memset(buffer, 0, SPLIT_STR_BUFFER_SIZE);
buf_index = 0;
continue;
}
if (cp == ignore_char) {
ignore_until_whitespace = 1;
continue;
}
if (is_gray_space(cp)) {
if (!last_was_white) // track at first whitespace in a series of whitespace
breakpoint = iter.pos();
last_was_white = 1;
} else {
// indicate next time around that this wasn't a whitespace character
last_was_white = 0;
}
auto encoded_width = unicode::encoded_size(cp);
Assertion(buf_index + encoded_width < SPLIT_STR_BUFFER_SIZE,
"buffer overflow in split_str: screen width causes this text to be longer than %d characters!",
SPLIT_STR_BUFFER_SIZE - 1);
// throw it in our buffer
unicode::encode(cp, &buffer[buf_index]);
buf_index += (int)encoded_width;
buffer[buf_index] = 0; // null terminate it
gr_get_string_size(&sw, NULL, buffer);
if (sw >= max_pixel_w || buf_index >= max_line_length) {
const char *end;
if (breakpoint) {
end = breakpoint;
iter = unicode::text_iterator(breakpoint, src, src + strlen(src));
} else {
end = iter.pos(); // force a split here since to whitespace
--iter; // reuse this character in next line
}
n_chars[line_num] = (int)(end - p_str[line_num]); // track length of line
Assert(n_chars[line_num]);
line_num++;
if (line_num < max_lines) {
p_str[line_num] = NULL;
}
new_line = 1;
memset(buffer, 0, sizeof(buffer));
buf_index = 0;
continue;
}
} // end for
if (!new_line && p_str[line_num]) {
n_chars[line_num] = (int)(iter.pos() - p_str[line_num]); // track length of line
Assert(n_chars[line_num]);
line_num++;
}
return line_num;
}
int split_str(const char *src, int max_pixel_w, SCP_vector<int> &n_chars, SCP_vector<const char*> &p_str, int max_line_length, unicode::codepoint_t ignore_char, bool strip_leading_whitespace)
{
char buffer[SPLIT_STR_BUFFER_SIZE];
const char *breakpoint = NULL;
int sw, new_line = 1, line_num = 0, last_was_white = 0;
int ignore_until_whitespace = 0, buf_index = 0;
// check our assumptions..
Assert(src != NULL);
Assert(max_pixel_w > 0);
Assertion(max_line_length > 0, "Max line length should be >0, not %d; get a coder!\n", max_line_length);
memset(buffer, 0, sizeof(buffer));
// get rid of any leading whitespace
while (strip_leading_whitespace && is_white_space(*src))
src++;
p_str.clear();
// iterate through chars in line, keeping track of most recent "white space" location that can be used
// as a line splitting point if necessary
unicode::codepoint_range range(src);
auto end_iter = std::end(range);
auto iter = std::begin(range);
for (; iter != end_iter; ++iter) {
auto cp = *iter;
// starting a new line of text, init stuff for that
if (new_line) {
if (strip_leading_whitespace && is_gray_space(cp))
continue;
p_str.push_back(iter.pos());
breakpoint = NULL;
new_line = 0;
}
// maybe skip leading whitespace
if (ignore_until_whitespace) {
if ( is_white_space(cp) ) {
ignore_until_whitespace = 0;
// don't eat the newline
if (cp == EOLN)
--iter;
}
continue;
}
// if we have a newline, split the line here
if (cp == UNICODE_CHAR('\n')) {
n_chars.push_back((int)(iter.pos() - p_str[line_num])); // track length of line
line_num++;
new_line = 1;
memset(buffer, 0, SPLIT_STR_BUFFER_SIZE);
buf_index = 0;
continue;
}
if (cp == ignore_char) {
ignore_until_whitespace = 1;
continue;
}
if (is_gray_space(cp)) {
if (!last_was_white) // track at first whitespace in a series of whitespace
breakpoint = iter.pos();
last_was_white = 1;
} else {
// indicate next time around that this wasn't a whitespace character
last_was_white = 0;
}
auto encoded_width = unicode::encoded_size(cp);
Assertion(buf_index + encoded_width < SPLIT_STR_BUFFER_SIZE,
"buffer overflow in split_str: screen width causes this text to be longer than %d characters!",
SPLIT_STR_BUFFER_SIZE - 1);
// throw it in our buffer
unicode::encode(cp, &buffer[buf_index]);
buf_index += (int)encoded_width;
buffer[buf_index] = 0; // null terminate it
gr_get_string_size(&sw, NULL, buffer);
if (sw >= max_pixel_w || buf_index >= max_line_length) {
const char *end;
if (breakpoint) {
end = breakpoint;
iter = unicode::text_iterator(breakpoint, src, src + strlen(src));
} else {
end = iter.pos(); // force a split here since to whitespace
--iter; // reuse this character in next line
}
n_chars.push_back((int)(end - p_str[line_num])); // track length of line
Assert(n_chars[line_num]);
line_num++;
new_line = 1;
memset(buffer, 0, sizeof(buffer));
buf_index = 0;
continue;
}
} // end for
if (!new_line && p_str[line_num]) {
n_chars.push_back((int)(iter.pos() - p_str[line_num])); // track length of line
Assert(n_chars[line_num]);
line_num++;
}
return line_num;
}
// A narrower but much faster alternative to split_str(), takes a string and a max pixel length, returns a vector with
// one pair (offset and length) per line. Does not currently support a max line count or ignoring of characters.
SCP_vector<std::pair<size_t, size_t>> str_wrap_to_width(const SCP_string& source_string, int max_pixel_width, bool strip_leading_whitespace, size_t source_start, size_t source_length)
{
auto lines = SCP_vector<std::pair<size_t, size_t>>();
if (source_length == std::string::npos)
source_length = source_string.length() - source_start;
Assertion(source_start + source_length <= source_string.length(), "In str_wrap_to_width(), source length must not exceed the actual length of the string!");
size_t pos_start = source_start;
size_t pos_end = source_start + source_length;
// Advance past leading whitespace.
while (strip_leading_whitespace && (pos_start < pos_end) && is_white_space(source_string[pos_start]))
pos_start++;
// Handle existing line breaks in the string recursively, then append the results.
while (pos_start < pos_end) {
auto newline_at = source_string.find_first_of(UNICODE_CHAR('\n'), pos_start);
if (newline_at == std::string::npos || newline_at >= source_length)
break;
if (newline_at == pos_start) {
// No content to split so just pushing a new string on.
lines.emplace_back(pos_start, 0);
} else {
auto sublines = str_wrap_to_width(source_string, max_pixel_width, strip_leading_whitespace, pos_start, (newline_at - pos_start));
lines.reserve(lines.size() + sublines.size()); // coverity[inefficient_reserve:FALSE]
std::move(sublines.begin(), sublines.end(), std::back_inserter(lines));
}
pos_start = newline_at + 1;
}
// With newlines handled, now move into actually wrapping the content.
while (pos_start < pos_end) {
auto split_at = std::string::npos;
// no newlines found, check length.
size_t stringlen = pos_end - pos_start;
int line_width = 0;
gr_get_string_size(&line_width, nullptr, source_string.c_str() + pos_start, 1.0f, stringlen);
if (stringlen <= 1) {
// in this case checking is pointless, single-character strings can't wrap.
// copy into the return vector and then bail.
lines.emplace_back(pos_start, stringlen);
break;
} else if (line_width < max_pixel_width) {
// The remaining string is shorter than our limit so we're done.
// copy into the return vector and then bail.
lines.emplace_back(pos_start, stringlen);
break;
} else {
size_t search_min = 0;
size_t search_max = stringlen;
size_t center = 0;
while (search_max > search_min) {
center = search_min + ((search_max - search_min) / 2);
gr_get_string_size(&line_width, nullptr, source_string.c_str() + pos_start, 1.0f, center);
if (line_width == max_pixel_width) {
search_max = center;
search_min = center;
split_at = center;
} else if (line_width > max_pixel_width) {
search_max = MIN(center, search_max - 1);
split_at = search_max;
} else {
search_min = MAX(center, search_min + 1);
split_at = search_min;
}
}
}
// don't split out of bounds, but it's ok to split exactly on the string boundary
if (split_at > stringlen) {
split_at = stringlen;
} else if (split_at < stringlen) {
// split_at is now the last point where we can split, but could be mid-word
// work backwards to find whitespace.
while ((split_at > 0) && !is_white_space(source_string[pos_start + split_at]))
split_at--;
// we need to always remove something from the current line or we're stuck
if (split_at == 0)
split_at = 1;
}
lines.emplace_back(pos_start, split_at);
pos_start += split_at;
// Trim the leading whitespace off the next line.
while ((pos_start < pos_end) && is_white_space(source_string[pos_start]))
pos_start++;
}
return lines;
}
SCP_vector<std::pair<size_t, size_t>> str_wrap_to_width(const char* source_string, int max_pixel_width, bool strip_leading_whitespace, size_t source_length)
{
auto lines = SCP_vector<std::pair<size_t, size_t>>();
if (source_length == std::string::npos)
source_length = strlen(source_string);
Assertion(source_length <= strlen(source_string), "In str_wrap_to_width(), source length must not exceed the actual length of the string!");
const char* ch_start = source_string;
const char* ch_end = ch_start + source_length;
// Advance past leading whitespace.
while (strip_leading_whitespace && (ch_start < ch_end) && is_white_space(*ch_start))
ch_start++;
// Handle existing line breaks in the string recursively, then append the results.
while (ch_start < ch_end) {
auto newline_at = strchr(ch_start, UNICODE_CHAR('\n'));
if (newline_at == nullptr || newline_at >= ch_end)
break;
if (newline_at == ch_start) {
// No content to split so just pushing a new string on.
lines.emplace_back(ch_start - source_string, 0);
} else {
auto sublines = str_wrap_to_width(ch_start, max_pixel_width, strip_leading_whitespace, (newline_at - ch_start));
// need to adjust the positions to make them relative to the source string
if (ch_start != source_string)
for (auto& subline : sublines)
subline.first += (ch_start - source_string);
lines.reserve(lines.size() + sublines.size()); // coverity[inefficient_reserve:FALSE]
std::move(sublines.begin(), sublines.end(), std::back_inserter(lines));
}
ch_start = newline_at + 1;
}
// With newlines handled, now move into actually wrapping the content.
while (ch_start < ch_end) {
auto split_at = std::string::npos;
// no newlines found, check length.
size_t stringlen = ch_end - ch_start;
int line_width = 0;
gr_get_string_size(&line_width, nullptr, ch_start, 1.0f, stringlen);
if (stringlen <= 1) {
// in this case checking is pointless, single-character strings can't wrap.
// copy into the return vector and then bail.
lines.emplace_back(ch_start - source_string, stringlen);
break;
} else if (line_width < max_pixel_width) {
// The remaining string is shorter than our limit so we're done.
// copy into the return vector and then bail.
lines.emplace_back(ch_start - source_string, stringlen);
break;
} else {
size_t search_min = 0;
size_t search_max = stringlen;
size_t center = 0;
while (search_max > search_min) {
center = search_min + ((search_max - search_min) / 2);
gr_get_string_size(&line_width, nullptr, ch_start, 1.0f, center);
if (line_width == max_pixel_width) {
search_max = center;
search_min = center;
split_at = center;
} else if (line_width > max_pixel_width) {
search_max = MIN(center, search_max - 1);
split_at = search_max;
} else {
search_min = MAX(center, search_min + 1);
split_at = search_min;
}
}
}
// don't split out of bounds, but it's ok to split exactly on the string boundary
if (split_at > stringlen) {
split_at = stringlen;
} else if (split_at < stringlen) {
// split_at is now the last point where we can split, but could be mid-word
// work backwards to find whitespace.
while ((split_at > 0) && !is_white_space(*(ch_start + split_at)))
split_at--;
// we need to always remove something from the current line or we're stuck
if (split_at == 0)
split_at = 1;
}
lines.emplace_back(ch_start - source_string, split_at);
ch_start += split_at;
// Trim the leading whitespace off the next line.
while ((ch_start < ch_end) && is_white_space(*ch_start))
ch_start++;
}
return lines;
}
// Goober5000
// accounts for the dumb communications != communication, etc.
int subsystem_stricmp(const char *str1, const char *str2)
{
Assert(str1 && str2);
// ensure len-1 will be valid
if (!*str1 || !*str2)
return stricmp(str1, str2);
// calc lengths
auto len1 = (int)strlen(str1);
auto len2 = (int)strlen(str2);
// get rid of trailing s on s1?
if (SCP_tolower(*(str1+len1-1)) == 's')
len1--;
// get rid of trailing s on s2?
if (SCP_tolower(*(str2+len2-1)) == 's')
len2--;
// once we remove the trailing s on both names, they should be the same length
if (len1 == len2)
return strnicmp(str1, str2, len1);
// if not, just do a regular comparison
return stricmp(str1, str2);
}
// Goober5000
// current algorithm adapted from http://www.codeproject.com/string/stringsearch.asp
const char *stristr(const char *str, const char *substr)
{
// check for null and insanity
Assert(str);
Assert(substr);
if (str == NULL || substr == NULL || *substr == '\0')
return NULL;
// save both a lowercase and an uppercase version of the first character of substr
char substr_ch_lower = SCP_tolower(*substr);
char substr_ch_upper = SCP_toupper(*substr);
// find the maximum distance to search
const char *upper_bound = str + strlen(str) - strlen(substr);
// loop through every character of str
for (const char *start = str; start <= upper_bound; start++)
{
// check first character of substr
if ((*start == substr_ch_upper) || (*start == substr_ch_lower))
{
// first character matched, so check the rest
for (const char *str_ch = start+1, *substr_ch = substr+1; *substr_ch != '\0'; str_ch++, substr_ch++)
{
// character match?
if (*str_ch == *substr_ch)
continue;
// converted character match?
if (SCP_tolower(*str_ch) == SCP_tolower(*substr_ch))
continue;
// mismatch
goto stristr_continue_outer_loop;
}
// finished inner loop with success!
return start;
}
stristr_continue_outer_loop:
/* NO-OP */ ;
}
// no match
return NULL;
}
// non-const version
char *stristr(char *str, const char *substr)
{
// check for null and insanity
Assert(str);
Assert(substr);
if (str == NULL || substr == NULL || *substr == '\0')
return NULL;
// save both a lowercase and an uppercase version of the first character of substr
char substr_ch_lower = SCP_tolower(*substr);
char substr_ch_upper = SCP_toupper(*substr);
// find the maximum distance to search
const char *upper_bound = str + strlen(str) - strlen(substr);
// loop through every character of str
for (char *start = str; start <= upper_bound; start++)
{
// check first character of substr
if ((*start == substr_ch_upper) || (*start == substr_ch_lower))
{
// first character matched, so check the rest
for (const char *str_ch = start+1, *substr_ch = substr+1; *substr_ch != '\0'; str_ch++, substr_ch++)
{
// character match?
if (*str_ch == *substr_ch)
continue;
// converted character match?
if (SCP_tolower(*str_ch) == SCP_tolower(*substr_ch))
continue;
// mismatch
goto stristr_continue_outer_loop;
}
// finished inner loop with success!
return start;
}
stristr_continue_outer_loop:
/* NO-OP */ ;
}
// no match
return NULL;
}
// Goober5000
bool can_construe_as_integer(const char *text)
{
// trivial case; evaluates to 0
if (*text == '\0')
return true;
// number sign or digit for first char
if ((*text != '+') && (*text != '-') && !isdigit(*text))
return false;
// check digits for rest
for (const char *p = text + 1; *p != '\0'; p++)
{
if (!isdigit(*p))
return false;
}
return true;
}
// Goober5000
// yoinked gratefully from dbugfile.cpp
void vsprintf(SCP_string &dest, const char *format, va_list ap, size_t write_offset)
{
va_list copy;
va_copy(copy, ap);
int needed_length = vsnprintf(nullptr, 0, format, copy);
va_end(copy);
if (needed_length < 0) {
// Error
return;
}
dest.resize(write_offset + i2sz(needed_length));
vsnprintf(&dest[write_offset], i2sz(needed_length) + 1, format, ap);
}
void sprintf(SCP_string &dest, SCP_FORMAT_STRING const char *format, ...)
{
va_list args;
va_start(args, format);
vsprintf(dest, format, args);
va_end(args);
}
void sprintf_concat(SCP_string &dest, SCP_FORMAT_STRING const char *format, ...)
{
va_list args;
va_start(args, format);
vsprintf(dest, format, args, dest.size());
va_end(args);
}
// Goober5000
bool end_string_at_first_hash_symbol(char *src, bool ignore_doubled_hash)
{
char *p;
Assert(src);
p = get_pointer_to_first_hash_symbol(src, ignore_doubled_hash);
if (p)
{
while ((p != src) && (*(p-1) == ' '))
p--;
*p = '\0';
return true;
}
return false;
}
// Goober5000
bool end_string_at_first_hash_symbol(SCP_string &src, bool ignore_doubled_hash)
{
int index = get_index_of_first_hash_symbol(src, ignore_doubled_hash);
if (index >= 0)
{
while (index > 0 && src[index-1] == ' ')
index--;
src.resize(index);
return true;
}
return false;
}
// Goober5000
char *get_pointer_to_first_hash_symbol(char *src, bool ignore_doubled_hash)
{
Assert(src);
if (ignore_doubled_hash)
{
for (auto ch = src; *ch; ++ch)
{
if (*ch == '#')
{
if (*(ch + 1) == '#')
++ch;
else
return ch;
}
}
return nullptr;
}
else
return strchr(src, '#');
}
// Goober5000
const char *get_pointer_to_first_hash_symbol(const char *src, bool ignore_doubled_hash)
{
Assert(src);
if (ignore_doubled_hash)
{
for (auto ch = src; *ch; ++ch)
{
if (*ch == '#')
{
if (*(ch + 1) == '#')
++ch;
else
return ch;
}
}
return nullptr;
}
else
return strchr(src, '#');
}
// Goober5000
int get_index_of_first_hash_symbol(const SCP_string &src, bool ignore_doubled_hash)
{
if (ignore_doubled_hash)
{
for (auto ch = src.begin(); ch != src.end(); ++ch)
{
if (*ch == '#')
{
if ((ch + 1) != src.end() && *(ch + 1) == '#')
++ch;
else
return static_cast<int>(std::distance(src.begin(), ch));
}
}
return -1;
}
else
{
size_t pos = src.find('#');
return (pos == SCP_string::npos) ? -1 : (int)pos;
}
}
// Goober5000
// Used for escape sequences: ## to #, !! to !, etc.
void consolidate_double_characters(char *src, char ch)
{
auto dest = src;
while (*src)
{
if (*src == ch && *(src + 1) == ch)
--dest;
++src;
++dest;
if (src != dest)
*dest = *src;
}
}
// Goober5000
// Used for escape sequences: ## to #, !! to !, etc.
void consolidate_double_characters(SCP_string &str, char ch)
{
auto src = str.begin();
auto dest = src;
while (src != str.end())
{
if (*src == ch && *(src + 1) == ch)
--dest;
++src;
++dest;
if (src != dest && src != str.end())
*dest = *src;
}
if (src != dest)
str.resize(dest - str.begin());
}
char *three_dot_truncate(char *buffer, const char *source, size_t buffer_size)
{
Assertion(buffer && source, "Arguments must not be null!");
// this would be silly
if (buffer_size < 6)
{
*buffer = '\0';
return buffer;
}
strncpy(buffer, source, buffer_size);
if (buffer[buffer_size - 1] != '\0')
strcpy(&buffer[buffer_size - 6], "[...]");
return buffer;
}
// Goober5000
// Returns position of replacement, or a negative value if replacement failed: -1 if search string was not found, -2 if replacement would exceed max length, or -3 if any string argument is null
// Note that the parameter here is max *length*, not max buffer size. Leave room for the null-terminator!
ptrdiff_t replace_one(char *str, const char *oldstr, const char *newstr, size_t max_len, ptrdiff_t range)
{
Assertion(str && oldstr && newstr, "Arguments must not be null!");
Assertion(max_len < SIZE_MAX, "Size must be less than SIZE_MAX because an extra char is added for the null terminator.");
if (!str || !oldstr || !newstr || max_len == SIZE_MAX)
return -3;
// search
char *ch = stristr(str, oldstr);
// found?
if (ch)
{
// not found within bounds?
if ((range > 0) && ((ch - str) > range))
{
return -1;
}
// determine if replacement will exceed max len
if (strlen(str) + strlen(newstr) - strlen(oldstr) > max_len)
{
return -2;
}
// allocate temp string to hold extra stuff
char *temp = (char *) vm_malloc(sizeof(char) * (max_len + 1));
// ensure allocation was successful
if (temp)
{
// save remainder of string
strcpy(temp, ch + strlen(oldstr));
// replace
strcpy(ch, newstr);
// append rest of string
strcpy(ch + strlen(newstr), temp);
}
// free temp string
vm_free(temp);
}
// not found
else
{
return -1;
}
// return pos of replacement
return (ch - str);
}
// Goober5000
// Returns number of replacements or a negative result from replace_one if a replacement failed for some reason other than not found
// Note that the parameter here is max *length*, not max buffer size. Leave room for the null-terminator!
int replace_all(char *str, const char *oldstr, const char *newstr, size_t max_len, ptrdiff_t range)
{
ptrdiff_t val;
int tally = 0;
while ((val = replace_one(str, oldstr, newstr, max_len, range)) >= 0)
{
tally++;
// adjust range (if we have one), because the text length might have changed
if (range) {
range += strlen(newstr) - strlen(oldstr);
}
}
// return the tally, even if it's 0, unless there was an exceptional situation like exceeding max len
return (val < -1) ? (int)val : tally;
}
// Goober5000
// Returns position of replacement, or -1 if search string was not found
ptrdiff_t replace_one(SCP_string& context, const SCP_string& from, const SCP_string& to)
{
size_t foundHere;
if ((foundHere = context.find(from, 0)) != SCP_string::npos)
{
context.replace(foundHere, from.length(), to);
return foundHere;
}
else
return -1;
}
// Goober5000
// Returns position of replacement, or -1 if search string was not found
ptrdiff_t replace_one(SCP_string& context, const char* from, const char* to)
{
size_t foundHere;
if ((foundHere = context.find(from, 0)) != SCP_string::npos)
{
context.replace(foundHere, strlen(from), to);
return foundHere;
}
else
return -1;
}
// Goober5000
// Returns number of replacements
// http://www.cppreference.com/wiki/string/replace
int replace_all(SCP_string& context, const SCP_string& from, const SCP_string& to)
{
size_t from_len = from.length();
size_t to_len = to.length();
size_t lookHere = 0;
size_t foundHere;
int tally = 0;
while ((foundHere = context.find(from, lookHere)) != SCP_string::npos)
{
tally++;
context.replace(foundHere, from_len, to);
lookHere = foundHere + to_len;
}
return tally;
}
// Goober5000
// Returns number of replacements
// http://www.cppreference.com/wiki/string/replace
int replace_all(SCP_string& context, const char* from, const char* to)
{
size_t from_len = strlen(from);
size_t to_len = strlen(to);
size_t lookHere = 0;
size_t foundHere;
int tally = 0;
while ((foundHere = context.find(from, lookHere)) != SCP_string::npos)
{
tally++;
context.replace(foundHere, from_len, to);
lookHere = foundHere + to_len;
}
return tally;
}
// WMC
// Compares two strings, ignoring (last) extension
// Returns 0 if equal, nonzero if not
int strextcmp(const char *s1, const char *s2)
{
// sanity check
Assert( (s1 != NULL) && (s2 != NULL) );
// find last '.' in both strings
char *s1_end = (char *)strrchr(s1, '.');
char *s2_end = (char *)strrchr(s2, '.');
// get length
size_t s1_len, s2_len;
if (s1_end != NULL)
s1_len = (s1_end - s1);
else
s1_len = strlen(s1);
if (s2_end != NULL)
s2_len = (s2_end - s2);
else
s2_len = strlen(s2);
// if the lengths aren't the same then it's deffinitely not the same name
if (s2_len != s1_len)
return 1;
return strnicmp(s1, s2, s1_len);
}
// Goober5000
bool drop_extension(char *str)
{
char *p = strrchr(str, '.');
if (p != NULL)
{
*p = 0;
return true;
}
return false;
}
// Goober5000
bool drop_extension(SCP_string &str)
{
size_t pos = str.rfind('.');
if (pos != SCP_string::npos)
{
str.resize(pos);
return true;
}
return false;
}
//WMC
void backspace(char* src)
{
Assert(src!= NULL); //this would be bad
char *dest = src;
src++;
while(*src != '\0') {
*dest++ = *src++;
}
*dest = '\0';
}
// Goober5000
void format_integer_with_commas(char *buf, int integer, bool use_comma_with_four_digits)
{
int old_pos, new_pos, triad_count;
char backward_buf[32];
// print an initial string of just the digits
sprintf(buf, "%d", integer);
// no commas needed?
if ((integer < 1000) || (integer < 10000 && !use_comma_with_four_digits))
return;
// scan the string backwards, writing commas after every third digit
new_pos = 0;
triad_count = 0;
for (old_pos = (int)strlen(buf) - 1; old_pos >= 0; old_pos--)
{
backward_buf[new_pos] = buf[old_pos];
new_pos++;
triad_count++;
if (triad_count == 3 && old_pos > 0)
{
backward_buf[new_pos] = ',';
new_pos++;
triad_count = 0;
}
}
backward_buf[new_pos] = '\0';
// now reverse the string
new_pos = 0;
for (old_pos = (int)strlen(backward_buf) - 1; old_pos >= 0; old_pos--)
{
buf[new_pos] = backward_buf[old_pos];
new_pos++;
}
buf[new_pos] = '\0';
}
// Goober5000
// there's probably a better way to do this, but this way works and is clear and short
int scan_fso_version_string(const char *text, int *major, int *minor, int *build, int *revis)
{
int val;
val = sscanf(text, ";;FSO %i.%i.%i.%i;;", major, minor, build, revis);
if (val == 4)
return val;
*revis = 0;
val = sscanf(text, ";;FSO %i.%i.%i;;", major, minor, build);
if (val == 3)
return val;
*build = *revis = 0;
val = sscanf(text, ";;FSO %i.%i;;", major, minor);
if (val == 2)
return val;
*minor = *build = *revis = 0;
val = sscanf(text, ";;FSO %i;;", major);
if (val == 1)
return val;
*major = *minor = *build = *revis = 0;
return 0;
}
// Goober5000 - used for long Warnings, Errors, and FRED error messages with SEXPs
void truncate_message_lines(SCP_string &text, int num_allowed_lines)
{
Assert(num_allowed_lines > 0);
size_t find_from = 0;
while (find_from < text.size())
{
if (num_allowed_lines <= 0)
{
text.resize(find_from);
text.append("[...]");
break;
}
size_t pos = text.find('\n', find_from);
if (pos == SCP_string::npos)
break;
num_allowed_lines--;
find_from = pos + 1;
}
}
// Goober5000 - ugh, I can't see why they didn't just use stuff_*_list for these;
// the only difference is the lack of parentheses
// from aicode.cpp
// Stuff a list of floats at *plist.
void parse_float_list(float *plist, size_t size)
{
for (size_t i=0; i<size; i++)
{
stuff_float(&plist[i]);
}
}
// from aicode.cpp
// Stuff a list of ints at *plist.
void parse_int_list(int *ilist, size_t size)
{
for (size_t i=0; i<size; i++)
{
stuff_int(&ilist[i]);
}
}
void parse_string_map(SCP_map<SCP_string, SCP_string>& outMap, const char* end_marker, const char* entry_prefix)
{
while(optional_string(entry_prefix))
{
SCP_string temp;
stuff_string(temp, F_RAW);
drop_white_space(temp);
if (temp.empty())
{
Warning(LOCATION, "Empty entry in string map.");
continue;
}
size_t sep = temp.find_first_of(' ');
SCP_string key = temp.substr(0, sep);
SCP_string value = temp.substr(sep+1);
//if the modder didn't add a value, make the value an empty string. (Without this, value would instead be an identical string to key)
if (sep == SCP_string::npos)
value = "";
drop_white_space(key);
drop_white_space(value);
outMap.emplace(key, value);
}
required_string(end_marker);
}
// parse a modular table of type "name_check" and parse it using the specified function callback
int parse_modular_table(const char *name_check, void (*parse_callback)(const char *filename), int path_type, int sort_type)
{
SCP_vector<SCP_string> tbl_file_names;
int i, num_files = 0;
Assertion( (name_check != nullptr) && (parse_callback != nullptr) && ((*name_check) == '*'), "parse_modular_table() called with invalid arguments; get a coder!\n");
if ( (name_check == nullptr) || (parse_callback == nullptr) || ((*name_check) != '*') ) {
return 0;
}
num_files = cf_get_file_list(tbl_file_names, path_type, name_check, sort_type);
Parsing_modular_table = true;
const auto ext = strrchr(name_check, '.');
for (i = 0; i < num_files; i++){
if (ext != nullptr) {
tbl_file_names[i] += ext;
}
mprintf(("TBM => Starting parse of '%s' ...\n", tbl_file_names[i].c_str()));
(*parse_callback)(tbl_file_names[i].c_str());
}
Parsing_modular_table = false;
return num_files;
}