diff --git a/engine/openbor.c b/engine/openbor.c index 92db70feb..328648652 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -33,9 +33,15 @@ s_sprite_list *sprite_list; s_sprite_map *sprite_map; s_savelevel *savelevel; +static char **savelevel_allowselect_args; +static size_t savelevel_count; s_savescore savescore; s_savedata savedata; +static void clear_saved_allowselect_arguments(void); +static const char* get_saved_allowselect_arguments(size_t index); +static void set_saved_allowselect_arguments(size_t index, const char* source); + ///////////////////////////////////////////////////////////////////////////// // Global Variables // ///////////////////////////////////////////////////////////////////////////// @@ -285,7 +291,7 @@ char *custModels = NULL; char rush_names[2][MAX_NAME_LEN]; char skipselect[MAX_PLAYERS][MAX_NAME_LEN]; char branch_name[MAX_NAME_LEN + 1]; // Used for branches -char allowselect_args[MAX_ALLOWSELECT_LEN]; // stored allowselect players +char *allowselect_args = NULL; // stored allowselect players int useSave = 0; int useSet = -1; unsigned char pal[MAX_PAL_SIZE] = {""}; @@ -1019,11 +1025,13 @@ int buffer_pakfile(const char *filename, char **pbuffer, size_t *psize) int buffer_append(char **buffer, const char *str, size_t n, size_t *bufferlen, size_t *len) { - size_t appendlen = strlen(str); - if(appendlen > n) + size_t appendlen = 0; + + while(appendlen < n && str[appendlen]) { - appendlen = n; + appendlen++; } + if(appendlen + *len + 1 > *bufferlen) { //printf("*Debug* reallocating buffer...\n"); @@ -1033,12 +1041,60 @@ int buffer_append(char **buffer, const char *str, size_t n, size_t *bufferlen, s borShutdown(1, "Unable to resize buffer.\n"); } } - strncpy(*buffer + *len, str, appendlen); + memcpy(*buffer + *len, str, appendlen); *len = *len + appendlen; (*buffer)[*len] = 0; return *len; } +/* +- Caskey, Damon V. +- 2026-08-12 +- +- Verify and atomically remove two adjoining suffixes + from a generated text buffer. Leave the buffer intact + if its tail does not match the complete sequence. +*/ +static bool buffer_remove_suffix_pair( + char* buffer, + size_t* length, + const char* first, + size_t first_length, + const char* second, + size_t second_length +) { + size_t pair_length; + + assert(length); + assert(first); + assert(second); + + if(!buffer || first_length > SIZE_MAX - second_length) { + return false; + } + + pair_length = first_length + second_length; + + if(*length < pair_length + || memcmp( + buffer + *length - second_length, + second, + second_length + ) + || memcmp( + buffer + *length - pair_length, + first, + first_length + )) { + return false; + } + + *length -= pair_length; + buffer[*length] = '\0'; + + return true; +} + int handle_txt_include(char *command, ArgList *arglist, char **fn, char *namebuf, char **buf, ptrdiff_t *pos, size_t *len) { char *incfile, *filename = *fn, *buf2, *endstr = "\r\n@end"; @@ -2753,11 +2809,157 @@ void loadfromdefault() } +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Append complete allowselect state to the fixed-record save + file using length-prefixed values without imposing a list-size + limit. +*/ +#define SAVE_ALLOWSELECT_EXTENSION_MAGIC UINT64_C(0x5443454C45534C41) +#define SAVE_ALLOWSELECT_EXTENSION_VERSION UINT32_C(1) + +static bool write_saved_allowselect_extension(FILE* handle) +{ + const uint64_t magic = SAVE_ALLOWSELECT_EXTENSION_MAGIC; + const uint32_t version = SAVE_ALLOWSELECT_EXTENSION_VERSION; + const uint64_t entry_count = (uint64_t)savelevel_count; + size_t i; + + if(fwrite(&magic, sizeof(magic), 1, handle) != 1 + || fwrite(&version, sizeof(version), 1, handle) != 1 + || fwrite(&entry_count, sizeof(entry_count), 1, handle) != 1) + { + return false; + } + + for(i = 0; i < savelevel_count; i++) + { + const char* value = get_saved_allowselect_arguments(i); + const uint64_t length = value ? (uint64_t)strlen(value) : 0; + + if(fwrite(&length, sizeof(length), 1, handle) != 1 + || (length + && fwrite(value, 1, (size_t)length, handle) + != (size_t)length)) + { + return false; + } + } + + return true; +} + +static bool get_file_remaining_size(FILE* handle, uint64_t* remaining) +{ + long current_position; + long end_position; + + current_position = ftell(handle); + + if(current_position < 0 || fseek(handle, 0, SEEK_END) != 0) + { + return false; + } + + end_position = ftell(handle); + + if(end_position < current_position + || fseek(handle, current_position, SEEK_SET) != 0) + { + return false; + } + + *remaining = (uint64_t)(end_position - current_position); + + return true; +} + +static bool read_saved_allowselect_extension(FILE* handle) +{ + uint64_t magic; + uint32_t version; + uint64_t entry_count; + char** loaded_values; + size_t i; + + if(fread(&magic, sizeof(magic), 1, handle) != 1 + || magic != SAVE_ALLOWSELECT_EXTENSION_MAGIC) + { + return false; + } + + if(fread(&version, sizeof(version), 1, handle) != 1 + || fread(&entry_count, sizeof(entry_count), 1, handle) != 1) + { + return false; + } + + if(version != SAVE_ALLOWSELECT_EXTENSION_VERSION + || entry_count != (uint64_t)savelevel_count) + { + return false; + } + + loaded_values = calloc(savelevel_count, sizeof(*loaded_values)); + + for(i = 0; i < savelevel_count; i++) + { + uint64_t length; + uint64_t remaining; + + if(fread(&length, sizeof(length), 1, handle) != 1 + || length > (uint64_t)(SIZE_MAX - 1) + || !get_file_remaining_size(handle, &remaining) + || length > remaining) + { + goto error; + } + + if(length) + { + loaded_values[i] = malloc((size_t)length + 1); + + if(fread(loaded_values[i], 1, (size_t)length, handle) + != (size_t)length) + { + goto error; + } + + loaded_values[i][(size_t)length] = '\0'; + } + } + + for(i = 0; i < savelevel_count; i++) + { + set_saved_allowselect_arguments(i, loaded_values[i]); + free(loaded_values[i]); + } + + free(loaded_values); + return true; + +error: + for(i = 0; i < savelevel_count; i++) + { + free(loaded_values[i]); + } + + free(loaded_values); + return false; +} + void clearSavedGame() { - memset(savelevel, 0, sizeof(*savelevel)*num_difficulties); + clear_saved_allowselect_arguments(); + + if(savelevel) + { + memset(savelevel, 0, sizeof(*savelevel) * savelevel_count); + } } @@ -2777,6 +2979,7 @@ void clearHighScore() int saveGameFile() { + size_t i; FILE *handle = NULL; char path[MAX_BUFFER_LEN] = {""}; char tmpname[MAX_BUFFER_LEN] = {""}; @@ -2792,7 +2995,24 @@ int saveGameFile() return 0; } - fwrite(savelevel, sizeof(*savelevel), num_difficulties, handle); + if(!savelevel || savelevel_count != (size_t)num_difficulties) + { + fclose(handle); + return 0; + } + + for(i = 0; i < savelevel_count; i++) + { + savelevel[i].compatibleversion = CV_SAVED_GAME; + } + + if(fwrite(savelevel, sizeof(*savelevel), savelevel_count, handle) + != savelevel_count + || !write_saved_allowselect_extension(handle)) + { + fclose(handle); + return 0; + } fclose(handle); @@ -2802,7 +3022,8 @@ int saveGameFile() int loadGameFile() { - int result = 1, i; + int result = 1; + size_t i; FILE *handle = NULL; char path[MAX_BUFFER_LEN] = {""}; char tmpname[MAX_BUFFER_LEN] = {""}; @@ -2818,12 +3039,24 @@ int loadGameFile() return 0; } + if(!savelevel || savelevel_count != (size_t)num_difficulties) + { + fclose(handle); + return 0; + } + + clearSavedGame(); + //fseek(handle, 0L, SEEK_END); //filesize = ftell(handle); //fseek(handle, 0L, SEEK_SET); // or rewind(handle); //(filesize != sizeof(*savelevel)*num_difficulties) - if( (fread(savelevel, sizeof(*savelevel), num_difficulties, handle) >= sizeof(*savelevel) && savelevel[0].compatibleversion != CV_SAVED_GAME) ) + if(fread(savelevel, sizeof(*savelevel), savelevel_count, handle) + != savelevel_count + || (savelevel_count + && savelevel[0].compatibleversion != CV_SAVED_GAME) + || !read_saved_allowselect_extension(handle)) { clearSavedGame(); result = 0; @@ -2831,7 +3064,7 @@ int loadGameFile() else { bonus = 0; - for(i = 0; i < num_difficulties; i++) if(savelevel[i].times_completed > 0) + for(i = 0; i < savelevel_count; i++) if(savelevel[i].times_completed > 0) { bonus += savelevel[i].times_completed; } @@ -3210,6 +3443,7 @@ typedef struct s_command_token { const char* text; size_t length; + size_t value_length; } s_command_token; /* @@ -3221,22 +3455,73 @@ typedef struct s_command_token typedef struct s_command_token_reader { const char* cursor; + char unterminated_quote; } s_command_token_reader; +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Identify command quote delimiters and update quote state. + Double quotes may group text anywhere within an argument. + Single quotes may begin grouping only at the argument boundary, + allowing apostrophes in ordinary words to remain literal. +*/ +static bool command_token_update_quote_state( + const char* token_start, + const char* cursor, + bool* inside_double_quotes, + bool* inside_single_quotes +) { + bool escaped; + + assert(token_start); + assert(cursor); + assert(inside_double_quotes); + assert(inside_single_quotes); + + escaped = + cursor > token_start + && cursor[-1] == '\\'; + + if(*cursor == '"' && !escaped && !*inside_single_quotes) { + *inside_double_quotes = !*inside_double_quotes; + return true; + } + + if(*cursor == '\'' && !escaped && !*inside_double_quotes) { + if(*inside_single_quotes || cursor == token_start) { + *inside_single_quotes = !*inside_single_quotes; + return true; + } + } + + return false; +} + /* * Read the next token from a command line. * * Tokens end at whitespace, a line ending, a comment -* marker, or the null terminator. Quoted text may -* contain whitespace and comment markers. +* marker, or the null terminator. Quoted text may contain +* whitespace, line endings, and comment markers. Double +* quotes may open anywhere in an argument. Single quotes +* may open only at its beginning so ordinary apostrophes +* remain literal. Matching delimiters are omitted from the +* logical value, while the opposite quote type is literal. * * Return true when a token is available. Return false -* when the command line has no remaining tokens. +* when the command line has no remaining tokens or the +* current token contains an unterminated quote. The reader +* records the invalid delimiter for callers that distinguish +* malformed input from an ordinary end. */ static bool command_token_reader_next(s_command_token_reader* reader, s_command_token* token) { const char* cursor; const char* token_start; + size_t value_length = 0; + bool inside_double_quotes = false; bool inside_single_quotes = false; @@ -3263,6 +3548,7 @@ static bool command_token_reader_next(s_command_token_reader* reader, s_command_ reader->cursor = cursor; token->text = NULL; token->length = 0; + token->value_length = 0; return false; } @@ -3270,18 +3556,12 @@ static bool command_token_reader_next(s_command_token_reader* reader, s_command_ token_start = cursor; while(*cursor) { - const bool escaped = - cursor > token_start - && cursor[-1] == '\\'; - - if(*cursor == '"' && !escaped && !inside_single_quotes) { - inside_double_quotes = !inside_double_quotes; - cursor++; - continue; - } - - if(*cursor == '\'' && !escaped && !inside_double_quotes) { - inside_single_quotes = !inside_single_quotes; + if(command_token_update_quote_state( + token_start, + cursor, + &inside_double_quotes, + &inside_single_quotes + )) { cursor++; continue; } @@ -3296,16 +3576,338 @@ static bool command_token_reader_next(s_command_token_reader* reader, s_command_ } } + value_length++; cursor++; } + if(inside_double_quotes || inside_single_quotes) { + reader->cursor = cursor; + reader->unterminated_quote = inside_double_quotes ? '"' : '\''; + token->text = NULL; + token->length = 0; + token->value_length = 0; + + return false; + } + token->text = token_start; token->length = (size_t)(cursor - token_start); + token->value_length = value_length; reader->cursor = cursor; return true; } +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Copy a command token while discarding quote delimiters. + The destination receives the logical argument text and + is always null terminated on success. +*/ +static bool command_token_copy_value( + const s_command_token* token, + char* destination, + const size_t capacity +) { + const char* cursor; + const char* source_end; + + size_t destination_index = 0; + + bool inside_double_quotes = false; + bool inside_single_quotes = false; + + assert(token); + assert(destination); + + if(!token->text) { + return false; + } + + source_end = token->text + token->length; + + if(capacity <= token->value_length) { + return false; + } + + if(token->length == token->value_length) { + memcpy(destination, token->text, token->length); + destination[token->length] = '\0'; + return true; + } + + for(cursor = token->text; cursor < source_end; cursor++) { + if(command_token_update_quote_state( + token->text, + cursor, + &inside_double_quotes, + &inside_single_quotes + )) { + continue; + } + + destination[destination_index++] = *cursor; + } + + assert(!inside_double_quotes); + assert(!inside_single_quotes); + assert(destination_index == token->value_length); + + destination[destination_index] = '\0'; + + return true; +} + +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Read one requested command argument sequentially from the + source line. Return its non-owning source view and decoded + length so the caller can allocate only the required storage. + Distinguish malformed quoting from a missing argument. +*/ +e_command_argument_read_result command_argument_read( + const char* command_line, + size_t argument_index, + s_command_argument_view* argument +) { + s_command_token_reader reader = { + .cursor = command_line + }; + + s_command_token token; + + assert(command_line); + assert(argument); + + *argument = (s_command_argument_view){0}; + + while(argument_index) { + if(!command_token_reader_next(&reader, &token)) { + return reader.unterminated_quote + ? COMMAND_ARGUMENT_READ_INVALID + : COMMAND_ARGUMENT_READ_END; + } + + argument_index--; + } + + if(!command_token_reader_next(&reader, &token)) { + return reader.unterminated_quote + ? COMMAND_ARGUMENT_READ_INVALID + : COMMAND_ARGUMENT_READ_END; + } + + argument->source = token.text; + argument->source_length = token.length; + argument->length = token.value_length; + + return COMMAND_ARGUMENT_READ_SUCCESS; +} + +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Copy a previously read command argument into caller-owned + storage while discarding its opening and closing quote + delimiters. Preserve literal apostrophes and opposite quotes. +*/ +bool command_argument_copy( + const s_command_argument_view* argument, + char* destination, + const size_t capacity +) { + s_command_token token; + + assert(argument); + assert(destination); + + token = (s_command_token){ + .text = argument->source, + .length = argument->source_length, + .value_length = argument->length + }; + + return command_token_copy_value(&token, destination, capacity); +} + +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Reserve fixed storage for one sequentially read command + argument. Keep this independent from the legacy command-line, + script file-stream, path, and persistent save-field limit. +*/ +#define MAX_COMMAND_ARGUMENT_LEN 512 + +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Maintain sequential command argument reading state. The + underlying token reader walks the source line directly, + while one fixed scratch buffer holds only the current item. +*/ +typedef struct s_command_argument_reader +{ + s_command_token_reader token_reader; + char value[MAX_COMMAND_ARGUMENT_LEN]; +} s_command_argument_reader; + +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Initialize a command argument reader at the requested + argument index. Skip preceding items directly in the + source line without collecting them into a buffer. +*/ +static bool command_argument_reader_initialize( + s_command_argument_reader* reader, + const char* command_line, + size_t argument_index +) { + s_command_token skipped_token; + + assert(reader); + assert(command_line); + + *reader = (s_command_argument_reader){ + .token_reader = { + .cursor = command_line + } + }; + + while(argument_index) { + if(!command_token_reader_next( + &reader->token_reader, + &skipped_token + )) { + if(reader->token_reader.unterminated_quote) { + borShutdown( + 1, + "Command argument has an unterminated %c quote.\n", + reader->token_reader.unterminated_quote + ); + } + + return false; + } + + argument_index--; + } + + return true; +} + +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Read the next command argument into the reader's reusable + fixed buffer. Enforce the dedicated per-item length limit + without imposing whole-line or argument-count limits. +*/ +static bool command_argument_reader_next( + s_command_argument_reader* reader, + const char** value +) { + s_command_token token; + + assert(reader); + assert(value); + + if(!command_token_reader_next(&reader->token_reader, &token)) { + if(reader->token_reader.unterminated_quote) { + borShutdown( + 1, + "Command argument has an unterminated %c quote.\n", + reader->token_reader.unterminated_quote + ); + } + + *value = NULL; + return false; + } + + if(token.value_length >= sizeof(reader->value)) { + borShutdown( + 1, + "Command argument exceeds the maximum length of %zu characters.\n", + sizeof(reader->value) - 1 + ); + *value = NULL; + return false; + } + + if(!command_token_copy_value( + &token, + reader->value, + sizeof(reader->value) + )) { + *value = NULL; + return false; + } + + *value = reader->value; + + return true; +} + +static void clear_saved_allowselect_arguments(void) +{ + size_t i; + + if(!savelevel_allowselect_args) { + return; + } + + for(i = 0; i < savelevel_count; i++) { + free(savelevel_allowselect_args[i]); + savelevel_allowselect_args[i] = NULL; + } +} + +static const char* get_saved_allowselect_arguments(size_t index) +{ + if(index >= savelevel_count || !savelevel_allowselect_args) { + return NULL; + } + + return savelevel_allowselect_args[index]; +} + +static void set_saved_allowselect_arguments( + size_t index, + const char* source +) { + char* owned_source = NULL; + + if(index >= savelevel_count || !savelevel_allowselect_args) { + return; + } + + if(source && source[0]) { + const size_t length = strlen(source); + + if(length == SIZE_MAX) { + borShutdown(1, "Allowselect state exceeds addressable memory.\n"); + return; + } + + owned_source = malloc(length + 1); + memcpy(owned_source, source, length + 1); + } + + free(savelevel_allowselect_args[index]); + savelevel_allowselect_args[index] = owned_source; +} + /* * Compare a command token with a null-terminated * string without regard to letter case. @@ -4605,67 +5207,6 @@ int readByte(char *buf) return num; } -char *findarg(char *command, int which) -{ - const char comment_mark[] = {"#"}; - int d; - int argc; - int inarg; - int argstart; - static char arg[MAX_ARG_LEN]; - - - // Copy the command line, replacing spaces by zeroes, - // finally returning a pointer to the requested arg. - d = 0; - inarg = 0; - argstart = 0; - argc = -1; - - while(d < MAX_ARG_LEN - 1 && command[d]) - { - // Zero out whitespace - if(command[d] == ' ' || command[d] == '\t') - { - arg[d] = 0; - inarg = 0; - if(argc == which) - { - return arg + argstart; - } - } - else if(command[d] == 0 || command[d] == '\n' || command[d] == '\r' || - strcmp(command + d, comment_mark) == 0) - { - // End of line - arg[d] = 0; - if(argc == which) - { - return arg + argstart; - } - return arg + d; - } - else - { - if(!inarg) - { - // if(argc==-1 && command[d]=='#') return arg; - inarg = 1; - argstart = d; - argc++; - } - arg[d] = command[d]; - } - ++d; - } - arg[d] = 0; - - return arg; -} - - - - float diff(float a, float b) { if(a < b) @@ -6624,43 +7165,126 @@ static void reset_playable_list(char which) } } -// Specify which Player Models are allowable for selecting -static void load_playable_list(char *buf) -{ - int i, index; - char *value; - s_model *playermodels = NULL; - ArgList arglist; - char argbuf[MAX_ALLOWSELECT_LEN] = ""; +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Grow a normalized command line as sequential arguments arrive. + Capacity follows actual content, so total command size is bound + only by addressable memory while individual arguments retain + their dedicated validation limit. +*/ +static void append_command_argument( + char** command_line, + size_t* length, + size_t* capacity, + const char* value +) { + const size_t value_length = strlen(value); + const size_t separator_length = *length ? 1 : 0; + size_t required_capacity; + size_t expanded_capacity; + + if(*length > SIZE_MAX - separator_length - 1 + || value_length + > SIZE_MAX - *length - separator_length - 1) { + borShutdown(1, "Command line exceeds addressable memory.\n"); + return; + } - ParseArgs(&arglist, buf, argbuf); + required_capacity = + *length + separator_length + value_length + 1; - // avoid to load characters if there isn't an allowselect - if ( stricmp(value = GET_ARG(0), "allowselect") != 0 ) return; + if(required_capacity > *capacity) { + expanded_capacity = *capacity ? *capacity : 64; - reset_playable_list(0); + while(expanded_capacity < required_capacity) { + if(expanded_capacity > SIZE_MAX / 2) { + expanded_capacity = required_capacity; + break; + } - for(i = 0; i < sizeof(argbuf); i++) allowselect_args[i] = ' '; - for(i = 0; i < sizeof(argbuf); i++) - { - if ( argbuf[i] != '\0' ) allowselect_args[i] = argbuf[i]; // store allowselect players for savefile - else allowselect_args[i] = ' '; + expanded_capacity *= 2; + } + + *command_line = realloc(*command_line, expanded_capacity); + *capacity = expanded_capacity; } - allowselect_args[sizeof(argbuf)-1] = '\0'; - for(i = 1; (value = GET_ARG(i))[0]; i++) - { - playermodels = findmodel(value); - //if(playermodels == NULL) borShutdown(1, "Player model '%s' is not loaded.\n", value); - index = get_cached_model_index(playermodels->name); - if(index == -1) - { + if(separator_length) { + (*command_line)[(*length)++] = ' '; + } + + memcpy(*command_line + *length, value, value_length); + *length += value_length; + (*command_line)[*length] = '\0'; +} + +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Read selectable player model names directly from the source + line one item at a time. Apply and retain the complete runtime + list without a whole-line or persistent save-field ceiling. +*/ +static void load_playable_list(const char* command_line) +{ + const char* value; + s_command_argument_reader reader; + s_model* playermodel; + char* stored_arguments = NULL; + size_t stored_length = 0; + size_t stored_capacity = 0; + int index; + + if(!command_line + || !command_argument_reader_initialize( + &reader, + command_line, + 0 + ) + || !command_argument_reader_next(&reader, &value) + || stricmp(value, "allowselect") != 0) { + return; + } + + reset_playable_list(0); + append_command_argument( + &stored_arguments, + &stored_length, + &stored_capacity, + "allowselect" + ); + + while(command_argument_reader_next(&reader, &value)) { + playermodel = findmodel((char*)value); + + if(!playermodel) { + free(stored_arguments); + borShutdown(1, "Player model '%s' is not loaded.\n", value); + return; + } + + index = get_cached_model_index(playermodel->name); + + if(index == -1) { + free(stored_arguments); borShutdown(1, "Player model '%s' is not cached.\n", value); + return; } + model_cache[index].selectable = 1; + append_command_argument( + &stored_arguments, + &stored_length, + &stored_capacity, + value + ); } - return; + free(allowselect_args); + allowselect_args = stored_arguments; } /* @@ -7448,7 +8072,9 @@ int child_spawn_get_color_from_argument(char* filename, char* command, char* val * Read a text argument for child spawn config * flag and output appropriate constant. */ -e_child_spawn_config child_spawn_get_config_bit_from_argument(char* value) +e_child_spawn_config child_spawn_get_config_bit_from_argument( + const char* value +) { e_child_spawn_config result = CHILD_SPAWN_CONFIG_NONE; @@ -7560,14 +8186,18 @@ e_child_spawn_config child_spawn_get_config_bit_from_argument(char* value) * and outputs integer. Accepts existing * argument as a default. */ -e_child_spawn_config child_spawn_get_config_argument(ArgList* arglist, e_child_spawn_config config_current) +e_child_spawn_config child_spawn_get_config_argument( + const char* command_line, + e_child_spawn_config config_current +) { + const char* value; + s_command_argument_reader reader; e_child_spawn_config result = config_current; - int i; - char* value; - for (i = 1; (value = GET_ARGP(i)) && value[0]; i++) - { + command_argument_reader_initialize(&reader, command_line, 1); + + while(command_argument_reader_next(&reader, &value)) { result |= child_spawn_get_config_bit_from_argument(value); } @@ -10778,28 +11408,29 @@ static inline e_damage_recursive_logic recursive_effect_get_mode_flag_from_argum } /* -* Caskey, Damon V. -* 2021-08-24 -* -* Reads text arguments from recursive mode -* command and outputs integer with appropriate -* bits toggled. +- Caskey, Damon V. +- 2026-08-11 +- +- Read recursive damage mode arguments directly from the + source line and combine their corresponding behavior flags. */ -e_damage_recursive_logic recursive_effect_get_mode_setup_from_arg_list(ArgList* arglist) +e_damage_recursive_logic recursive_effect_get_mode_setup_from_command_line( + const char* command_line +) { + const char* value; + s_command_argument_reader reader; e_damage_recursive_logic result = 0; - int i; - char* value; - /* * Read all arguments left to right. We send each arg * to function that interprets the value to get appropriate * bit to toggle. */ - for (i = 1; (value = GET_ARGP(i)) && value[0]; i++) - { + command_argument_reader_initialize(&reader, command_line, 1); + + while(command_argument_reader_next(&reader, &value)) { result |= recursive_effect_get_mode_flag_from_argument(value); } @@ -11445,7 +12076,7 @@ void free_modelcache() } -int get_cached_model_index(char *name) +int get_cached_model_index(const char *name) { int i; for(i = 0; i < models_cached; i++) @@ -13053,21 +13684,21 @@ e_entity_type get_type_from_string(const char* value) } /* -* Caskey, Damon V. -* 2022-06-14 -* -* Get arguments for type and output final -* bitmask so we can have a reusable function. +- Caskey, Damon V. +- 2026-08-11 +- +- Read entity type arguments directly from the source line + and combine their corresponding type flags. */ -e_entity_type get_type_from_arglist(ArgList* arglist) +e_entity_type get_type_from_command_line(const char* command_line) { - int i = 0; - char* value = ""; - + const char* value; + s_command_argument_reader reader; e_entity_type result = TYPE_UNDECLARED; - for (i = 1; (value = GET_ARGP(i)) && value[0]; i++) - { + command_argument_reader_initialize(&reader, command_line, 1); + + while(command_argument_reader_next(&reader, &value)) { result |= get_type_from_string(value); } @@ -13160,7 +13791,7 @@ e_weapon_loss_condition weapon_loss_condition_interpret_from_legacy_weaploss(e_w * Accept string input and return * matching constant. */ -e_weapon_loss_condition get_weapon_loss_from_argument(char* value) +e_weapon_loss_condition get_weapon_loss_from_argument(const char* value) { e_weapon_loss_condition result; @@ -13220,16 +13851,22 @@ e_weapon_loss_condition get_weapon_loss_from_argument(char* value) * Populate weapon loss model property * from text arguments. */ -void lcmHandleCommandWeaponLossCondition(ArgList* arglist, s_model* newchar) +void lcmHandleCommandWeaponLossCondition( + const char* command_line, + s_model* newchar +) { - int i; - char* value; + const char* value; + s_command_argument_reader reader; + newchar->weapon_properties.loss_condition = WEAPON_LOSS_CONDITION_NONE; - for (i = 1; (value = GET_ARGP(i)) && value[0]; i++) - { + command_argument_reader_initialize(&reader, command_line, 1); + + while(command_argument_reader_next(&reader, &value)) { newchar->weapon_properties.loss_condition |= get_weapon_loss_from_argument(value); } + } /* @@ -13240,7 +13877,11 @@ void lcmHandleCommandWeaponLossCondition(ArgList* arglist, s_model* newchar) * and output appropriate constant. If input * is legacy integer, we just pass it on. */ -e_model_copy get_model_flag_from_argument(char* filename, char* command, char* value) +e_model_copy get_model_flag_from_argument( + const char* filename, + const char* command, + const char* value +) { e_model_copy result = MODEL_COPY_FLAG_NONE; @@ -13312,16 +13953,24 @@ e_model_copy get_model_flag_from_legacy_int(int legacy_int) * Populate model flag property * from text arguments. */ -void lcmHandleCommandModelFlag(char* filename, char* command, ArgList* arglist, s_model* newchar) +void lcmHandleCommandModelFlag( + char* filename, + char* command, + const char* command_line, + s_model* newchar +) { - int i; - char* value; + const char* value; + s_command_argument_reader reader; + newchar->model_flag = MODEL_COPY_FLAG_NONE; - for (i = 1; (value = GET_ARGP(i)) && value[0]; i++) - { + command_argument_reader_initialize(&reader, command_line, 1); + + while(command_argument_reader_next(&reader, &value)) { newchar->model_flag |= get_model_flag_from_argument(filename, command, value); } + } /* @@ -13755,16 +14404,22 @@ e_air_control find_air_control_from_string(const char* value) * Populate air control model property * from text arguments. */ -void lcmHandleCommandAirControl(const ArgList* arglist, s_model* newchar) +void lcmHandleCommandAirControl( + const char* command_line, + s_model* newchar +) { - int i; - char* value; + const char* value; + s_command_argument_reader reader; + newchar->air_control = AIR_CONTROL_NONE; - for (i = 1; (value = GET_ARGP(i)) && value[0]; i++) - { + command_argument_reader_initialize(&reader, command_line, 1); + + while(command_argument_reader_next(&reader, &value)) { newchar->air_control |= find_air_control_from_string(value); } + } /* @@ -13848,22 +14503,23 @@ e_ko_colorset_config komap_type_get_value_from_argument(char* filename, char* co } /* -* Caskey, Damon V. -* 2022-06-14 -* -* Get arguments for move constraint and -* output final bitmask so we can have a -* reusable function. +- Caskey, Damon V. +- 2026-08-11 +- +- Read movement configuration arguments directly from the + source line and combine their corresponding behavior flags. */ -e_move_config_flags get_move_config_flags_from_arguments(ArgList* arglist) +e_move_config_flags get_move_config_flags_from_command_line( + const char* command_line +) { - int i = 0; - char* value = ""; - + const char* value; + s_command_argument_reader reader; e_move_config_flags result = MOVE_CONFIG_NONE; - for (i = 1; (value = GET_ARGP(i)) && value[0]; i++) - { + command_argument_reader_initialize(&reader, command_line, 1); + + while(command_argument_reader_next(&reader, &value)) { result |= find_move_config_flags_from_string(value); } @@ -13930,16 +14586,19 @@ e_cheat_options find_cheat_options_from_string(const char* value) * Populate global config cheats * property from text arguments. */ -void lcmHandleCommandGlobalConfigCheats(ArgList* arglist) +void lcmHandleCommandGlobalConfigCheats(const char* command_line) { - int i; - char* value; + const char* value; + s_command_argument_reader reader; + global_config.cheats = CHEAT_OPTIONS_NONE; - for (i = 1; (value = GET_ARGP(i)) && value[0]; i++) - { + command_argument_reader_initialize(&reader, command_line, 1); + + while(command_argument_reader_next(&reader, &value)) { global_config.cheats |= find_cheat_options_from_string(value); } + } /* @@ -13996,24 +14655,26 @@ e_aimove get_aimove_constant_from_string(const char* value) } /* -* Caskey, Damon V. -* 2022-06-08 -* -* Get arguments for Aimove and output final -* bitmask. Replaces lcmHandleCommandAiMove -* so we can have a reusable function. +- Caskey, Damon V. +- 2026-08-11 +- +- Read AI movement arguments directly from the source line + and combine them with the supplied default behavior flags. */ -e_aimove get_aimove_from_arguments(const ArgList *arglist, e_aimove default_value) +e_aimove get_aimove_from_command_line( + const char* command_line, + e_aimove default_value +) { - int i = 0; - char* value = ""; - + const char* value; + s_command_argument_reader reader; e_aimove result = default_value; - - for (i = 1; (value = GET_ARGP(i)) && value[0]; i++) - { + + command_argument_reader_initialize(&reader, command_line, 1); + + while(command_argument_reader_next(&reader, &value)) { result |= get_aimove_constant_from_string(value); - } + } return result; } @@ -14056,32 +14717,55 @@ void lcmHandleCommandAiattack(ArgList *arglist, s_model *newchar, int *aiattacks }*/ } -void lcmHandleCommandWeapons(ArgList *arglist, s_model *newchar) +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Read weapon model names directly from the source line, + allocate an exact-sized list, and safely replace any list + owned by the model without modifying inherited storage. +*/ +void lcmHandleCommandWeapons( + const char* command_line, + s_model* newchar +) { - int weapon_index = 0; - char *value; - - for(weapon_index = 0; ; weapon_index++) - { - value = GET_ARGP(weapon_index + 1); - if(!value[0]) - { - break; + const char* value; + s_command_argument_reader reader; + s_command_token token; + s_command_token_reader count_reader = { + .cursor = command_line + }; + int* weapon_list; + size_t weapon_count = 0; + size_t weapon_index = 0; + + /* Skip the command name before counting its values. */ + command_token_reader_next(&count_reader, &token); + + while(command_token_reader_next(&count_reader, &token)) { + if(weapon_count == INT_MAX) { + borShutdown( + 1, + "Weapon list exceeds the supported integer count range.\n" + ); } + + weapon_count++; } - if(!weapon_index) - { + if(!weapon_count) { return; } - newchar->weapon_properties.weapon_count = weapon_index; + if(weapon_count > SIZE_MAX / sizeof(*weapon_list)) { + borShutdown(1, E_OUT_OF_MEMORY); + } - if(!newchar->weapon_properties.weapon_list) - { - newchar->weapon_properties.weapon_list = malloc(sizeof(*newchar->weapon_properties.weapon_list) * newchar->weapon_properties.weapon_count); - memset(newchar->weapon_properties.weapon_list, 0xFF, sizeof(*newchar->weapon_properties.weapon_list) * newchar->weapon_properties.weapon_count); - newchar->weapon_properties.weapon_state |= WEAPON_STATE_HAS_LIST; + weapon_list = malloc(sizeof(*weapon_list) * weapon_count); + + if(!weapon_list) { + borShutdown(1, E_OUT_OF_MEMORY); } /* @@ -14091,19 +14775,25 @@ void lcmHandleCommandWeapons(ArgList *arglist, s_model *newchar) * a model index to populate with. */ - for(weapon_index = 0; weapon_index < newchar->weapon_properties.weapon_count; weapon_index++) - { - value = GET_ARGP(weapon_index + 1); + command_argument_reader_initialize(&reader, command_line, 1); - if(stricmp(value, "none") != 0) - { - newchar->weapon_properties.weapon_list[weapon_index] = get_cached_model_index(value); - } - else - { - newchar->weapon_properties.weapon_list[weapon_index] = MODEL_INDEX_NONE; - } + while(command_argument_reader_next(&reader, &value)) { + weapon_list[weapon_index] = stricmp(value, "none") != 0 + ? get_cached_model_index(value) + : MODEL_INDEX_NONE; + + weapon_index++; + } + + if(hasFreetype(newchar, MF_WEAPONS) + && newchar->weapon_properties.weapon_list) { + free(newchar->weapon_properties.weapon_list); } + + newchar->weapon_properties.weapon_list = weapon_list; + newchar->weapon_properties.weapon_count = (int)weapon_count; + newchar->weapon_properties.weapon_state |= WEAPON_STATE_HAS_LIST; + newchar->freetypes |= MF_WEAPONS; } //fetch string between next @script and @end_script @@ -14741,7 +15431,6 @@ s_model *load_cached_model(char *name, char *owner, char unload) char* command = NULL; char* value = NULL; char* value2 = NULL; - char* value3 = NULL; char fnbuf[MAX_BUFFER_LEN] = { "" }; char namebuf[MAX_BUFFER_LEN] = { "" }; @@ -14752,8 +15441,8 @@ s_model *load_cached_model(char *name, char *owner, char unload) int ani_id = ANI_NONE; int script_id = -1; int frm_id = -1; + bool at_cmd_mergeable = false; int i = 0; - int j = 0; int tempInt = 0; int framecount = 0; int frameset = 0; @@ -14889,9 +15578,14 @@ s_model *load_cached_model(char *name, char *owner, char unload) ", " }; - const char call_text[] = //begin of function call + const char call_indent_text[] = //begin of function call { - " %s(" + " " + }; + + const char call_open_text[] = + { + "(" }; const char endcall_text[] = //end of function call @@ -15056,7 +15750,7 @@ s_model *load_cached_model(char *name, char *owner, char unload) break; case CMD_MODEL_BLOCK_CONFIG: - newchar->block_config_flags = block_get_config_flags_from_arguments(&arglist); + newchar->block_config_flags = block_get_config_flags_from_command_line(buf + pos); break; case CMD_MODEL_BLOCKBACK: @@ -15218,45 +15912,45 @@ s_model *load_cached_model(char *name, char *owner, char unload) /* Faction set up. */ case CMD_MODEL_FACTION_GROUP_DAMAGE_DIRECT: - newchar->faction.damage_direct = faction_get_flags_from_arglist(&arglist); + newchar->faction.damage_direct = faction_get_flags_from_command_line(buf + pos); break; case CMD_MODEL_FACTION_GROUP_DAMAGE_INDIRECT: - newchar->faction.damage_indirect = faction_get_flags_from_arglist(&arglist); + newchar->faction.damage_indirect = faction_get_flags_from_command_line(buf + pos); break; case CMD_MODEL_FACTION_GROUP_HOSTILE: - newchar->faction.hostile = faction_get_flags_from_arglist(&arglist); + newchar->faction.hostile = faction_get_flags_from_command_line(buf + pos); break; case CMD_MODEL_FACTION_GROUP_MEMBER: - newchar->faction.member = faction_get_flags_from_arglist(&arglist); + newchar->faction.member = faction_get_flags_from_command_line(buf + pos); break; /* Legacy type based faction */ case CMD_MODEL_FACTION_TYPE_HOSTILE: case CMD_MODEL_HOSTILE: - newchar->faction.type_hostile = get_type_from_arglist(&arglist); + newchar->faction.type_hostile = get_type_from_command_line(buf + pos); break; case CMD_MODEL_FACTION_TYPE_DAMAGE_DIRECT: case CMD_MODEL_CANDAMAGE: - newchar->faction.type_damage_direct = get_type_from_arglist(&arglist); + newchar->faction.type_damage_direct = get_type_from_command_line(buf + pos); break; case CMD_MODEL_FACTION_TYPE_DAMAGE_INDIRECT: case CMD_MODEL_PROJECTILEHIT: - newchar->faction.type_damage_indirect = get_type_from_arglist(&arglist); + newchar->faction.type_damage_indirect = get_type_from_command_line(buf + pos); break; case CMD_MODEL_AIMOVE: - newchar->aimove = get_aimove_from_arguments(&arglist, AIMOVE1_NORMAL); + newchar->aimove = get_aimove_from_command_line(buf + pos, AIMOVE1_NORMAL); break; case CMD_MODEL_AIATTACK: lcmHandleCommandAiattack(&arglist, newchar, &aiattackset, filename); break; case CMD_MODEL_MOVE_CONFIG: - newchar->move_config_flags = get_move_config_flags_from_arguments(&arglist); + newchar->move_config_flags = get_move_config_flags_from_command_line(buf + pos); break; case CMD_MODEL_SUBJECT_TO_BASEMAP: @@ -15391,7 +16085,7 @@ s_model *load_cached_model(char *name, char *owner, char unload) case CMD_MODEL_MODELFLAG: // Legacy model copy flag. - lcmHandleCommandModelFlag(filename, command, &arglist, newchar); + lcmHandleCommandModelFlag(filename, command, buf + pos, newchar); break; // weapons @@ -15405,7 +16099,7 @@ s_model *load_cached_model(char *name, char *owner, char unload) case CMD_MODEL_WEAPON_LOSS_CONFIG: - lcmHandleCommandWeaponLossCondition(&arglist, newchar); + lcmHandleCommandWeaponLossCondition(buf + pos, newchar); break; case CMD_MODEL_WEAPON_LOSS_INDEX: @@ -15431,7 +16125,7 @@ s_model *load_cached_model(char *name, char *owner, char unload) } break; case CMD_MODEL_WEAPONS: - lcmHandleCommandWeapons(&arglist, newchar); + lcmHandleCommandWeapons(buf + pos, newchar); break; case CMD_MODEL_SHOOTNUM: newchar->weapon_properties.use_count = GET_INT_ARG(1); @@ -15612,7 +16306,7 @@ s_model *load_cached_model(char *name, char *owner, char unload) case CMD_MODEL_DEATH_CONFIG: - newchar->death_config_flags = death_get_config_flags_from_arguments(&arglist, 1); + newchar->death_config_flags = death_get_config_flags_from_command_line(buf + pos, 1); break; case CMD_MODEL_SPEED: @@ -15672,49 +16366,49 @@ s_model *load_cached_model(char *name, char *owner, char unload) * from 3.0 builds. See function for details. */ - defense_setup_from_arg(filename, command, newchar->defense, &arglist, DEFENSE_PARAMETER_LEGACY); + defense_setup_from_arg(filename, command, buf + pos, newchar->defense, &arglist, DEFENSE_PARAMETER_LEGACY); break; case CMD_MODEL_DEFENSE_BLOCK_DAMAGE_ADJUST: - defense_setup_from_arg(filename, command, newchar->defense, &arglist, DEFENSE_PARAMETER_BLOCK_DAMAGE_ADJUST); + defense_setup_from_arg(filename, command, buf + pos, newchar->defense, &arglist, DEFENSE_PARAMETER_BLOCK_DAMAGE_ADJUST); break; case CMD_MODEL_DEFENSE_BLOCK_DAMAGE_MAX: - defense_setup_from_arg(filename, command, newchar->defense, &arglist, DEFENSE_PARAMETER_BLOCK_DAMAGE_MAX); + defense_setup_from_arg(filename, command, buf + pos, newchar->defense, &arglist, DEFENSE_PARAMETER_BLOCK_DAMAGE_MAX); break; case CMD_MODEL_DEFENSE_BLOCK_DAMAGE_MIN: - defense_setup_from_arg(filename, command, newchar->defense, &arglist, DEFENSE_PARAMETER_BLOCK_DAMAGE_MIN); + defense_setup_from_arg(filename, command, buf + pos, newchar->defense, &arglist, DEFENSE_PARAMETER_BLOCK_DAMAGE_MIN); break; case CMD_MODEL_DEFENSE_BLOCK_POWER: - defense_setup_from_arg(filename, command, newchar->defense, &arglist, DEFENSE_PARAMETER_BLOCK_POWER); + defense_setup_from_arg(filename, command, buf + pos, newchar->defense, &arglist, DEFENSE_PARAMETER_BLOCK_POWER); break; case CMD_MODEL_DEFENSE_BLOCK_RATIO: - defense_setup_from_arg(filename, command, newchar->defense, &arglist, DEFENSE_PARAMETER_BLOCK_RATIO); + defense_setup_from_arg(filename, command, buf + pos, newchar->defense, &arglist, DEFENSE_PARAMETER_BLOCK_RATIO); break; case CMD_MODEL_DEFENSE_BLOCK_THRESHOLD: - defense_setup_from_arg(filename, command, newchar->defense, &arglist, DEFENSE_PARAMETER_BLOCK_THRESHOLD); + defense_setup_from_arg(filename, command, buf + pos, newchar->defense, &arglist, DEFENSE_PARAMETER_BLOCK_THRESHOLD); break; case CMD_MODEL_DEFENSE_BLOCK_TYPE: - defense_setup_from_arg(filename, command, newchar->defense, &arglist, DEFENSE_PARAMETER_BLOCK_TYPE); + defense_setup_from_arg(filename, command, buf + pos, newchar->defense, &arglist, DEFENSE_PARAMETER_BLOCK_TYPE); break; case CMD_MODEL_DEFENSE_DAMAGE_ADJUST: - defense_setup_from_arg(filename, command, newchar->defense, &arglist, DEFENSE_PARAMETER_DAMAGE_ADJUST); + defense_setup_from_arg(filename, command, buf + pos, newchar->defense, &arglist, DEFENSE_PARAMETER_DAMAGE_ADJUST); break; case CMD_MODEL_DEFENSE_DAMAGE_MAX: - defense_setup_from_arg(filename, command, newchar->defense, &arglist, DEFENSE_PARAMETER_DAMAGE_MAX); + defense_setup_from_arg(filename, command, buf + pos, newchar->defense, &arglist, DEFENSE_PARAMETER_DAMAGE_MAX); break; case CMD_MODEL_DEFENSE_DAMAGE_MIN: - defense_setup_from_arg(filename, command, newchar->defense, &arglist, DEFENSE_PARAMETER_DAMAGE_MIN); + defense_setup_from_arg(filename, command, buf + pos, newchar->defense, &arglist, DEFENSE_PARAMETER_DAMAGE_MIN); break; case CMD_MODEL_DEFENSE_DEATH_CONFIG: - defense_setup_from_arg(filename, command, newchar->defense, &arglist, DEFENSE_PARAMETER_DEATH_CONFIG); + defense_setup_from_arg(filename, command, buf + pos, newchar->defense, &arglist, DEFENSE_PARAMETER_DEATH_CONFIG); break; case CMD_MODEL_DEFENSE_FACTOR: - defense_setup_from_arg(filename, command, newchar->defense, &arglist, DEFENSE_PARAMETER_FACTOR); + defense_setup_from_arg(filename, command, buf + pos, newchar->defense, &arglist, DEFENSE_PARAMETER_FACTOR); break; case CMD_MODEL_DEFENSE_KNOCKDOWN: - defense_setup_from_arg(filename, command, newchar->defense, &arglist, DEFENSE_PARAMETER_KNOCKDOWN); + defense_setup_from_arg(filename, command, buf + pos, newchar->defense, &arglist, DEFENSE_PARAMETER_KNOCKDOWN); break; case CMD_MODEL_DEFENSE_PAIN: - defense_setup_from_arg(filename, command, newchar->defense, &arglist, DEFENSE_PARAMETER_PAIN); + defense_setup_from_arg(filename, command, buf + pos, newchar->defense, &arglist, DEFENSE_PARAMETER_PAIN); break; case CMD_MODEL_OFFENSE: offense_setup_from_arg(filename, command, newchar->offense, &arglist, OFFENSE_PARAMETER_LEGACY); @@ -15741,7 +16435,7 @@ s_model *load_cached_model(char *name, char *owner, char unload) case CMD_MODEL_AIR_CONTROL: - lcmHandleCommandAirControl(&arglist, newchar); + lcmHandleCommandAirControl(buf + pos, newchar); break; case CMD_MODEL_JUMPMOVE: @@ -15781,7 +16475,7 @@ s_model *load_cached_model(char *name, char *owner, char unload) break; case CMD_MODEL_SHADOW_CONFIG: - newchar->shadow_config_flags = shadow_get_config_flags_from_arguments(&arglist); + newchar->shadow_config_flags = shadow_get_config_flags_from_command_line(buf + pos); break; case CMD_MODEL_GFXSHADOW: @@ -15891,7 +16585,7 @@ s_model *load_cached_model(char *name, char *owner, char unload) break; case CMD_MODEL_RUN_CONFIG: - newchar->run_config_flags = run_get_config_flags_from_arguments(&arglist, 1); + newchar->run_config_flags = run_get_config_flags_from_command_line(buf + pos, 1); break; case CMD_MODEL_RUNNING: // The speed at which the player runs @@ -16066,7 +16760,7 @@ s_model *load_cached_model(char *name, char *owner, char unload) tempInt = GET_INT_ARG(1); - newchar->pain_config_flags = pain_get_config_flags_from_arguments(&arglist); + newchar->pain_config_flags = pain_get_config_flags_from_command_line(buf + pos); break; @@ -16653,6 +17347,7 @@ s_model *load_cached_model(char *name, char *owner, char unload) newanim->model_index = newchar->index; // Reset vars curframe = 0; + at_cmd_mergeable = false; /* * Caskey, Damon V. @@ -16920,28 +17615,28 @@ s_model *load_cached_model(char *name, char *owner, char unload) break; case CMD_MODEL_CHILD_SPAWN_AIMOVE: - child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->aimove = get_aimove_from_arguments(&arglist, AIMOVE1_NONE); + child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->aimove = get_aimove_from_command_line(buf + pos, AIMOVE1_NONE); break; case CMD_MODEL_CHILD_SPAWN_CANDAMAGE: - child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->candamage = get_type_from_arglist(&arglist); + child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->candamage = get_type_from_command_line(buf + pos); break; case CMD_MODEL_CHILD_SPAWN_COLOR: child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->color = child_spawn_get_color_from_argument(filename, command, GET_ARG(1)); break; case CMD_MODEL_CHILD_SPAWN_CONFIG: - child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->config = child_spawn_get_config_argument(&arglist, 0); + child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->config = child_spawn_get_config_argument(buf + pos, 0); break; case CMD_MODEL_CHILD_SPAWN_DIRECTION_ADJUST: child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->direction_adjust = direction_get_adjustment_from_argument(filename, command, GET_ARG(1)); break; case CMD_MODEL_CHILD_SPAWN_HOSTILE: - child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->hostile = get_type_from_arglist(&arglist); + child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->hostile = get_type_from_command_line(buf + pos); break; case CMD_MODEL_CHILD_SPAWN_MODEL: child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->model_index = get_cached_model_index(GET_ARG(1)); break; case CMD_MODEL_CHILD_SPAWN_MOVE_CONSTRAINT: - child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->move_config_flags = get_move_config_flags_from_arguments(&arglist); + child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->move_config_flags = get_move_config_flags_from_command_line(buf + pos); break; case CMD_MODEL_CHILD_SPAWN_OFFSET_X: child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->position.x = GET_INT_ARG(1); @@ -16953,7 +17648,7 @@ s_model *load_cached_model(char *name, char *owner, char unload) child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->position.z = GET_INT_ARG(1); break; case CMD_MODEL_CHILD_SPAWN_PROJECTILEHIT: - child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->projectilehit = get_type_from_arglist(&arglist); + child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->projectilehit = get_type_from_command_line(buf + pos); break; case CMD_MODEL_CHILD_SPAWN_TAKEDAMAGE: child_spawn_upsert_property(&temp_child_spawn_head, temp_child_spawn_index)->takedamage = takedamage_get_reference_from_argument(GET_ARG(1)); @@ -18159,7 +18854,6 @@ s_model *load_cached_model(char *name, char *owner, char unload) break; case CMD_MODEL_PLATFORM: newchar->hasPlatforms = 1; - //for(i=0;(GET_ARG(i+1)[0]; i++); for(i = 0; i < arglist.count && arglist.args[i] && arglist.args[i][0]; i++); if(i < 8) { @@ -18446,7 +19140,7 @@ s_model *load_cached_model(char *name, char *owner, char unload) * Toggle bits based on items provided in argument list. */ - tempInt = recursive_effect_get_mode_setup_from_arg_list(&arglist); + tempInt = recursive_effect_get_mode_setup_from_command_line(buf + pos); } @@ -19313,6 +20007,9 @@ s_model *load_cached_model(char *name, char *owner, char unload) break; case CMD_MODEL_FRAME: { + s_command_token frame_token; + s_command_token_reader frame_reader; + // Command title for log. Details will be added blow accordingly. //printf("\t\t\tFrame: "); @@ -19327,12 +20024,22 @@ s_model *load_cached_model(char *name, char *owner, char unload) } while(!frameset) { - value3 = findarg(buf + pos + peek, 0); - if(stricmp(value3, "frame") == 0) { - framecount++; + frame_reader.cursor = buf + pos + peek; + + if(command_token_reader_next( + &frame_reader, + &frame_token + )) { + if(command_token_equals(&frame_token, "frame")) { + framecount++; + } + + if(command_token_equals(&frame_token, "anim")) { + frameset = 1; + } } - if((stricmp(value3, "anim") == 0) || (pos + peek >= size)) { + if(pos + peek >= size) { frameset = 1; } @@ -19495,6 +20202,7 @@ s_model *load_cached_model(char *name, char *owner, char unload) temp_child_spawn_index = 0; frm_id = -1; + at_cmd_mergeable = false; } break; case CMD_MODEL_ALPHAMASK: @@ -19669,6 +20377,8 @@ s_model *load_cached_model(char *name, char *owner, char unload) break; case CMD_MODEL_AT_SCRIPT: + at_cmd_mergeable = false; + if(!scriptbuf[0]) // if empty, paste the main function text here { buffer_append(&scriptbuf, pre_text, 0xffffff, &sbsize, &scriptlen); @@ -19707,6 +20417,12 @@ s_model *load_cached_model(char *name, char *owner, char unload) buffer_append(&scriptbuf, sur_text, 0xffffff, &sbsize, &scriptlen);// put back last chars break; case CMD_MODEL_AT_CMD: + { + s_command_token command_token; + s_command_token_reader command_token_reader; + bool command_emitted = false; + bool first_command_argument; + //translate @cmd into script function call if(ani_id < 0) { @@ -19721,15 +20437,20 @@ s_model *load_cached_model(char *name, char *owner, char unload) scriptlen = strlen(scriptbuf); if(script_id != ani_id) // if expression 1 { + at_cmd_mergeable = false; sprintf(namebuf, ifid_text, newanim->index); buffer_append(&scriptbuf, namebuf, 0xffffff, &sbsize, &scriptlen); script_id = ani_id; } - j = 1; - value = GET_ARG(j); scriptbuf[scriptlen - strclen(endifid_text)] = 0; // cut last chars scriptlen = strlen(scriptbuf); - if(value && value[0]) + command_token_reader = (s_command_token_reader){ + .cursor = buf + pos + }; + + /* Skip @cmd, then read the function name. */ + if(command_token_reader_next(&command_token_reader, &command_token) + && command_token_reader_next(&command_token_reader, &command_token)) { /* //no_cmd_compatible will try to optimize if(frame==n) @@ -19757,36 +20478,86 @@ s_model *load_cached_model(char *name, char *owner, char unload) // f(); // } */ - if(!no_cmd_compatible || frm_id != curframe) + /* + - Caskey, Damon V. + - 2026-08-12 + - + - Merge same-frame @cmd calls only when the + previous generated section is an eligible + @cmd block with the exact expected suffix. + Otherwise, open a new frame condition without + removing any existing script text. + */ + const size_t frame_close_length = strclen(endif_text); + const size_t frame_return_length = strclen(endif_return_text); + + bool merge_previous_command = + no_cmd_compatible + && at_cmd_mergeable + && frm_id == curframe; + + if(merge_previous_command) + { + merge_previous_command = buffer_remove_suffix_pair( + scriptbuf, + &scriptlen, + endif_return_text, + frame_return_length, + endif_text, + frame_close_length + ); + } + + if(!merge_previous_command) { sprintf(namebuf, if_text, curframe);//only execute in current frame buffer_append(&scriptbuf, namebuf, 0xffffff, &sbsize, &scriptlen); frm_id = curframe; } - else //no_cmd_compatible==1 - { - scriptbuf[scriptlen - strclen(endif_text)] = 0; // cut last chars - scriptlen = strlen(scriptbuf); - scriptbuf[scriptlen - strclen(endif_return_text)] = 0; // cut last chars - scriptlen = strlen(scriptbuf); - } - sprintf(namebuf, call_text, value); - buffer_append(&scriptbuf, namebuf, 0xffffff, &sbsize, &scriptlen); + buffer_append(&scriptbuf, call_indent_text, 0xffffff, &sbsize, &scriptlen); + buffer_append( + &scriptbuf, + command_token.text, + command_token.length, + &sbsize, + &scriptlen + ); + buffer_append(&scriptbuf, call_open_text, 0xffffff, &sbsize, &scriptlen); - do //argument and comma + first_command_argument = true; + while(command_token_reader_next( + &command_token_reader, + &command_token + )) { - j++; - value = GET_ARG(j); - if(value && value[0]) + if(!first_command_argument) { - if(j != 2) - { - buffer_append(&scriptbuf, comma_text, 0xffffff, &sbsize, &scriptlen); - } - buffer_append(&scriptbuf, value, 0xffffff, &sbsize, &scriptlen); + buffer_append(&scriptbuf, comma_text, 0xffffff, &sbsize, &scriptlen); } + buffer_append( + &scriptbuf, + command_token.text, + command_token.length, + &sbsize, + &scriptlen + ); + first_command_argument = false; } - while(value && value[0]); + + command_emitted = true; + } + + if(command_token_reader.unterminated_quote) + { + snprintf( + alert_buffer, + sizeof(alert_buffer), + "Command '@cmd' has an unterminated %c quote.\n", + command_token_reader.unterminated_quote + ); + + shutdownmessage = alert_buffer; + goto lCleanup; } buffer_append(&scriptbuf, endcall_text, 0xffffff, &sbsize, &scriptlen); @@ -19797,7 +20568,9 @@ s_model *load_cached_model(char *name, char *owner, char unload) buffer_append(&scriptbuf, endif_text, 0xffffff, &sbsize, &scriptlen);//end of if buffer_append(&scriptbuf, endifid_text, 0xffffff, &sbsize, &scriptlen); // put back last chars buffer_append(&scriptbuf, sur_text, 0xffffff, &sbsize, &scriptlen); // put back last chars + at_cmd_mergeable = no_cmd_compatible && command_emitted; break; + } default: if(command && command[0]) { @@ -20948,7 +21721,7 @@ int load_models() break; case CMD_MODELSTXT_GLOBAL_CONFIG_CHEATS: - lcmHandleCommandGlobalConfigCheats(&arglist); + lcmHandleCommandGlobalConfigCheats(buf + pos); break; case CMD_MODELSTXT_GLOBAL_CONFIG_FLASH_LAYER_ADJUST: global_config.flash.layer_adjust = GET_INT_ARG(1); @@ -22504,9 +23277,25 @@ void load_levelorder() free(buf); } - if(!savelevel) + if(!savelevel || savelevel_count != (size_t)num_difficulties) { - savelevel = calloc(num_difficulties, sizeof(*savelevel)); + clear_saved_allowselect_arguments(); + free(savelevel_allowselect_args); + free(savelevel); + + savelevel_count = (size_t)num_difficulties; + savelevel = calloc(savelevel_count, sizeof(*savelevel)); + savelevel_allowselect_args = calloc( + savelevel_count, + sizeof(*savelevel_allowselect_args) + ); + } + else if(!savelevel_allowselect_args) + { + savelevel_allowselect_args = calloc( + savelevel_count, + sizeof(*savelevel_allowselect_args) + ); } if(errormessage) @@ -23728,37 +24517,37 @@ void load_level(char *filename) case CMD_LEVEL_FACTION_GROUP_DAMAGE_DIRECT: - next.faction.damage_direct = faction_get_flags_from_arglist(&arglist); + next.faction.damage_direct = faction_get_flags_from_command_line(buf + pos); break; case CMD_LEVEL_FACTION_GROUP_DAMAGE_INDIRECT: - next.faction.damage_indirect = faction_get_flags_from_arglist(&arglist); + next.faction.damage_indirect = faction_get_flags_from_command_line(buf + pos); break; case CMD_LEVEL_FACTION_GROUP_HOSTILE: - next.faction.hostile = faction_get_flags_from_arglist(&arglist); + next.faction.hostile = faction_get_flags_from_command_line(buf + pos); break; case CMD_LEVEL_FACTION_GROUP_MEMBER: - next.faction.member = faction_get_flags_from_arglist(&arglist); + next.faction.member = faction_get_flags_from_command_line(buf + pos); break; case CMD_LEVEL_FACTION_TYPE_DAMAGE_DIRECT: - next.faction.type_damage_direct = get_type_from_arglist(&arglist); + next.faction.type_damage_direct = get_type_from_command_line(buf + pos); break; case CMD_LEVEL_FACTION_TYPE_DAMAGE_INDIRECT: - next.faction.type_damage_indirect = get_type_from_arglist(&arglist); + next.faction.type_damage_indirect = get_type_from_command_line(buf + pos); break; case CMD_LEVEL_FACTION_TYPE_HOSTILE: - next.faction.type_hostile = get_type_from_arglist(&arglist); + next.faction.type_hostile = get_type_from_command_line(buf + pos); break; case CMD_LEVEL_FLIP: @@ -29136,21 +29925,23 @@ e_pain_config_flags pain_get_config_flag_from_string(const char* value) } /* -* Caskey, Damon V. -* 2023-04-10 -* -* Get arguments and output final -* bitmask. +- Caskey, Damon V. +- 2026-08-11 +- +- Read pain configuration arguments directly from the source + line and combine their corresponding behavior flags. */ -e_pain_config_flags pain_get_config_flags_from_arguments(const ArgList* arglist) +e_pain_config_flags pain_get_config_flags_from_command_line( + const char* command_line +) { - int i = 0; - char* value = ""; - + const char* value; + s_command_argument_reader reader; e_pain_config_flags result = PAIN_CONFIG_NONE; - for (i = 1; (value = GET_ARGP(i)) && value[0]; i++) - { + command_argument_reader_initialize(&reader, command_line, 1); + + while(command_argument_reader_next(&reader, &value)) { result |= pain_get_config_flag_from_string(value); } @@ -29201,21 +29992,23 @@ e_block_config_flags block_get_config_flag_from_string(const char* value) } /* -* Caskey, Damon V. -* 2023-04-05 -* -* Get arguments and output final -* bitmask. +- Caskey, Damon V. +- 2026-08-11 +- +- Read blocking configuration arguments directly from the + source line and combine their corresponding behavior flags. */ -e_block_config_flags block_get_config_flags_from_arguments(const ArgList * arglist) +e_block_config_flags block_get_config_flags_from_command_line( + const char* command_line +) { - int i = 0; - char* value = ""; - + const char* value; + s_command_argument_reader reader; e_block_config_flags result = BLOCK_CONFIG_NONE; - for (i = 1; (value = GET_ARGP(i)) && value[0]; i++) - { + command_argument_reader_initialize(&reader, command_line, 1); + + while(command_argument_reader_next(&reader, &value)) { result |= block_get_config_flag_from_string(value); } @@ -36018,21 +36811,28 @@ e_death_config_flags death_get_config_flag_from_string(const char* value) } /* -* Caskey, Damon V. -* 2023-03-20 -* -* Get arguments to output final -* bitmask. +- Caskey, Damon V. +- 2026-08-11 +- +- Read death configuration arguments directly from the source + line, beginning at the requested item, and combine their flags. */ -e_death_config_flags death_get_config_flags_from_arguments(const ArgList* arglist, int start_position) +e_death_config_flags death_get_config_flags_from_command_line( + const char* command_line, + const size_t start_position +) { - int i = 0; - char* value = ""; - + const char* value; + s_command_argument_reader reader; e_death_config_flags result = DEATH_CONFIG_NONE; - for (i = start_position; (value = GET_ARGP(i)) && value[0]; i++) - { + command_argument_reader_initialize( + &reader, + command_line, + start_position + ); + + while(command_argument_reader_next(&reader, &value)) { result |= death_get_config_flag_from_string(value); } @@ -36232,21 +37032,28 @@ e_run_config_flags run_get_config_flag_from_string(const char* value) } /* -* Caskey, Damon V. -* 2023-04-26 -* -* Get arguments to output final -* bitmask. +- Caskey, Damon V. +- 2026-08-11 +- +- Read running configuration arguments directly from the source + line, beginning at the requested item, and combine their flags. */ -e_run_config_flags run_get_config_flags_from_arguments(const ArgList* arglist, const uint64_t start_position) +e_run_config_flags run_get_config_flags_from_command_line( + const char* command_line, + const size_t start_position +) { - int i = 0; - char* value = ""; - + const char* value; + s_command_argument_reader reader; e_run_config_flags result = RUN_CONFIG_NONE; - for (i = start_position; (value = GET_ARGP(i)) && value[0]; i++) - { + command_argument_reader_initialize( + &reader, + command_line, + start_position + ); + + while(command_argument_reader_next(&reader, &value)) { result |= run_get_config_flag_from_string(value); } @@ -36300,21 +37107,23 @@ e_shadow_config_flags shadow_get_config_flag_from_string(const char* value) } /* -* Caskey, Damon V. -* 2023-03-20 -* -* Get arguments foroutput final -* bitmask. +- Caskey, Damon V. +- 2026-08-11 +- +- Read shadow configuration arguments directly from the source + line and combine their corresponding behavior flags. */ -e_shadow_config_flags shadow_get_config_flags_from_arguments(const ArgList* arglist) +e_shadow_config_flags shadow_get_config_flags_from_command_line( + const char* command_line +) { - int i = 0; - char* value = ""; - + const char* value; + s_command_argument_reader reader; e_shadow_config_flags result = SHADOW_CONFIG_NONE; - for (i = 1; (value = GET_ARGP(i)) && value[0]; i++) - { + command_argument_reader_initialize(&reader, command_line, 1); + + while(command_argument_reader_next(&reader, &value)) { result |= shadow_get_config_flag_from_string(value); } @@ -36560,7 +37369,14 @@ s_defense* defense_allocate_object(void) { * Applies value to an attack type element * of defense. */ -void defense_apply_setup_to_property(char* filename, char* command, s_defense* defense, ArgList* arglist, e_defense_parameters target_parameter) +void defense_apply_setup_to_property( + char* filename, + char* command, + const char* command_line, + s_defense* defense, + ArgList* arglist, + e_defense_parameters target_parameter +) { //printf("\n\n defense_apply_setup_to_property(%s, %s, %p, %p, %d)", filename, command, defense, arglist, target_parameter); //printf("\n\t GET_FLOAT_ARGP(2): %f", GET_FLOAT_ARGP(2)); @@ -36629,7 +37445,7 @@ void defense_apply_setup_to_property(char* filename, char* command, s_defense* d break; case DEFENSE_PARAMETER_DEATH_CONFIG: - defense->death_config_flags = death_get_config_flags_from_arguments(arglist, 2); + defense->death_config_flags = death_get_config_flags_from_command_line(command_line, 2); break; case DEFENSE_PARAMETER_FACTOR: @@ -37212,6 +38028,7 @@ int get_attack_type_from_string(const char* value, const char* filename) void defense_setup_from_arg( char* filename, char* command, + const char* command_line, s_defense* target_defense, ArgList* arglist, e_defense_parameters target_parameter @@ -37230,6 +38047,7 @@ void defense_setup_from_arg( defense_apply_setup_to_property( filename, command, + command_line, &target_defense[attack_type_map[i].attack_type], arglist, target_parameter @@ -37248,6 +38066,7 @@ void defense_setup_from_arg( defense_apply_setup_to_property( filename, command, + command_line, &target_defense[tempInt + STA_ATKS - 1], arglist, target_parameter @@ -37268,6 +38087,7 @@ void defense_setup_from_arg( defense_apply_setup_to_property( filename, command, + command_line, &target_defense[i], arglist, target_parameter @@ -46859,21 +47679,23 @@ faction_group_mask_t faction_get_flag_from_string(const char* value) { } /* -* Caskey, Damon V. -* 2022-05-24 -* -* Populate faction property from -* text arguments. +- Caskey, Damon V. +- 2026-08-11 +- +- Read faction arguments directly from the source line and + combine their corresponding group flags. */ -faction_group_mask_t faction_get_flags_from_arglist(const ArgList* arglist) +faction_group_mask_t faction_get_flags_from_command_line( + const char* command_line +) { - int i = 0; - char* value = ""; - + const char* value; + s_command_argument_reader reader; faction_group_mask_t result = FACTION_GROUP_NONE; - for (i = 1; (value = GET_ARGP(i)) && value[0]; i++) - { + command_argument_reader_initialize(&reader, command_line, 1); + + while(command_argument_reader_next(&reader, &value)) { result |= faction_get_flag_from_string(value); } @@ -49744,7 +50566,14 @@ void draw_textobjs() { if(textobj->text) { - font_printf(textobj->position.x, textobj->position.y, textobj->font, textobj->position.z, "%s", textobj->text); + font_print_length( + textobj->position.x, + textobj->position.y, + textobj->font, + textobj->position.z, + textobj->text, + strlen(textobj->text) + ); } } } @@ -50506,10 +51335,10 @@ void display_credits() } -void borShutdown(int status, char *msg, ...) +void borShutdown(int status, const char *msg, ...) { - char buf[1024] = ""; va_list arglist; + va_list output_arguments; int i; static int shuttingdown = 0; @@ -50520,13 +51349,10 @@ void borShutdown(int status, char *msg, ...) } shuttingdown = 1; + va_start(arglist, msg); //printf("savedata.logo %d\n", savedata.logo); - va_start(arglist, msg); - vsprintf(buf, msg, arglist); - va_end(arglist); - if(!disablelog) { switch(status) @@ -50543,7 +51369,9 @@ void borShutdown(int status, char *msg, ...) if(!disablelog) { - printf("%s", buf); + va_copy(output_arguments, arglist); + writeToLogFileV(msg, output_arguments); + va_end(output_arguments); } @@ -50737,10 +51565,17 @@ void borShutdown(int status, char *msg, ...) } freeModelList(); + clear_saved_allowselect_arguments(); + free(savelevel_allowselect_args); + savelevel_allowselect_args = NULL; if(savelevel) { free(savelevel); + savelevel = NULL; } + savelevel_count = 0; + free(allowselect_args); + allowselect_args = NULL; freefilenamecache(); ob_termtrans(); @@ -50754,9 +51589,12 @@ void borShutdown(int status, char *msg, ...) if(!disablelog) { - printf("%s", buf); + va_copy(output_arguments, arglist); + writeToLogFileV(msg, output_arguments); + va_end(output_arguments); } + va_end(arglist); shuttingdown = 0; borExit(status); } @@ -51497,8 +52335,7 @@ void savelevelinfo() save->stage = current_stage; save->which_set = current_set; strncpy(save->dName, set->name, MAX_NAME_LEN - 1); - for(i = 0; i < sizeof(allowselect_args); i++) save->allowSelectArgs[i] = '\0'; // clear - for(i = 0; i < sizeof(allowselect_args); i++) save->allowSelectArgs[i] = allowselect_args[i]; + set_saved_allowselect_arguments(current_set, allowselect_args); } void tryvictorypose(entity *ent) @@ -51816,8 +52653,9 @@ int selectplayer(int *players, char *filename, int useSavedGame) // Allow select? 'a' is the first char of allowselect, // if there's 'a' then there is allowselect. - if (allowselect_args[0] != 'a' - && allowselect_args[0] != 'A') + if (!allowselect_args + || (allowselect_args[0] != 'a' + && allowselect_args[0] != 'A')) { reset_playable_list(1); } @@ -51831,7 +52669,9 @@ int selectplayer(int *players, char *filename, int useSavedGame) if (save->selectFlag) { load_select_screen_info(save); - load_playable_list(save->allowSelectArgs); + load_playable_list( + get_saved_allowselect_arguments(current_set) + ); saved_select_screen = 1; } } @@ -51873,7 +52713,10 @@ int selectplayer(int *players, char *filename, int useSavedGame) else if (stricmp(command, "allowselect") == 0) { load_playable_list(buf + pos); - memcpy(&save->allowSelectArgs, &allowselect_args, sizeof(allowselect_args)); // SAVE + set_saved_allowselect_arguments( + current_set, + allowselect_args + ); } else if (stricmp(command, "background") == 0) { @@ -52367,7 +53210,9 @@ void playgame(int *players, unsigned which_set, int useSavedGame) } credits = save->credits; } - load_playable_list(save->allowSelectArgs); //TODO: change sav format to support dynamic allowselect list. + load_playable_list( + get_saved_allowselect_arguments(current_set) + ); //reset_playable_list(1); // add this because there's no select screen, temporary solution } diff --git a/engine/openbor.h b/engine/openbor.h index f1a3f5bd9..030563a35 100644 --- a/engine/openbor.h +++ b/engine/openbor.h @@ -74,7 +74,7 @@ typedef uint64_t key_mask_t; "Special thanks to SEGA and SNK.\n\n" #define COMPATIBLEVERSION 0x00033749 -#define CV_SAVED_GAME 0x00033747 +#define CV_SAVED_GAME 0x00033748 #define CV_HIGH_SCORE 0x00033747 #define GAME_SPEED_DEFAULT 200 #define THINK_SPEED 2 @@ -123,7 +123,6 @@ typedef uint64_t key_mask_t; #define MAX_COLLISIONS 2 // Collision boxes. #define MAX_RECURSIVE_EFFECTS 64 // Max number of recursive effects on an entity at a time. #define MAX_ARG_LEN 512 -#define MAX_ALLOWSELECT_LEN 1024 #define MAX_SELECT_LOADS 512 #define MAX_PAL_SIZE 1024 #define MAX_CACHED_BACKGROUNDS 9 @@ -4353,13 +4352,13 @@ void unfrozen(entity *e); /* Defense. */ int calculate_force_damage(entity* target, entity* attacker, s_attack* attack_object, const s_defense* defense_object, const bool blocked); s_defense* defense_allocate_object(void); -void defense_apply_setup_to_property(char* filename, char* command, s_defense* defense, ArgList* arglist, e_defense_parameters target_parameter); +void defense_apply_setup_to_property(char* filename, char* command, const char* command_line, s_defense* defense, ArgList* arglist, e_defense_parameters target_parameter); void defense_dump_object(const s_defense* target); void defense_free_object(s_defense* target); const s_defense* defense_find_current_object(const entity* ent, const s_body* body_object, const attack_type_t attack_type); int64_t defense_result_damage(const s_defense* defense_object, int64_t attack_force, bool blocked); int defense_result_pain(s_attack* attack_object, const s_defense* defense_object); -void defense_setup_from_arg(char* filename, char* command, s_defense* defense, ArgList* arglist, e_defense_parameters target_parameter); +void defense_setup_from_arg(char* filename, char* command, const char* command_line, s_defense* defense, ArgList* arglist, e_defense_parameters target_parameter); s_offense* offense_allocate_object(void); void offense_free_object(s_offense* target); @@ -4372,11 +4371,11 @@ s_recursive_effect* recursive_effect_allocate_object(void); void recursive_effect_check_apply(entity* ent, entity* other, s_attack* attack); void recursive_effect_dump_object(s_recursive_effect* recursive); void recursive_effect_free_object(s_recursive_effect* target); -e_damage_recursive_logic recursive_effect_get_mode_setup_from_arg_list(ArgList* arglist); +e_damage_recursive_logic recursive_effect_get_mode_setup_from_command_line(const char* command_line); e_damage_recursive_logic recursive_effect_get_mode_setup_from_legacy_argument(e_damage_recursive_cmd_read value); /* Blocking logic. */ -e_block_config_flags block_get_config_flags_from_arguments(const ArgList* arglist); +e_block_config_flags block_get_config_flags_from_command_line(const char* command_line); e_block_config_flags block_get_config_flag_from_string(const char* value); bool check_blocking_decision(entity *ent); bool check_blocking_eligible(entity *ent, entity *other, s_attack *attack, s_body* body, e_block_state_flags block_state); @@ -4411,6 +4410,32 @@ int prevcolourmapn (s_model *model, int map_index, int player_index); int buffer_pakfile (const char *filename, char **pbuffer, size_t *psize); size_t ParseArgs (ArgList *list, char *input, char *output); + +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Describe one sequential command argument as a source view + and its decoded length. Quote delimiters remain in the source + view so callers can scan without allocation, then discard the + delimiters while copying into correctly sized owned storage. +*/ +typedef enum e_command_argument_read_result +{ + COMMAND_ARGUMENT_READ_END, + COMMAND_ARGUMENT_READ_SUCCESS, + COMMAND_ARGUMENT_READ_INVALID +} e_command_argument_read_result; + +typedef struct s_command_argument_view +{ + const char* source; + size_t source_length; + size_t length; +} s_command_argument_view; + +e_command_argument_read_result command_argument_read(const char* command_line, size_t argument_index, s_command_argument_view* argument); +bool command_argument_copy (const s_command_argument_view* argument, char* destination, size_t capacity); int getsyspropertybyindex (ScriptVariant *var, int index); int changesyspropertybyindex (int index, ScriptVariant *value); e_direction direction_get_direction_from_argument(const char* filename, const char* command, const char* value); @@ -4460,7 +4485,6 @@ int loadHighScoreFile(void); int translate_SDID(char *value); int music(char *filename, int loop, long offset); int readByte(char* buf); -char *findarg(char *command, int which); float diff(float a, float b); int inair(entity *e); int inair_range(entity *e); @@ -4542,7 +4566,7 @@ void faction_copy_data(s_faction* dest, s_faction* source); bool faction_check_can_damage(entity* acting_entity, entity* target_entity, const bool indirect); int faction_check_is_hostile(entity* acting_entity, entity* target_entity); int faction_check_player_verses(entity* acting_entity, entity* target_entity, faction_group_mask_t faction_property); -faction_group_mask_t faction_get_flags_from_arglist(const ArgList* arglist); +faction_group_mask_t faction_get_flags_from_command_line(const char* command_line); faction_group_mask_t faction_get_flag_from_string(const char* value); /* Bind control */ @@ -4559,8 +4583,8 @@ s_child_follow* child_follow_allocate_object(); /* Child spawn control */ int child_spawn_get_color_from_argument(char* filename, char* command, char* value); -e_child_spawn_config child_spawn_get_config_argument(ArgList* arglist, e_child_spawn_config config_current); -e_child_spawn_config child_spawn_get_config_bit_from_argument(char* value); +e_child_spawn_config child_spawn_get_config_argument(const char* command_line, e_child_spawn_config config_current); +e_child_spawn_config child_spawn_get_config_bit_from_argument(const char* value); s_child_spawn* child_spawn_allocate_object(); s_child_spawn* child_spawn_append_node(struct s_child_spawn* head); @@ -4659,7 +4683,7 @@ e_falldie_config death_config_get_falldie_from_value(e_death_config_flags acting e_death_config_flags death_config_get_value_from_falldie(e_death_config_flags current_value, e_falldie_config acting_value); e_death_config_flags death_config_get_value_from_nodieblink(e_death_config_flags current_value, e_nodieblink_config acting_value); e_nodieblink_config death_config_get_nodieblink_from_value(e_death_config_flags acting_value); -e_death_config_flags death_get_config_flags_from_arguments(const ArgList* arglist, int start_position); +e_death_config_flags death_get_config_flags_from_command_line(const char* command_line, size_t start_position); e_death_config_flags death_get_config_flag_from_string(const char* value); typedef enum e_death_sequence_acting_event @@ -4672,7 +4696,7 @@ int death_try_sequence_damage(entity* acting_entity, e_death_config_flags death_ /* Running */ e_run_config_flags run_get_config_flag_from_string(const char* value); -e_run_config_flags run_get_config_flags_from_arguments(const ArgList* arglist, const uint64_t start_position); +e_run_config_flags run_get_config_flags_from_command_line(const char* command_line, size_t start_position); void run_try_runstop_player(entity* acting_entity, const s_player* acting_player); void run_try_runstop_check(entity* acting_entity, const e_RunXDirection movex, const e_RunZDirection movez, const e_RunXDirection running_x, const e_RunZDirection running_z, const int runConfigFlags, const int dashCommandFlag, const int dashFixedFlag, const int enabledFlag, const int stopStateFlag); @@ -4682,29 +4706,29 @@ e_shadow_config_flags shadow_get_config_from_legacy_aironly(e_shadow_config_flag e_shadow_config_flags shadow_get_config_from_legacy_gfxshadow(e_shadow_config_flags shadow_config_flags, int legacy_value); e_shadow_config_flags shadow_get_config_from_legacy_shadowbase(e_shadow_config_flags shadow_config_flags, e_shadowbase_config legacy_value); e_shadow_config_flags shadow_get_config_flag_from_string(const char* value); -e_shadow_config_flags shadow_get_config_flags_from_arguments(const ArgList* arglist); +e_shadow_config_flags shadow_get_config_flags_from_command_line(const char* command_line); // Meta data control. void meta_data_free_list(s_meta_data* head); /* Model flag control. */ e_model_copy get_model_flag_from_legacy_int(int legacy_int); -e_model_copy get_model_flag_from_argument(char* filename, char* command, char* value); -void lcmHandleCommandModelFlag(char* filename, char* command, ArgList* arglist, s_model* newchar); +e_model_copy get_model_flag_from_argument(const char* filename, const char* command, const char* value); +void lcmHandleCommandModelFlag(char* filename, char* command, const char* command_line, s_model* newchar); /* Pain and fall (model) */ -e_pain_config_flags pain_get_config_flags_from_arguments(const ArgList* arglist); +e_pain_config_flags pain_get_config_flags_from_command_line(const char* command_line); e_pain_config_flags pain_get_config_flag_from_string(const char* value); /* Weapon loss control */ -e_weapon_loss_condition get_weapon_loss_from_argument(char* value); -void lcmHandleCommandWeaponLossCondition(ArgList* arglist, s_model* newchar); +e_weapon_loss_condition get_weapon_loss_from_argument(const char* value); +void lcmHandleCommandWeaponLossCondition(const char* command_line, s_model* newchar); int play_hit_impact_sound(s_attack* attack_object, entity* attacking_entity, int attack_blocked); void cache_model(char *name, char *path, int flag); void free_modelcache(); -int get_cached_model_index(char *name); +int get_cached_model_index(const char *name); char *get_cached_model_path(char *name); s_model *load_cached_model(char *name, char *owner, char unload); int is_set(s_model *model, int m); @@ -4969,7 +4993,7 @@ int ai_check_grab(); int ai_check_escape(); int ai_check_busy(); void display_credits(void); -void borShutdown(int status, char *msg, ...); +void borShutdown(int status, const char *msg, ...); void startup(void); int playgif(char *filename, int x, int y, int noskip); void playscene(char *filename); @@ -5036,7 +5060,6 @@ typedef struct int pColourmap[MAX_PLAYERS]; // colour map int selectFlag; // saved a select.txt infos - char allowSelectArgs[MAX_ALLOWSELECT_LEN]; // allowselect arguments char selectMusic[MAX_ARG_LEN]; // select music arguments char selectBackground[MAX_ARG_LEN]; // select background arguments char selectLoad[MAX_SELECT_LOADS][MAX_ARG_LEN]; // select load arguments diff --git a/engine/openborscript.c b/engine/openborscript.c index 46bd94523..7a1163aee 100644 --- a/engine/openborscript.c +++ b/engine/openborscript.c @@ -34,6 +34,8 @@ #include "ImportCache.h" #include "models.h" #include "scriptcommon.h" +#include +#include Varlist global_var_list; Script *pcurrentscript = NULL; //used by local script functions @@ -1339,8 +1341,9 @@ HRESULT openbor_setsystemvariant(ScriptVariant **varlist , ScriptVariant **pretv //drawstring(x, y, font, string, z); HRESULT openbor_drawstring(ScriptVariant **varlist , ScriptVariant **pretvar, int paramCount) { + ScriptVariantStringView string_view; + char conversion_buffer[SCRIPT_VARIANT_CONVERSION_BUFFER_LENGTH]; int i; - char buf[MAX_BUFFER_LEN]; LONG value[4]; *pretvar = NULL; @@ -1367,8 +1370,24 @@ HRESULT openbor_drawstring(ScriptVariant **varlist , ScriptVariant **pretvar, in { value[3] = 0; } - ScriptVariant_ToString(varlist[3], buf); - font_printf((int)value[0], (int)value[1], (int)value[2], (int)value[3], "%s", buf); + if(FAILED(ScriptVariant_GetStringView( + varlist[3], + conversion_buffer, + sizeof(conversion_buffer), + &string_view + ))) + { + goto drawstring_error; + } + + font_print_length( + (int)value[0], + (int)value[1], + (int)value[2], + (int)value[3], + string_view.string, + string_view.length + ); return S_OK; drawstring_error: @@ -1380,9 +1399,10 @@ HRESULT openbor_drawstring(ScriptVariant **varlist , ScriptVariant **pretvar, in //drawstringtoscreen(screen, x, y, font, string); HRESULT openbor_drawstringtoscreen(ScriptVariant **varlist , ScriptVariant **pretvar, int paramCount) { + ScriptVariantStringView string_view; + char conversion_buffer[SCRIPT_VARIANT_CONVERSION_BUFFER_LENGTH]; int i; s_screen *scr; - char buf[MAX_BUFFER_LEN]; LONG value[3]; *pretvar = NULL; @@ -1409,8 +1429,24 @@ HRESULT openbor_drawstringtoscreen(ScriptVariant **varlist , ScriptVariant **pre } } - ScriptVariant_ToString(varlist[4], buf); - screen_printf(scr, (int)value[0], (int)value[1], (int)value[2], "%s", buf); + if(FAILED(ScriptVariant_GetStringView( + varlist[4], + conversion_buffer, + sizeof(conversion_buffer), + &string_view + ))) + { + goto drawstring_error; + } + + screen_print_length( + scr, + (int)value[0], + (int)value[1], + (int)value[2], + string_view.string, + string_view.length + ); return S_OK; drawstring_error: @@ -1422,7 +1458,8 @@ HRESULT openbor_drawstringtoscreen(ScriptVariant **varlist , ScriptVariant **pre //log(string); HRESULT openbor_log(ScriptVariant **varlist , ScriptVariant **pretvar, int paramCount) { - char buf[MAX_BUFFER_LEN]; + ScriptVariantStringView string_view; + char conversion_buffer[SCRIPT_VARIANT_CONVERSION_BUFFER_LENGTH]; *pretvar = NULL; if(paramCount != 1) @@ -1430,8 +1467,17 @@ HRESULT openbor_log(ScriptVariant **varlist , ScriptVariant **pretvar, int param goto drawstring_error; } - ScriptVariant_ToString(varlist[0], buf); - printf("%s", buf); + if(FAILED(ScriptVariant_GetStringView( + varlist[0], + conversion_buffer, + sizeof(conversion_buffer), + &string_view + ))) + { + goto drawstring_error; + } + + writeToLogFileLength(string_view.string, string_view.length); return S_OK; drawstring_error: @@ -8121,12 +8167,13 @@ HRESULT openbor_getplayerproperty(ScriptVariant **varlist , ScriptVariant **pret //changeplayerproperty(index, propname, value[, value2, value3,...]); HRESULT openbor_changeplayerproperty(ScriptVariant **varlist , ScriptVariant **pretvar, int paramCount) { + ScriptVariantStringView string_view; + char conversion_buffer[SCRIPT_VARIANT_CONVERSION_BUFFER_LENGTH]; LONG ltemp, ltemp2; int index; entity *ent = NULL; int prop = -1; char *tempstr = NULL; - static char buffer[64]; ScriptVariant *arg = NULL; *pretvar = NULL; @@ -8427,8 +8474,23 @@ HRESULT openbor_changeplayerproperty(ScriptVariant **varlist , ScriptVariant **p return S_OK; cpperror: - ScriptVariant_ToString(arg, buffer); - printf("Function changeplayerproperty receives an invalid value: %s.\n", buffer); + if(SUCCEEDED(ScriptVariant_GetStringView( + arg, + conversion_buffer, + sizeof(conversion_buffer), + &string_view + ))) + { + printf( + "Function changeplayerproperty receives an invalid value: %.*s.\n", + (int)string_view.length, + string_view.string + ); + } + else + { + printf("Function changeplayerproperty receives an invalid or oversized value.\n"); + } return E_FAIL; } @@ -10608,7 +10670,7 @@ HRESULT openbor_openfilestream(ScriptVariant **varlist , ScriptVariant **pretvar } HRESULT openbor_getfilestreamline(ScriptVariant **varlist , ScriptVariant **pretvar, int paramCount) { - int length; + size_t length; char *buf; char *dst; ScriptVariant *arg = NULL; @@ -10632,7 +10694,16 @@ HRESULT openbor_getfilestreamline(ScriptVariant **varlist , ScriptVariant **pret ++length; } - (*pretvar)->strVal = StrCache_Pop(length); + if(length > MAX_SCRIPT_STRING_LENGTH) { + ScriptVariant_Clear(*pretvar); + printf( + "File stream line exceeds the maximum script string length of %u characters.\n", + MAX_SCRIPT_STRING_LENGTH + ); + return E_FAIL; + } + + (*pretvar)->strVal = StrCache_Pop((int)length); dst = StrCache_Get((*pretvar)->strVal); memcpy(dst, buf, length); dst[length] = '\0'; @@ -10640,9 +10711,22 @@ HRESULT openbor_getfilestreamline(ScriptVariant **varlist , ScriptVariant **pret return S_OK; } +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Read one requested file-stream argument sequentially and + copy only its decoded value into exact-sized script storage. + Opening and closing quote delimiters are discarded. Numeric + conversions reuse the same temporary script string. +*/ HRESULT openbor_getfilestreamargument(ScriptVariant **varlist , ScriptVariant **pretvar, int paramCount) { + e_command_argument_read_result argument_result = COMMAND_ARGUMENT_READ_END; + s_command_argument_view argument_view = {0}; + char *converted_text; ScriptVariant *arg = NULL; LONG filestreamindex, argument; + size_t argument_length = 0; char *argtype = NULL; if(paramCount < 3) { @@ -10669,27 +10753,73 @@ HRESULT openbor_getfilestreamargument(ScriptVariant **varlist , ScriptVariant ** argtype = (char *)StrCache_Get(varlist[2]->strVal); - if(stricmp(argtype, "string") == 0) { - ScriptVariant_ChangeType(*pretvar, VT_STR); - (*pretvar)->strVal = StrCache_CreateNewFrom(findarg(filestreams[filestreamindex].buf + filestreams[filestreamindex].pos, argument)); - - } else if(stricmp(argtype, "int") == 0) { - ScriptVariant_ChangeType(*pretvar, VT_INTEGER); - (*pretvar)->lVal = (LONG)atoi(findarg(filestreams[filestreamindex].buf + filestreams[filestreamindex].pos, argument)); - - } else if(stricmp(argtype, "float") == 0) { - ScriptVariant_ChangeType(*pretvar, VT_DECIMAL); - (*pretvar)->dblVal = (DOUBLE)atof(findarg(filestreams[filestreamindex].buf + filestreams[filestreamindex].pos, argument)); - - } else if(stricmp(argtype, "byte") == 0) { + if(stricmp(argtype, "byte") == 0) { ScriptVariant_ChangeType(*pretvar, VT_INTEGER); (*pretvar)->lVal = (LONG)(readByte(filestreams[filestreamindex].buf + filestreams[filestreamindex].pos)); - - } else { + + return S_OK; + } + + if(stricmp(argtype, "string") != 0 + && stricmp(argtype, "int") != 0 + && stricmp(argtype, "float") != 0) { printf("Invalid type for argument converted to (getfilestreamargument).\n"); return E_FAIL; } + if(argument >= 0) { + argument_result = command_argument_read( + filestreams[filestreamindex].buf + + filestreams[filestreamindex].pos, + (size_t)argument, + &argument_view + ); + + if(argument_result == COMMAND_ARGUMENT_READ_INVALID) { + printf("File stream argument contains an unterminated quote.\n"); + return E_FAIL; + } + + argument_length = argument_view.length; + } + + if(argument_length > MAX_SCRIPT_STRING_LENGTH) { + printf( + "File stream argument exceeds the maximum script string length of %u characters.\n", + MAX_SCRIPT_STRING_LENGTH + ); + return E_FAIL; + } + + ScriptVariant_ChangeType(*pretvar, VT_STR); + (*pretvar)->strVal = StrCache_Pop((int)argument_length); + converted_text = StrCache_Get((*pretvar)->strVal); + + if(argument_result == COMMAND_ARGUMENT_READ_SUCCESS) { + if(!command_argument_copy( + &argument_view, + converted_text, + argument_length + 1 + )) { + ScriptVariant_Clear(*pretvar); + return E_FAIL; + } + } else { + converted_text[0] = '\0'; + } + + if(stricmp(argtype, "int") == 0) { + const LONG converted_integer = (LONG)atoi(converted_text); + + ScriptVariant_ChangeType(*pretvar, VT_INTEGER); + (*pretvar)->lVal = converted_integer; + } else if(stricmp(argtype, "float") == 0) { + const DOUBLE converted_decimal = (DOUBLE)atof(converted_text); + + ScriptVariant_ChangeType(*pretvar, VT_DECIMAL); + (*pretvar)->dblVal = converted_decimal; + } + return S_OK; } @@ -10769,12 +10899,13 @@ HRESULT openbor_setfilestreamposition(ScriptVariant **varlist , ScriptVariant ** } HRESULT openbor_filestreamappend(ScriptVariant **varlist , ScriptVariant **pretvar, int paramCount) { + ScriptVariantStringView append_view; + char conversion_buffer[SCRIPT_VARIANT_CONVERSION_BUFFER_LENGTH]; LONG filestreamindex; ScriptVariant *arg = NULL; LONG appendtype = -1; - size_t len1, len2; + size_t len1, len2, output_length; char *temp; - static char append[2048]; *pretvar = NULL; if(paramCount < 2) { @@ -10828,30 +10959,60 @@ HRESULT openbor_filestreamappend(ScriptVariant **varlist , ScriptVariant **pretv } } else { - - ScriptVariant_ToString(arg, append); - len1 = strlen(append); + if(FAILED(ScriptVariant_GetStringView( + arg, + conversion_buffer, + sizeof(conversion_buffer), + &append_view + ))) { + goto append_error; + } + + len1 = append_view.length; len2 = filestreams[filestreamindex].size; - filestreams[filestreamindex].buf = realloc(filestreams[filestreamindex].buf, sizeof(*temp) * (len1 + len2 + 4)); + if(len2 > SIZE_MAX - 4 || len1 > SIZE_MAX - len2 - 4) { + goto append_error; + } + + output_length = len2 + len1; if(appendtype == 0) { - append[len1] = ' '; - append[++len1] = '\0'; - strcpy(filestreams[filestreamindex].buf + len2, "\r\n"); - len2 += 2; - strcpy(filestreams[filestreamindex].buf + len2, append); - + output_length += 3; } else if(appendtype == 1) { - append[len1] = ' '; - append[++len1] = '\0'; - strcpy(filestreams[filestreamindex].buf + len2, append); - + output_length += 1; + } + + temp = realloc( + filestreams[filestreamindex].buf, + sizeof(*temp) * (output_length + 1) + ); + + if(!temp) { + goto append_error; + } + + filestreams[filestreamindex].buf = temp; + temp += len2; + + if(appendtype == 0) { + *temp++ = '\r'; + *temp++ = '\n'; + memcpy(temp, append_view.string, len1); + temp += len1; + *temp++ = ' '; + } else if(appendtype == 1) { + memcpy(temp, append_view.string, len1); + temp += len1; + *temp++ = ' '; } else { - strcpy(filestreams[filestreamindex].buf + len2, append); + memcpy(temp, append_view.string, len1); + temp += len1; } - filestreams[filestreamindex].size = len1 + len2; + + *temp = '\0'; + filestreams[filestreamindex].size = output_length; } return S_OK; @@ -13040,7 +13201,11 @@ HRESULT openbor_gettextobjproperty(ScriptVariant **varlist , ScriptVariant **pre case _top_text: { ScriptVariant_ChangeType(*pretvar, VT_STR); - (*pretvar)->strVal = StrCache_CreateNewFrom(level->textobjs[ind].text); + (*pretvar)->strVal = StrCache_CreateNewFrom( + level->textobjs[ind].text + ? level->textobjs[ind].text + : "" + ); break; } case _top_time: @@ -13081,11 +13246,55 @@ HRESULT openbor_gettextobjproperty(ScriptVariant **varlist , ScriptVariant **pre return E_FAIL; } +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Replace a text object's owned string with exact-size storage. + Conversion uses the common script string policy and the old + value remains intact if conversion or allocation fails. +*/ +static HRESULT openbor_textobj_set_text( + s_textobj *textobj, + const ScriptVariant *value +) +{ + ScriptVariantStringView string_view; + char conversion_buffer[SCRIPT_VARIANT_CONVERSION_BUFFER_LENGTH]; + char *text; + + if(!textobj || FAILED(ScriptVariant_GetStringView( + value, + conversion_buffer, + sizeof(conversion_buffer), + &string_view + ))) + { + return E_FAIL; + } + + text = malloc(string_view.length + 1); + + if(!text) + { + return E_FAIL; + } + + memcpy(text, string_view.string, string_view.length); + text[string_view.length] = '\0'; + + free(textobj->text); + textobj->text = text; + + return S_OK; +} + HRESULT openbor_changetextobjproperty(ScriptVariant **varlist , ScriptVariant **pretvar, int paramCount) { + ScriptVariantStringView string_view; + char conversion_buffer[SCRIPT_VARIANT_CONVERSION_BUFFER_LENGTH]; LONG ind; int propind; - static char buf[MAX_STR_VAR_LEN]; LONG ltemp; const char *ctotext = "changetextobjproperty(int index, \"property\", value)"; @@ -13140,9 +13349,13 @@ HRESULT openbor_changetextobjproperty(ScriptVariant **varlist , ScriptVariant ** } case _top_text: { - ScriptVariant_ToString(varlist[2], buf); - level->textobjs[ind].text = malloc(MAX_STR_VAR_LEN); - strncpy(level->textobjs[ind].text, buf, MAX_STR_VAR_LEN); + if(FAILED(openbor_textobj_set_text( + &level->textobjs[ind], + varlist[2] + ))) + { + goto changetextobjproperty_error; + } break; } case _top_time: @@ -13203,8 +13416,23 @@ HRESULT openbor_changetextobjproperty(ScriptVariant **varlist , ScriptVariant ** return S_OK; changetextobjproperty_error: - ScriptVariant_ToString(varlist[2], buf); - printf("Invalid textobj value: %s\n", buf); + if(SUCCEEDED(ScriptVariant_GetStringView( + varlist[2], + conversion_buffer, + sizeof(conversion_buffer), + &string_view + ))) + { + printf( + "Invalid textobj value: %.*s\n", + (int)string_view.length, + string_view.string + ); + } + else + { + printf("Invalid or oversized textobj value.\n"); + } return E_FAIL; } @@ -13213,7 +13441,6 @@ HRESULT openbor_settextobj(ScriptVariant **varlist , ScriptVariant **pretvar, in { LONG ind; LONG X, Y, Z, F, T = 0; - static char buf[MAX_STR_VAR_LEN]; const char *stotext = "settextobj(int index, int x, int y, int font, int z, char text, int time {optional})"; *pretvar = NULL; @@ -13258,24 +13485,25 @@ HRESULT openbor_settextobj(ScriptVariant **varlist , ScriptVariant **pretvar, in { goto settextobj_error; } - ScriptVariant_ToString(varlist[5], buf); if(paramCount >= 7 && FAILED(ScriptVariant_IntegerValue(varlist[6], &T))) { goto settextobj_error; } + if(FAILED(openbor_textobj_set_text( + &level->textobjs[ind], + varlist[5] + ))) + { + goto settextobj_error; + } + level->textobjs[ind].time = (int)T; level->textobjs[ind].position.x = (int)X; level->textobjs[ind].position.y = (int)Y; level->textobjs[ind].position.z = (int)Z; level->textobjs[ind].font = (int)F; - if(!level->textobjs[ind].text) - { - level->textobjs[ind].text = (char *)malloc(MAX_STR_VAR_LEN); - } - strncpy(level->textobjs[ind].text, buf, MAX_STR_VAR_LEN); - return S_OK; settextobj_error: @@ -13899,7 +14127,13 @@ HRESULT openbor_shutdown(ScriptVariant **varlist , ScriptVariant **pretvar, int goto shutdown_error; } - borShutdown((LONG)ltemp, paramCount > 1 ? StrCache_Get(varlist[1]->strVal) : (DEFAULT_SHUTDOWN_MESSAGE)); + borShutdown( + (LONG)ltemp, + "%s", + paramCount > 1 + ? StrCache_Get(varlist[1]->strVal) + : DEFAULT_SHUTDOWN_MESSAGE + ); return S_OK; shutdown_error: diff --git a/engine/source/gamelib/font.c b/engine/source/gamelib/font.c index 3c5397f63..082d8eaf0 100644 --- a/engine/source/gamelib/font.c +++ b/engine/source/gamelib/font.c @@ -23,6 +23,94 @@ s_font **fonts[MAX_FONTS]; static char b[1024]; +#define FONT_FORMAT_STACK_LENGTH 1024 + +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Format font text into caller-provided stack storage when it + fits, or allocate exact spillover storage when it does not. + The returned text remains valid until the caller frees it if + the returned pointer differs from the stack buffer. +*/ +static char *font_format_text( + char *stack_buffer, + size_t stack_capacity, + const char *format, + va_list arguments, + size_t *output_length +) +{ + va_list arguments_copy; + char *result; + int required_length; + + if(!stack_buffer || !stack_capacity || !format || !output_length) + { + return NULL; + } + + *output_length = 0; + + va_copy(arguments_copy, arguments); + required_length = vsnprintf( + stack_buffer, + stack_capacity, + format, + arguments_copy + ); + va_end(arguments_copy); + + if(required_length < 0) + { + stack_buffer[0] = '\0'; + return NULL; + } + + *output_length = (size_t)required_length; + + if(*output_length < stack_capacity) + { + return stack_buffer; + } + + if(*output_length == SIZE_MAX) + { + stack_buffer[0] = '\0'; + *output_length = 0; + return NULL; + } + + result = malloc(*output_length + 1); + + if(!result) + { + stack_buffer[0] = '\0'; + *output_length = 0; + return NULL; + } + + va_copy(arguments_copy, arguments); + required_length = vsnprintf( + result, + *output_length + 1, + format, + arguments_copy + ); + va_end(arguments_copy); + + if(required_length < 0 || (size_t)required_length != *output_length) + { + free(result); + stack_buffer[0] = '\0'; + *output_length = 0; + return NULL; + } + + return result; +} + void _font_unload(s_font *font) { int i; @@ -357,48 +445,52 @@ int fontheight(int which) } -int font_string_width(int which, char *format, ...) +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Measure a length-delimited text value directly, without + treating its contents as a format string or staging it in a + fixed-capacity buffer. +*/ +int font_string_width_length(int which, const char *text, size_t length) { int w = 0; - char *buf = b, c; - va_list arglist; + char c; s_font **sets, *font; int mbs, index; + size_t position = 0; which %= MAX_FONTS; sets = fonts[which]; - if(!sets || !format) + if(!sets || !text) { return 0; } mbs = sets[0]->mbs; - va_start(arglist, format); - vsprintf(buf, format, arglist); - va_end(arglist); - if(!mbs) { font = sets[0]; if(font) - while(*buf) + while(position < length && text[position]) { - w += font->token_width[((int)(*buf)) & 0xFF]; - buf++; + w += font->token_width[((int)text[position]) & 0xFF]; + position++; } } else { - while((c = *buf)) + while(position < length && (c = text[position])) { - if((c & 0x80) && buf[1]) + if((c & 0x80) && position + 1 < length && text[position + 1]) { index = (unsigned char)c; - buf++; + position++; } else { @@ -409,14 +501,47 @@ int font_string_width(int which, char *format, ...) if(font) { - w += font->token_width[((int)(*buf)) & 0xFF]; + w += font->token_width[((int)text[position]) & 0xFF]; } - buf++; + position++; } } return w; } +int font_string_width(int which, const char *format, ...) +{ + char stack_buffer[FONT_FORMAT_STACK_LENGTH]; + char *text; + int result; + size_t text_length; + va_list arguments; + + va_start(arguments, format); + text = font_format_text( + stack_buffer, + sizeof(stack_buffer), + format, + arguments, + &text_length + ); + va_end(arguments); + + if(!text) + { + return 0; + } + + result = font_string_width_length(which, text, text_length); + + if(text != stack_buffer) + { + free(text); + } + + return result; +} + // Caskey, Damon V. // 2016-11-21 @@ -446,7 +571,9 @@ int font_string_width_max(char **strings, int elements, int font) // be the width of longest string. for (i = 0; i < elements; i++) { - width_temp = font_string_width(font, strings[i]); + width_temp = strings[i] + ? font_string_width_length(font, strings[i], strlen(strings[i])) + : 0; if (width_temp > result) { @@ -457,37 +584,48 @@ int font_string_width_max(char **strings, int elements, int font) return result; } -void font_printf(int x, int y, int which, int layeroffset, char *format, ...) +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Queue a length-delimited text value directly as font sprites. + Newlines advance to the next font row and multibyte font + selection retains the legacy two-byte behavior. +*/ +void font_print_length( + int x, + int y, + int which, + int layeroffset, + const char *text, + size_t length +) { - char *buf = b, c; - va_list arglist; + char c; int ox = x; s_font **sets, *font; int mbs, index, w, lf; + size_t position = 0; which %= MAX_FONTS; sets = fonts[which]; - if(!sets) + if(!sets || !text) { return; } mbs = sets[0]->mbs; - va_start(arglist, format); - vsprintf(buf, format, arglist); - va_end(arglist); - - while((c = *buf)) + while(position < length && (c = text[position])) { lf = (c == '\n'); - if(mbs && (c & 0x80) && buf[1]) + if(mbs && (c & 0x80) && position + 1 < length && text[position + 1]) { index = (unsigned char)c; - buf++; + position++; } else { @@ -505,48 +643,88 @@ void font_printf(int x, int y, int which, int layeroffset, char *format, ...) } else { - w = font->token_width[((int)(*buf)) & 0xFF]; - spriteq_add_frame(x, y, FONT_LAYER + layeroffset, font->token[((int)(*buf)) & 0xFF], NULL, 0); + w = font->token_width[((int)text[position]) & 0xFF]; + spriteq_add_frame(x, y, FONT_LAYER + layeroffset, font->token[((int)text[position]) & 0xFF], NULL, 0); x += w; } } - buf++; + position++; + } +} + +void font_printf(int x, int y, int which, int layeroffset, const char *format, ...) +{ + char stack_buffer[FONT_FORMAT_STACK_LENGTH]; + char *text; + size_t text_length; + va_list arguments; + + va_start(arguments, format); + text = font_format_text( + stack_buffer, + sizeof(stack_buffer), + format, + arguments, + &text_length + ); + va_end(arguments); + + if(!text) + { + return; + } + + font_print_length(x, y, which, layeroffset, text, text_length); + + if(text != stack_buffer) + { + free(text); } } // Print to a screen rather than queueing the sprites -void screen_printf(s_screen *screen, int x, int y, int which, char *format, ...) +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Draw length-delimited font text directly to a target screen, + bypassing format parsing and fixed-capacity staging. +*/ +void screen_print_length( + s_screen *screen, + int x, + int y, + int which, + const char *text, + size_t length +) { - char *buf = b, c; - va_list arglist; + char c; int ox = x; s_font **sets, *font; int mbs, index, w, lf; + size_t position = 0; which %= MAX_FONTS; sets = fonts[which]; - if(!sets) + if(!sets || !text) { return; } mbs = sets[0]->mbs; - va_start(arglist, format); - vsprintf(buf, format, arglist); - va_end(arglist); - - while((c = *buf)) + while(position < length && (c = text[position])) { lf = (c == '\n'); - if(mbs && (c & 0x80) && buf[1]) + if(mbs && (c & 0x80) && position + 1 < length && text[position + 1]) { index = (unsigned char)c; - buf++; + position++; } else { @@ -564,14 +742,42 @@ void screen_printf(s_screen *screen, int x, int y, int which, char *format, ...) } else { - w = font->token_width[((int)(*buf)) & 0xFF]; - putsprite(x, y, font->token[((int)(*buf)) & 0xFF], screen, NULL); + w = font->token_width[((int)text[position]) & 0xFF]; + putsprite(x, y, font->token[((int)text[position]) & 0xFF], screen, NULL); x += w; } } - buf++; + position++; } } +void screen_printf(s_screen *screen, int x, int y, int which, const char *format, ...) +{ + char stack_buffer[FONT_FORMAT_STACK_LENGTH]; + char *text; + size_t text_length; + va_list arguments; + + va_start(arguments, format); + text = font_format_text( + stack_buffer, + sizeof(stack_buffer), + format, + arguments, + &text_length + ); + va_end(arguments); + + if(!text) + { + return; + } + screen_print_length(screen, x, y, which, text, text_length); + + if(text != stack_buffer) + { + free(text); + } +} diff --git a/engine/source/gamelib/font.h b/engine/source/gamelib/font.h index dc947296e..3a7f9bc1b 100644 --- a/engine/source/gamelib/font.h +++ b/engine/source/gamelib/font.h @@ -30,10 +30,13 @@ typedef struct void font_unload(int which); int font_load(int which, char *filename, char *packfile, int flags); int font_loadmask(int which, char *filename, char *packfile, int flags); -int font_string_width(int which, char *buf, ...); +int font_string_width_length(int which, const char *text, size_t length); +int font_string_width(int which, const char *format, ...); int font_string_width_max(char **strings, int elements, int font); -void font_printf(int x, int y, int which, int layeroffset, char *format, ...); -void screen_printf(s_screen *screen, int x, int y, int which, char *format, ...); +void font_print_length(int x, int y, int which, int layeroffset, const char *text, size_t length); +void screen_print_length(s_screen *screen, int x, int y, int which, const char *text, size_t length); +void font_printf(int x, int y, int which, int layeroffset, const char *format, ...); +void screen_printf(s_screen *screen, int x, int y, int which, const char *format, ...); int fontmonowidth(int which); int fontheight(int which); #endif diff --git a/engine/source/openborscript/constants.c b/engine/source/openborscript/constants.c index 57858f183..34a00d87c 100644 --- a/engine/source/openborscript/constants.c +++ b/engine/source/openborscript/constants.c @@ -1389,6 +1389,7 @@ bool mapstrings_transconst(ScriptVariant **varlist, int paramCount) ICMPCONST(MAX_INT) ICMPCONST(MAX_NAME_LEN) ICMPCONST(MAX_PLAYERS) + ICMPCONST(MAX_SCRIPT_STRING_LENGTH) ICMPCONST(MAX_SPECIALS) ICMPCONST(MIN_INT) @@ -1816,7 +1817,8 @@ bool mapstrings_transconst(ScriptVariant **varlist, int paramCount) //openborconstant(constname); //translate a constant by string, used to retrieve a constant or macro of openbor HRESULT openbor_transconst(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount) { - static char buf[128]; + ScriptVariantStringView string_view; + char conversion_buffer[SCRIPT_VARIANT_CONVERSION_BUFFER_LENGTH]; if (paramCount < 1) { *pretvar = NULL; @@ -1831,8 +1833,20 @@ HRESULT openbor_transconst(ScriptVariant **varlist, ScriptVariant **pretvar, int return S_OK; } - ScriptVariant_ToString(varlist[0], buf); - printf("Can't translate constant %s\n", buf); + if(SUCCEEDED(ScriptVariant_GetStringView( + varlist[0], + conversion_buffer, + sizeof(conversion_buffer), + &string_view + ))) { + printf( + "Can't translate constant %.*s\n", + (int)string_view.length, + string_view.string + ); + } else { + printf("Can't translate invalid or oversized constant.\n"); + } *pretvar = NULL; return E_FAIL; diff --git a/engine/source/openborscript/level.c b/engine/source/openborscript/level.c index 861a86ff6..c4eb29089 100644 --- a/engine/source/openborscript/level.c +++ b/engine/source/openborscript/level.c @@ -2101,9 +2101,10 @@ HRESULT openbor_getlevelproperty(ScriptVariant **varlist , ScriptVariant **pretv //changelevelproperty(name, value) HRESULT openbor_changelevelproperty(ScriptVariant **varlist , ScriptVariant **pretvar, int paramCount) { + ScriptVariantStringView string_view; + char conversion_buffer[SCRIPT_VARIANT_CONVERSION_BUFFER_LENGTH]; LONG ltemp, ltemp1; DOUBLE dbltemp, dbltemp2, dbltemp3; - static char buf[64]; int i; ScriptVariant *arg = NULL; @@ -2482,11 +2483,25 @@ HRESULT openbor_changelevelproperty(ScriptVariant **varlist , ScriptVariant **pr printf("Dumping values: "); for(i = 1; i < paramCount; i++) { - ScriptVariant_ToString(varlist[i], buf); - printf("%s, ", buf); + if(SUCCEEDED(ScriptVariant_GetStringView( + varlist[i], + conversion_buffer, + sizeof(conversion_buffer), + &string_view + ))) + { + printf( + "%.*s, ", + (int)string_view.length, + string_view.string + ); + } + else + { + printf(", "); + } } printf("\n"); return E_FAIL; } - diff --git a/engine/source/openborscript/string.c b/engine/source/openborscript/string.c index 34c03080a..b52327c9d 100644 --- a/engine/source/openborscript/string.c +++ b/engine/source/openborscript/string.c @@ -135,12 +135,23 @@ HRESULT openbor_strlength(ScriptVariant **varlist , ScriptVariant **pretvar, int //strwidth(char string, int font); HRESULT openbor_strwidth(ScriptVariant **varlist , ScriptVariant **pretvar, int paramCount) { + ScriptVariantStringView string_view; LONG ltemp; if(paramCount >= 2 && varlist[0]->vt == VT_STR && - SUCCEEDED(ScriptVariant_IntegerValue(varlist[1], <emp))) + SUCCEEDED(ScriptVariant_IntegerValue(varlist[1], <emp)) && + SUCCEEDED(ScriptVariant_GetStringView( + varlist[0], + NULL, + 0, + &string_view + ))) { ScriptVariant_ChangeType(*pretvar, VT_INTEGER); - (*pretvar)->lVal = font_string_width((int)ltemp, (char*)StrCache_Get(varlist[0]->strVal)); + (*pretvar)->lVal = font_string_width_length( + (int)ltemp, + string_view.string, + string_view.length + ); return S_OK; } @@ -183,4 +194,3 @@ HRESULT openbor_strright(ScriptVariant **varlist , ScriptVariant **pretvar, int *pretvar = NULL; return E_FAIL; } - diff --git a/engine/source/preprocessorlib/pp_lexer.c b/engine/source/preprocessorlib/pp_lexer.c index f7e3c3638..c39a2c2e9 100644 --- a/engine/source/preprocessorlib/pp_lexer.c +++ b/engine/source/preprocessorlib/pp_lexer.c @@ -58,6 +58,8 @@ void pp_token_Init(pp_token *ptoken, PP_TOKEN_TYPE theType, LPCSTR theSource, TEXTPOS theTextPosition, ULONG charOffset) { ptoken->theType = theType; + ptoken->theStringLiteralSource = NULL; + ptoken->theStringLiteralLength = 0; ptoken->theTextPosition = theTextPosition; ptoken->charOffset = charOffset; strcpy(ptoken->theSource, theSource ); @@ -679,29 +681,59 @@ HRESULT pp_lexer_GetTokenNumber(pp_lexer *plexer, pp_token *theNextToken) ******************************************************************************/ HRESULT pp_lexer_GetTokenStringLiteral(pp_lexer *plexer, pp_token *theNextToken) { - //copy the source that makes up this token - //an identifier is a string of letters, digits and/or underscores - //consume that first quote mark - int esc = 0; - CONSUMECHARACTER; - while ( strncmp( plexer->pcurChar, "\"", 1)) + const char *literal_start = plexer->pcurChar; + size_t literal_length; + size_t preview_length; + + /* + * String literals remain views into the source text. This + * lets the lexer scan them sequentially without copying the + * complete literal into the fixed identifier token buffer. + */ + SKIPCHARACTER; + + while(*plexer->pcurChar && *plexer->pcurChar != '"') { - if(!strncmp( plexer->pcurChar, "\\", 1)) - { - esc = 1; - } - CONSUMECHARACTER; - if(esc) + if(*plexer->pcurChar == '\\') { - CONSUMECHARACTER; - esc = 0; + SKIPCHARACTER; + + if(!*plexer->pcurChar) + { + return E_FAIL; + } } + + SKIPCHARACTER; + } + + if(*plexer->pcurChar != '"') + { + return E_FAIL; } - //consume that last quote mark - CONSUMECHARACTER; + SKIPCHARACTER; + + literal_length = (size_t)(plexer->pcurChar - literal_start); + preview_length = literal_length < MAX_TOKEN_LENGTH + ? literal_length + : MAX_TOKEN_LENGTH; + + memcpy(plexer->theTokenSource, literal_start, preview_length); + plexer->theTokenSource[preview_length] = '\0'; + plexer->theTokenLen = (ULONG)preview_length; + + pp_token_Init( + theNextToken, + PP_TOKEN_STRING_LITERAL, + plexer->theTokenSource, + plexer->theTokenPosition, + plexer->tokOffset + ); + + theNextToken->theStringLiteralSource = literal_start; + theNextToken->theStringLiteralLength = literal_length; - MAKETOKEN( PP_TOKEN_STRING_LITERAL ); return S_OK; } /****************************************************************************** @@ -1116,4 +1148,3 @@ HRESULT pp_lexer_SkipComment(pp_lexer *plexer, COMMENT_TYPE theType) return S_OK; } - diff --git a/engine/source/preprocessorlib/pp_lexer.h b/engine/source/preprocessorlib/pp_lexer.h index 44c3ee479..5923df745 100644 --- a/engine/source/preprocessorlib/pp_lexer.h +++ b/engine/source/preprocessorlib/pp_lexer.h @@ -73,6 +73,8 @@ typedef struct pp_token { PP_TOKEN_TYPE theType; CHAR theSource[MAX_TOKEN_LENGTH + 1]; + LPCSTR theStringLiteralSource; // Non-owning complete literal view. + size_t theStringLiteralLength; // Length of complete literal view. TEXTPOS theTextPosition; ULONG charOffset; } pp_token; @@ -119,5 +121,3 @@ HRESULT pp_lexer_SkipComment(pp_lexer *lexer, COMMENT_TYPE theType); #endif - - diff --git a/engine/source/preprocessorlib/pp_parser.c b/engine/source/preprocessorlib/pp_parser.c index daab04039..7a5fcaa3b 100644 --- a/engine/source/preprocessorlib/pp_parser.c +++ b/engine/source/preprocessorlib/pp_parser.c @@ -272,6 +272,32 @@ void pp_warning(pp_parser *self, char *format, ...) pp_message(self, "warning", buf); } +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Return a non-owning view of a preprocessor token's complete + source. String literals may exceed fixed identifier storage. +*/ +static const char *pp_token_source_view( + const pp_token *token, + size_t *source_length +) +{ + assert(token); + assert(source_length); + + if(token->theType == PP_TOKEN_STRING_LITERAL + && token->theStringLiteralSource) + { + *source_length = token->theStringLiteralLength; + return token->theStringLiteralSource; + } + + *source_length = strlen(token->theSource); + return token->theSource; +} + /** * Gets the next parsable token from the lexer. * @param skip_whitespace true to ignore whitespace, false otherwise @@ -645,7 +671,7 @@ pp_token *pp_parser_emit_token(pp_parser *self) // self->token contains the first token of the macro/message if self->overread == true HRESULT pp_parser_readline(pp_parser *self, char *buf, int bufsize) { - int total_length = 1; + size_t total_length = 0; if(FAILED(pp_parser_lex_token(self, true))) { @@ -665,15 +691,21 @@ HRESULT pp_parser_readline(pp_parser *self, char *buf, int bufsize) break; } - if((total_length + strlen(self->token.theSource)) > bufsize) + const char *source; + size_t source_length; + + source = pp_token_source_view(&self->token, &source_length); + + if(total_length + source_length + 1 > (size_t)bufsize) { // Prevent buffer overflow pp_error(self, "length of macro or message contents is too long; must be <= %i characters", bufsize); return E_FAIL; } - strcat(buf, self->token.theSource); - total_length += strlen(self->token.theSource); + memcpy(buf + total_length, source, source_length); + total_length += source_length; + buf[total_length] = '\0'; if(FAILED(pp_parser_lex_token(self, false))) { return E_FAIL; @@ -692,6 +724,7 @@ HRESULT pp_parser_stringify(pp_parser *self) char *contents = (char *)List_Retrieve(&self->ctx->macros); pp_parser parser; pp_token *token; + size_t output_length = 1; pp_token_Init(&self->token, PP_TOKEN_STRING_LITERAL, "\"", self->token.theTextPosition, 0); @@ -699,34 +732,55 @@ HRESULT pp_parser_stringify(pp_parser *self) while((token = pp_parser_emit_token(&parser)) && token->theType != PP_TOKEN_EOF) { - char *source = token->theSource; + const char *source; + const char *source_end; + size_t source_length; bool in_string = false; - while(*source) + + source = pp_token_source_view(token, &source_length); + source_end = source + source_length; + + while(source < source_end) { + const char *addition; + size_t addition_length; + if(*source == '"') { - strcat(self->token.theSource, "\\\""); + addition = "\\\""; + addition_length = 2; in_string = !in_string; } else if(*source == '\\' && in_string) { - strcat(self->token.theSource, "\\\\"); + addition = "\\\\"; + addition_length = 2; } else { - strncat(self->token.theSource, source, 1); + addition = source; + addition_length = 1; } - if(strlen(self->token.theSource) + 2 > MAX_TOKEN_LENGTH) + if(output_length + addition_length + 1 > MAX_TOKEN_LENGTH) { return pp_error(self, "sequence is too long to stringify"); } + memcpy( + self->token.theSource + output_length, + addition, + addition_length + ); + output_length += addition_length; + self->token.theSource[output_length] = '\0'; + source++; } } - strcat(self->token.theSource, "\""); + self->token.theSource[output_length++] = '"'; + self->token.theSource[output_length] = '\0'; return S_OK; } @@ -1362,11 +1416,18 @@ HRESULT pp_parser_insert_function_macro(pp_parser *self, char *name) } if(write) { - if((strlen(paramBuffer) + strlen(self->token.theSource) + 1) > sizeof(paramBuffer)) + const char *source; + size_t source_length; + size_t param_length = strlen(paramBuffer); + + source = pp_token_source_view(&self->token, &source_length); + + if(param_length + source_length + 1 > sizeof(paramBuffer)) return pp_error(self, "parameter %d of function '%s' exceeds max length of %d characters", name, sizeof(paramBuffer) - 1); - strcat(paramBuffer, self->token.theSource); + memcpy(paramBuffer + param_length, source, source_length); + paramBuffer[param_length + source_length] = '\0'; } } while(parenLevel >= 0 || type != PP_TOKEN_RPAREN); @@ -1453,4 +1514,3 @@ bool pp_parser_is_defined(pp_parser *self, const char *name) return false; } - diff --git a/engine/source/scriptlib/Instruction.c b/engine/source/scriptlib/Instruction.c index c3a649601..212e31803 100644 --- a/engine/source/scriptlib/Instruction.c +++ b/engine/source/scriptlib/Instruction.c @@ -12,20 +12,42 @@ #include #include -void Instruction_InitViaToken(Instruction *pins, OpCode code, Token *pToken ) +HRESULT Instruction_InitViaToken(Instruction *pins, OpCode code, Token *pToken ) { + if(!pins) + { + return E_FAIL; + } + memset(pins, 0, sizeof(Instruction)); pins->OpCode = code; pins->theToken = malloc(sizeof(Token)); + + if(!pins->theToken) + { + return E_FAIL; + } + memset(pins->theToken, 0, sizeof(Token)); if(pToken) { *(pins->theToken) = *pToken; + + /* + * Materialize string constants while their non-owning + * source view is guaranteed to remain valid. + */ + if(code == CONSTSTR) + { + return Instruction_ConvertConstant(pins); + } } else { pins->theToken->theType = END_OF_TOKENS; } + + return S_OK; } void Instruction_InitViaLabel(Instruction *pins, OpCode code, LPCSTR label ) @@ -422,18 +444,31 @@ static int Instruction_IsHexIntegerSource(const char *source) { } //'compile' constant to improve speed -void Instruction_ConvertConstant(Instruction *pins) { +HRESULT Instruction_ConvertConstant(Instruction *pins) { ScriptVariant *pvar; CHAR *sc; + HRESULT result = S_OK; + + if(!pins) + { + return E_FAIL; + } + if(pins->theVal) { - return; //already have the constant as a variant + return S_OK; //already have the constant as a variant } if( pins->OpCode == CONSTDBL) { pvar = (ScriptVariant *)malloc(sizeof(ScriptVariant)); + + if(!pvar) + { + return E_FAIL; + } + ScriptVariant_Init(pvar); ScriptVariant_ChangeType(pvar, VT_DECIMAL); //Note: There shouldn't be any double constants added via a label, @@ -458,7 +493,7 @@ void Instruction_ConvertConstant(Instruction *pins) { pvar = (ScriptVariant *)malloc(sizeof(ScriptVariant)); if (!pvar) { - return; + return E_FAIL; } ScriptVariant_Init(pvar); @@ -496,14 +531,34 @@ void Instruction_ConvertConstant(Instruction *pins) { } else if(pins->OpCode == CONSTSTR) { pvar = (ScriptVariant *)malloc(sizeof(ScriptVariant)); + + if(!pvar) + { + return E_FAIL; + } + ScriptVariant_Init(pvar); - ScriptVariant_ParseStringConstant(pvar, pins->theToken->theSource); + + if(pins->theToken->theStringLiteralSource) + { + result = ScriptVariant_ParseStringLiteral( + pvar, + pins->theToken->theStringLiteralSource, + pins->theToken->theStringLiteralLength + ); + } + else + { + result = ScriptVariant_ParseStringConstant(pvar, pins->theToken->theSource); + } } else { - return; + return E_FAIL; } pins->theVal = pvar; + + return result; } diff --git a/engine/source/scriptlib/Instruction.h b/engine/source/scriptlib/Instruction.h index 113a9455a..98697f5d5 100644 --- a/engine/source/scriptlib/Instruction.h +++ b/engine/source/scriptlib/Instruction.h @@ -45,13 +45,13 @@ typedef struct Instruction #pragma pack() -void Instruction_InitViaToken(Instruction *pins, OpCode code, Token *pToken ); +HRESULT Instruction_InitViaToken(Instruction *pins, OpCode code, Token *pToken ); void Instruction_InitViaLabel(Instruction *pins, OpCode code, LPCSTR label ); void Instruction_Init(Instruction *pins); void Instruction_Clear(Instruction *pins); void Instruction_NewData(Instruction *pins); -void Instruction_ConvertConstant(Instruction *pins); +HRESULT Instruction_ConvertConstant(Instruction *pins); void Instruction_ToString(Instruction *pins, LPSTR strRep); #endif diff --git a/engine/source/scriptlib/Interpreter.c b/engine/source/scriptlib/Interpreter.c index 605e07d22..533547561 100644 --- a/engine/source/scriptlib/Interpreter.c +++ b/engine/source/scriptlib/Interpreter.c @@ -215,7 +215,8 @@ HRESULT Interpreter_Call(Interpreter *pinterpreter) { ScriptVariant **parameters; ScriptVariant *parameter; ScriptVariant *pretvar; - char buffer[256]; + ScriptVariantStringView string_view; + char conversion_buffer[SCRIPT_VARIANT_CONVERSION_BUFFER_LENGTH]; int i; if(!pinterpreter) { @@ -329,13 +330,29 @@ HRESULT Interpreter_Call(Interpreter *pinterpreter) { continue; } - ScriptVariant_ToString(parameter, buffer); + if(FAILED(ScriptVariant_GetStringView( + parameter, + conversion_buffer, + sizeof(conversion_buffer), + &string_view + ))) { + printf(", "); + continue; + } if(parameter->vt == VT_STR) { - printf("\"%s\", ", buffer); + printf( + "\"%.*s\", ", + (int)string_view.length, + string_view.string + ); } else { - printf("%s, ", buffer); + printf( + "%.*s, ", + (int)string_view.length, + string_view.string + ); } } @@ -601,7 +618,11 @@ HRESULT Interpreter_CompileInstructions(Interpreter *pinterpreter) //Push a constant integer case CONSTINT: //convert to constant first - Instruction_ConvertConstant(pInstruction); + if(FAILED(Instruction_ConvertConstant(pInstruction))) + { + hr = E_FAIL; + break; + } Instruction_NewData(pInstruction); Stack_Push(&(pinterpreter->theDataStack), (void *)pInstruction->theVal); break; @@ -1050,7 +1071,10 @@ HRESULT Interpreter_CompileInstructions(Interpreter *pinterpreter) //number of arguments we have case CHECKARG: //cache the argument count - Instruction_ConvertConstant(pInstruction); + if(FAILED(Instruction_ConvertConstant(pInstruction))) + { + hr = E_FAIL; + } break; //This instructs the interpreter to clean one value off the stack. @@ -1107,7 +1131,9 @@ HRESULT Interpreter_CompileInstructions(Interpreter *pinterpreter) pInstruction = (Instruction *)List_Retrieve(&(pinterpreter->theInstructionList)); if(pInstruction->theVal) { + ScriptVariant_Clear(pInstruction->theVal); free(pInstruction->theVal); + pInstruction->theVal = NULL; } if(pInstruction->theRefList) { @@ -1631,4 +1657,3 @@ void Interpreter_Reset(Interpreter *pinterpreter) pinterpreter->bReset = TRUE; pinterpreter->bCallCompleted = FALSE; } - diff --git a/engine/source/scriptlib/Lexer.c b/engine/source/scriptlib/Lexer.c index c915f5546..e0dfd9d80 100644 --- a/engine/source/scriptlib/Lexer.c +++ b/engine/source/scriptlib/Lexer.c @@ -7,6 +7,7 @@ */ #include "Lexer.h" +#include "ScriptVariant.h" #include #include #include @@ -15,6 +16,8 @@ void Token_Init(Token *ptoken, MY_TOKEN_TYPE theType, LPCSTR theSource, TEXTPOS theTextPosition, ULONG charOffset) { ptoken->theType = theType; + ptoken->theStringLiteralSource = NULL; + ptoken->theStringLiteralLength = 0; ptoken->theTextPosition = theTextPosition; ptoken->charOffset = charOffset; strcpy(ptoken->theSource, theSource ); @@ -25,7 +28,10 @@ HRESULT Token_InitFromPreprocessor(Token *ptoken, pp_token *ppToken) { ptoken->theTextPosition = ppToken->theTextPosition; ptoken->charOffset = ppToken->charOffset; - strncpy(ptoken->theSource, ppToken->theSource, MAX_TOKEN_LENGTH + 1); + ptoken->theStringLiteralSource = NULL; + ptoken->theStringLiteralLength = 0; + strncpy(ptoken->theSource, ppToken->theSource, MAX_TOKEN_LENGTH); + ptoken->theSource[MAX_TOKEN_LENGTH] = '\0'; switch (ppToken->theType) { @@ -55,63 +61,28 @@ HRESULT Token_InitFromPreprocessor(Token *ptoken, pp_token *ppToken) break; case PP_TOKEN_STRING_LITERAL: { - char *src, *dest; + const char *literal_source; + size_t literal_length; // handle escape sequences and convert to correct format ptoken->theType = TOKEN_STRING_LITERAL; - src = ppToken->theSource + 1; // skip first quote mark - dest = ptoken->theSource; - while (*src && *src != '"') - { - if (*src == '\\') - { - switch (*(++src)) - { - case 's': - *dest++ = ' '; - src++; - break; - case 'r': - *dest++ = '\r'; - src++; - break; - case 'n': - *dest++ = '\n'; - src++; - break; - case 't': - *dest++ = '\t'; - src++; - break; - case '0': - *dest++ = '\0'; - src++; - break; - case '\"': - *dest++ = '\"'; - src++; - break; - case '\'': - *dest++ = '\''; - src++; - break; - case '\\': - *dest++ = '\\'; - src++; - break; - default: // invalid escape sequence - // TODO: emit a warning here - *dest++ = '\\'; - *dest++ = *src; - } - } - else - { - *dest++ = *src++; - } - } - *dest = '\0'; + literal_source = ppToken->theStringLiteralSource + ? ppToken->theStringLiteralSource + : ppToken->theSource; + literal_length = ppToken->theStringLiteralSource + ? ppToken->theStringLiteralLength + : strlen(ppToken->theSource); + + ptoken->theStringLiteralSource = literal_source; + ptoken->theStringLiteralLength = literal_length; + + ScriptString_DecodeLiteral( + ptoken->theSource, + sizeof(ptoken->theSource), + literal_source, + literal_length + ); break; } case PP_TOKEN_SIZEOF: @@ -419,4 +390,3 @@ HRESULT Lexer_GetNextToken(Lexer *plexer, Token *theNextToken) return Token_InitFromPreprocessor(theNextToken, ppToken); } - diff --git a/engine/source/scriptlib/Lexer.h b/engine/source/scriptlib/Lexer.h index bbe3e2385..7accc56b3 100644 --- a/engine/source/scriptlib/Lexer.h +++ b/engine/source/scriptlib/Lexer.h @@ -47,6 +47,8 @@ typedef struct Token { MY_TOKEN_TYPE theType; CHAR theSource[MAX_TOKEN_LENGTH + 1]; + LPCSTR theStringLiteralSource; // Non-owning complete literal view. + size_t theStringLiteralLength; // Length of complete literal view. TEXTPOS theTextPosition; ULONG charOffset; } Token; @@ -81,5 +83,3 @@ HRESULT Lexer_SkipComment(Lexer *lexer, COMMENT_TYPE theType); #endif - - diff --git a/engine/source/scriptlib/Parser.c b/engine/source/scriptlib/Parser.c index 04abd573c..e3a08b366 100644 --- a/engine/source/scriptlib/Parser.c +++ b/engine/source/scriptlib/Parser.c @@ -153,10 +153,32 @@ void Parser_ParseExpression(Parser *pparser, List *pIList, LPSTR scriptText, void Parser_AddInstructionViaToken(Parser *pparser, OpCode pCode, Token *pToken, Label label ) { + HRESULT result; Instruction *pInstruction = NULL; pInstruction = (Instruction *)malloc(sizeof(Instruction)); - Instruction_InitViaToken(pInstruction, pCode, pToken); + result = Instruction_InitViaToken(pInstruction, pCode, pToken); List_InsertAfter(pparser->pIList, pInstruction, label); + + if(FAILED(result)) + { + if(pCode == CONSTSTR) + { + pp_error( + &(pparser->theLexer.preprocessor), + "String literal exceeds the maximum length of %u characters", + MAX_SCRIPT_STRING_LENGTH + ); + } + else + { + pp_error( + &(pparser->theLexer.preprocessor), + "Unable to create script instruction" + ); + } + + pparser->errorFound = TRUE; + } } /****************************************************************************** @@ -805,6 +827,10 @@ void Parser_Select_stmt(Parser *pparser ) int opcode = pToken->theType == TOKEN_STRING_LITERAL ? CONSTSTR : CONSTINT; Parser_AddInstructionViaToken(pparser, opcode, pToken, NULL ); Parser_AddInstructionViaLabel(pparser, Branch_EQUAL, List_GetName(&cases), NULL ); + if(pToken->theType == TOKEN_STRING_LITERAL) + { + free((void *)pToken->theStringLiteralSource); + } free(pToken); } else @@ -899,6 +925,20 @@ void Parser_Case_label(Parser *pparser, List *pCases ) } token = malloc(sizeof(Token)); memcpy(token, &pparser->theNextToken, sizeof(Token)); + + if(token->theType == TOKEN_STRING_LITERAL && token->theStringLiteralSource) + { + CHAR *literal_source = malloc(token->theStringLiteralLength + 1); + + memcpy( + literal_source, + token->theStringLiteralSource, + token->theStringLiteralLength + ); + literal_source[token->theStringLiteralLength] = '\0'; + token->theStringLiteralSource = literal_source; + } + List_InsertAfter(pCases, token, label); Parser_Match(pparser); Parser_Check(pparser, TOKEN_COLON ); @@ -1844,9 +1884,39 @@ void Parser_Unary_expr(Parser *pparser ) } else if(pInstruction->OpCode == CONSTSTR) { - //convert to negative constant - sprintf(buf, "!%s", pInstruction->theToken->theSource); - strcpy(pInstruction->theToken->theSource, buf); + /* + * String constants are materialized when emitted so + * long literal source does not outlive its lexer view. + * Preserve the legacy leading-! string result in the + * dynamically sized runtime representation. + */ + if(pInstruction->theVal && pInstruction->theVal->vt == VT_STR) + { + const CHAR *value = StrCache_Get(pInstruction->theVal->strVal); + size_t value_length = strlen(value); + CHAR *prefixed_value = malloc(value_length + 2); + + prefixed_value[0] = '!'; + memcpy(prefixed_value + 1, value, value_length + 1); + ScriptVariant_Clear(pInstruction->theVal); + if(FAILED(ScriptVariant_ParseStringConstant( + pInstruction->theVal, + prefixed_value))) + { + pp_error( + &(pparser->theLexer.preprocessor), + "String result exceeds the maximum length of %u characters", + MAX_SCRIPT_STRING_LENGTH + ); + pparser->errorFound = TRUE; + } + free(prefixed_value); + } + else + { + sprintf(buf, "!%s", pInstruction->theToken->theSource); + strcpy(pInstruction->theToken->theSource, buf); + } } else { diff --git a/engine/source/scriptlib/ScriptVariant.c b/engine/source/scriptlib/ScriptVariant.c index 6ba63f3f2..17a6a4aa6 100644 --- a/engine/source/scriptlib/ScriptVariant.c +++ b/engine/source/scriptlib/ScriptVariant.c @@ -136,6 +136,9 @@ void StrCache_Collect(int index) int StrCache_Pop(int length) { int i; + + assert(length >= 0); + if(strcache_size == 0) { StrCache_Init(); @@ -163,6 +166,7 @@ int StrCache_Pop(int length) i = strcache_index[strcache_top--]; strcache[i].str = malloc(length + 1); strcache[i].str[0] = 0; + strcache[i].str[length] = 0; strcache[i].len = length; strcache[i].ref = 1; return i; @@ -229,12 +233,136 @@ void ScriptVariant_ChangeType(ScriptVariant *var, VARTYPE cvt) } } +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Decode one quoted script string literal. Source length is + explicit so the lexer may provide a non-owning view instead + of copying the literal through fixed token storage. +*/ +size_t ScriptString_DecodeLiteral( + CHAR *destination, + size_t destination_size, + const CHAR *source, + size_t source_length +) +{ + const CHAR *cursor; + const CHAR *end; + size_t output_length = 0; + + assert(source); + + cursor = source; + end = source + source_length; + + if(source_length >= 2 && cursor[0] == '"' && end[-1] == '"') + { + cursor++; + end--; + } + +#define APPEND_DECODED_CHARACTER(character) \ + do \ + { \ + if(destination && output_length + 1 < destination_size) \ + { \ + destination[output_length] = (character); \ + } \ + output_length++; \ + } while(0) + + while(cursor < end) + { + if(*cursor == '\\' && cursor + 1 < end) + { + cursor++; + + switch(*cursor) + { + case 's': + APPEND_DECODED_CHARACTER(' '); + cursor++; + break; + case 'r': + APPEND_DECODED_CHARACTER('\r'); + cursor++; + break; + case 'n': + APPEND_DECODED_CHARACTER('\n'); + cursor++; + break; + case 't': + APPEND_DECODED_CHARACTER('\t'); + cursor++; + break; + case '0': + APPEND_DECODED_CHARACTER('\0'); + cursor++; + break; + case '"': + APPEND_DECODED_CHARACTER('"'); + cursor++; + break; + case '\'': + APPEND_DECODED_CHARACTER('\''); + cursor++; + break; + case '\\': + APPEND_DECODED_CHARACTER('\\'); + cursor++; + break; + default: + /* Preserve the legacy invalid-escape result. */ + APPEND_DECODED_CHARACTER('\\'); + APPEND_DECODED_CHARACTER(*cursor); + break; + } + } + else + { + APPEND_DECODED_CHARACTER(*cursor); + cursor++; + } + } + + if(destination && destination_size) + { + destination[output_length < destination_size + ? output_length + : destination_size - 1] = '\0'; + } + +#undef APPEND_DECODED_CHARACTER + + return output_length; +} + // find an existing constant before copy -void ScriptVariant_ParseStringConstant(ScriptVariant *var, CHAR *str) +HRESULT ScriptVariant_ParseStringConstant(ScriptVariant *var, const CHAR *str) { //assert(index0); int i; + size_t length; + + if(!var || !str) + { + return E_FAIL; + } + + for(length = 0; + length <= MAX_SCRIPT_STRING_LENGTH && str[length]; + length++) + { + } + + if(length > MAX_SCRIPT_STRING_LENGTH) + { + return E_FAIL; + } + for(i = 0; i < strcache_size; i++) { if (strcache[i].ref && strcmp(str, strcache[i].str) == 0) @@ -242,12 +370,71 @@ void ScriptVariant_ParseStringConstant(ScriptVariant *var, CHAR *str) var->strVal = i; strcache[i].ref++; var->vt = VT_STR; - return; + return S_OK; } } ScriptVariant_ChangeType(var, VT_STR); var->strVal = StrCache_CreateNewFrom(str); + + return S_OK; +} + +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Convert a length-delimited source literal directly into a + runtime string constant without fixed intermediate storage. +*/ +HRESULT ScriptVariant_ParseStringLiteral( + ScriptVariant *var, + const CHAR *source, + size_t source_length +) +{ + CHAR *decoded; + size_t decoded_length; + + if(!var || !source) + { + return E_FAIL; + } + + decoded_length = ScriptString_DecodeLiteral( + NULL, + 0, + source, + source_length + ); + + if(decoded_length > MAX_SCRIPT_STRING_LENGTH) + { + return E_FAIL; + } + + decoded = malloc(decoded_length + 1); + + if(!decoded) + { + return E_FAIL; + } + + ScriptString_DecodeLiteral( + decoded, + decoded_length + 1, + source, + source_length + ); + if(FAILED(ScriptVariant_ParseStringConstant(var, decoded))) + { + free(decoded); + return E_FAIL; + } + + free(decoded); + + return S_OK; } /* @@ -458,84 +645,161 @@ BOOL ScriptVariant_IsTrue(ScriptVariant *svar) { } /* -* Caskey, Damon V. -* Orginal author (Utunels?) and date unknown. -* -* Reworked 2026-06-02 to handle 64-bit -* integers. +- Caskey, Damon V. +- 2026-08-11 +- +- Return a non-owning view of a script variant's string + representation. String variants reference their cache-owned + text directly. Other types use caller-provided conversion + storage with explicit capacity. */ -void ScriptVariant_ToString(ScriptVariant *svar, LPSTR buffer) { - - switch( svar->vt ) { - case VT_EMPTY: - sprintf( buffer, " Unitialized" ); - break; +HRESULT ScriptVariant_GetStringView( + const ScriptVariant *svar, + CHAR *conversion_buffer, + size_t conversion_buffer_size, + ScriptVariantStringView *view +) +{ + const CHAR *terminator; + int result; - case VT_INTEGER: - sprintf( buffer, "%ld", (long)svar->lVal); - break; + if(!svar || !view) + { + return E_FAIL; + } - case VT_INTEGER64: - sprintf( buffer, "%" PRId64, (int64_t)svar->llVal); - break; + view->string = NULL; + view->length = 0; - case VT_UINTEGER64: - sprintf( buffer, "%" PRIu64, (uint64_t)svar->ullVal); - break; + if(svar->vt == VT_STR) + { + if(svar->strVal < 0 || svar->strVal >= strcache_size || + !strcache[svar->strVal].str || + strcache[svar->strVal].len < 0 || + (size_t)strcache[svar->strVal].len > MAX_SCRIPT_STRING_LENGTH) + { + return E_FAIL; + } - case VT_DECIMAL: - sprintf( buffer, "%lf", svar->dblVal ); - break; + view->string = strcache[svar->strVal].str; + terminator = memchr( + view->string, + '\0', + (size_t)strcache[svar->strVal].len + 1 + ); - case VT_PTR: - sprintf(buffer, "#%" PRIuPTR, (uintptr_t)svar->ptrVal); - break; + if(!terminator) + { + view->string = NULL; + view->length = 0; + return E_FAIL; + } - case VT_STR: - sprintf(buffer, "%s", StrCache_Get(svar->strVal)); - break; + view->length = (size_t)(terminator - view->string); - default: - sprintf(buffer, "" ); - break; + return S_OK; } + + if(!conversion_buffer || !conversion_buffer_size) + { + return E_FAIL; + } + + conversion_buffer[0] = '\0'; + + switch(svar->vt) + { + case VT_EMPTY: + result = snprintf(conversion_buffer, conversion_buffer_size, " Unitialized"); + break; + case VT_INTEGER: + result = snprintf(conversion_buffer, conversion_buffer_size, "%ld", (long)svar->lVal); + break; + case VT_INTEGER64: + result = snprintf(conversion_buffer, conversion_buffer_size, "%" PRId64, (int64_t)svar->llVal); + break; + case VT_UINTEGER64: + result = snprintf(conversion_buffer, conversion_buffer_size, "%" PRIu64, (uint64_t)svar->ullVal); + break; + case VT_DECIMAL: + result = snprintf(conversion_buffer, conversion_buffer_size, "%lf", svar->dblVal); + break; + case VT_PTR: + result = snprintf(conversion_buffer, conversion_buffer_size, "#%" PRIuPTR, (uintptr_t)svar->ptrVal); + break; + default: + result = snprintf(conversion_buffer, conversion_buffer_size, ""); + break; + } + + if(result < 0 || (size_t)result >= conversion_buffer_size || + (size_t)result > MAX_SCRIPT_STRING_LENGTH) + { + conversion_buffer[0] = '\0'; + return E_FAIL; + } + + view->string = conversion_buffer; + view->length = (size_t)result; + + return S_OK; } /* -* Caskey, Damon V. -* Orginal author (Utunels?) and date unknown. -* -* Reworked 2026-06-02 to handle 64-bit integers. -* -* Get the length of a variant when converted to a string. +- Caskey, Damon V. +- 2026-08-11 +- +- Copy a script variant's string representation into caller + storage with explicit capacity and optional output length. */ -static int ScriptVariant_LengthAsString(ScriptVariant *svar) { - - switch (svar->vt) { - case VT_EMPTY: - return snprintf(NULL, 0, " Unitialized"); +HRESULT ScriptVariant_ToString( + const ScriptVariant *svar, + LPSTR buffer, + size_t buffer_size, + size_t *output_length +) +{ + ScriptVariantStringView view; - case VT_INTEGER: - return snprintf(NULL, 0, "%ld", (long)svar->lVal); + if(output_length) + { + *output_length = 0; + } - case VT_INTEGER64: - return snprintf(NULL, 0, "%" PRId64, (int64_t)svar->llVal); + if(!buffer || !buffer_size) + { + return E_FAIL; + } - case VT_UINTEGER64: - return snprintf(NULL, 0, "%" PRIu64, (uint64_t)svar->ullVal); + buffer[0] = '\0'; - case VT_DECIMAL: - return snprintf(NULL, 0, "%lf", svar->dblVal); + if(FAILED(ScriptVariant_GetStringView( + svar, + buffer, + buffer_size, + &view + ))) + { + return E_FAIL; + } - case VT_PTR: - return snprintf(NULL, 0, "#%" PRIuPTR, (uintptr_t)svar->ptrVal); + if(view.length >= buffer_size) + { + buffer[0] = '\0'; + return E_FAIL; + } - case VT_STR: - return snprintf(NULL, 0, "%s", StrCache_Get(svar->strVal)); + if(view.string != buffer) + { + memmove(buffer, view.string, view.length + 1); + } - default: - return snprintf(NULL, 0, ""); + if(output_length) + { + *output_length = view.length; } + + return S_OK; } /* @@ -1759,8 +2023,26 @@ ScriptVariant *ScriptVariant_Add(ScriptVariant *svar, ScriptVariant *rightChild) if (svar->vt == VT_STR || rightChild->vt == VT_STR) { CHAR *destination_string; - int length_a = ScriptVariant_LengthAsString(svar); - int length_b = ScriptVariant_LengthAsString(rightChild); + CHAR conversion_a[SCRIPT_VARIANT_CONVERSION_BUFFER_LENGTH]; + CHAR conversion_b[SCRIPT_VARIANT_CONVERSION_BUFFER_LENGTH]; + ScriptVariantStringView view_a; + ScriptVariantStringView view_b; + + if(FAILED(ScriptVariant_GetStringView( + svar, + conversion_a, + sizeof(conversion_a), + &view_a)) || + FAILED(ScriptVariant_GetStringView( + rightChild, + conversion_b, + sizeof(conversion_b), + &view_b)) || + view_a.length > MAX_SCRIPT_STRING_LENGTH - view_b.length) + { + ScriptVariant_Clear(&retvar); + return &retvar; + } ScriptVariant_ChangeType(&retvar, VT_STR); @@ -1769,22 +2051,22 @@ ScriptVariant *ScriptVariant_Add(ScriptVariant *svar, ScriptVariant *rightChild) * inline text. Reserve a cache entry large enough for * both operands and get its writable character buffer. */ - retvar.strVal = StrCache_Pop(length_a + length_b); + retvar.strVal = StrCache_Pop((int)(view_a.length + view_b.length)); destination_string = StrCache_Get(retvar.strVal); /* * Fill the cache-owned buffer: left operand first, * then right operand at the end of the left text. */ - ScriptVariant_ToString(svar, destination_string); - ScriptVariant_ToString(rightChild, destination_string + length_a); + memcpy(destination_string, view_a.string, view_a.length); + memcpy(destination_string + view_a.length, view_b.string, view_b.length); /* * Finalize the cache-owned buffer as a C string. * The cache releases it later through reference * counting. */ - destination_string[length_a + length_b] = '\0'; + destination_string[view_a.length + view_b.length] = '\0'; return &retvar; } @@ -2567,6 +2849,3 @@ void ScriptVariant_Boolean_Not(ScriptVariant *svar ) svar->lVal = b; } - - - diff --git a/engine/source/scriptlib/ScriptVariant.h b/engine/source/scriptlib/ScriptVariant.h index dc85bb528..c74946ca6 100644 --- a/engine/source/scriptlib/ScriptVariant.h +++ b/engine/source/scriptlib/ScriptVariant.h @@ -10,6 +10,7 @@ #define SCRIPTVARIANT_H #include "depends.h" +#include #include typedef enum VariantType { @@ -23,6 +24,13 @@ typedef enum VariantType { VT_STR = (1U << 5) // char*. } VARTYPE; +/* +* Script strings are dynamically allocated. This is a policy +* bound, not the capacity of a fixed storage buffer. +*/ +#define MAX_SCRIPT_STRING_LENGTH 65535U +#define SCRIPT_VARIANT_CONVERSION_BUFFER_LENGTH 512U + /* * Query masks only. These are not concrete * types and must never be stored in @@ -47,6 +55,11 @@ typedef struct ScriptVariant { VARTYPE vt; } ScriptVariant; +typedef struct ScriptVariantStringView { + const CHAR *string; + size_t length; +} ScriptVariantStringView; + /* * Caskey, Damon V. * 2023-04-17 @@ -77,13 +90,16 @@ void ScriptVariant_Clear(ScriptVariant *var); void ScriptVariant_Init(ScriptVariant *var); void ScriptVariant_Copy(ScriptVariant *svar, ScriptVariant *rightChild ); // faster in some situations void ScriptVariant_ChangeType(ScriptVariant *var, VARTYPE cvt); -void ScriptVariant_ParseStringConstant(ScriptVariant *var, CHAR *str); +size_t ScriptString_DecodeLiteral(CHAR *destination, size_t destination_size, const CHAR *source, size_t source_length); +HRESULT ScriptVariant_ParseStringConstant(ScriptVariant *var, const CHAR *str); +HRESULT ScriptVariant_ParseStringLiteral(ScriptVariant *var, const CHAR *source, size_t source_length); HRESULT ScriptVariant_IntegerValue(ScriptVariant *var, LONG *pVal); HRESULT ScriptVariant_DecimalValue(ScriptVariant *var, DOUBLE *pVal); HRESULT ScriptVariant_Integer64Value(ScriptVariant *var, int64_t *pVal); HRESULT ScriptVariant_Unsigned64Value(ScriptVariant *var, uint64_t *pVal); BOOL ScriptVariant_IsTrue(ScriptVariant *svar); -void ScriptVariant_ToString(ScriptVariant *svar, LPSTR buffer ); +HRESULT ScriptVariant_GetStringView(const ScriptVariant *svar, CHAR *conversion_buffer, size_t conversion_buffer_size, ScriptVariantStringView *view); +HRESULT ScriptVariant_ToString(const ScriptVariant *svar, LPSTR buffer, size_t buffer_size, size_t *output_length); // light version, for compiled call, faster than above, but not safe in some situations // This function are used by compiled scripts diff --git a/engine/source/utils.c b/engine/source/utils.c index 375b09d2a..fccd3be6e 100644 --- a/engine/source/utils.c +++ b/engine/source/utils.c @@ -196,9 +196,15 @@ stringptr *readFromLogFile(int which) } -void writeToLogFile(const char *msg, ...) +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Write a formatted message to the engine log from an existing + variable-argument list without fixed-capacity staging. +*/ +void writeToLogFileV(const char *message, va_list arguments) { - va_list arglist; if(openborLog == NULL) { openborLog = OPEN_LOGFILE(OPENBOR_LOG); @@ -207,9 +213,43 @@ void writeToLogFile(const char *msg, ...) return; } } + vfprintf(openborLog, message, arguments); + fflush(openborLog); +} + +void writeToLogFile(const char *msg, ...) +{ + va_list arglist; + va_start(arglist, msg); - vfprintf(openborLog, msg, arglist); + writeToLogFileV(msg, arglist); va_end(arglist); +} + +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Write a length-delimited message to the engine log without + treating creator-provided text as a format string. +*/ +void writeToLogFileLength(const char *message, size_t length) +{ + if(!message) + { + return; + } + + if(openborLog == NULL) + { + openborLog = OPEN_LOGFILE(OPENBOR_LOG); + if(openborLog == NULL) + { + return; + } + } + + fwrite(message, 1, length, openborLog); fflush(openborLog); } @@ -622,4 +662,3 @@ void Array_Check_Size( const char *f_caller, char **array, int new_size, int *cu // ReAssign the new allocated array *array = copy; } - diff --git a/engine/source/utils.h b/engine/source/utils.h index 9f17767ba..e16c63bc4 100644 --- a/engine/source/utils.h +++ b/engine/source/utils.h @@ -10,6 +10,7 @@ #define UTILS_H // *** INCLUDES *** +#include #include #include "types.h" #include "stringptr.h" @@ -25,6 +26,8 @@ extern u32 debug_time; // *** FUNCTIONS DECLARATIONS *** void writeToLogFile(const char *, ...); +void writeToLogFileV(const char *message, va_list arguments); +void writeToLogFileLength(const char *message, size_t length); void writeToScriptLog(const char *msg); int fileExists(char *fnam); int dirExists(char *dname, int create); @@ -54,4 +57,3 @@ void get_now_string(char buffer[], unsigned buffer_size, char* pattern); void Array_Check_Size( const char *f_caller, char **array, int new_size, int *curr_size_allocated, int grow_step ); #endif -