From 9e742a247273debeda7a7ea7efe50dd2c888327c Mon Sep 17 00:00:00 2001 From: DCurrent Date: Tue, 11 Aug 2026 14:31:16 -0400 Subject: [PATCH 01/11] Read sequential properties one argument at a time Replace whole-line `ArgList` collection for multi-value properties with sequential argument reading. * Read each argument directly from the source line into a reusable 64-byte buffer. * Remove aggregate `MAX_ARG_LEN` and `MAX_ARG_COUNT` limits from open-ended property lists. * Preserve quoting, comments, and the existing 63-character per-item limit. * Count weapon arguments before allocating the persistent weapon list exactly once. * Apply complete `allowselect` lists at runtime while retaining only whole entries that fit its legacy save field. * Keep the legacy 512-byte limit unchanged for general parsing, paths, save fields, and script file-stream arguments. --- engine/openbor.c | 743 +++++++++++++++++++++++++++++++---------------- engine/openbor.h | 32 +- 2 files changed, 509 insertions(+), 266 deletions(-) diff --git a/engine/openbor.c b/engine/openbor.c index 92db70feb..6cb3f6307 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -3306,6 +3306,107 @@ static bool command_token_reader_next(s_command_token_reader* reader, s_command_ return true; } +/* +- 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 64 + +/* +- 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 + )) { + 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)) { + *value = NULL; + return false; + } + + if(token.length >= sizeof(reader->value)) { + borShutdown( + 1, + "Command argument exceeds the maximum length of %zu characters.\n", + sizeof(reader->value) - 1 + ); + *value = NULL; + return false; + } + + memcpy(reader->value, token.text, token.length); + reader->value[token.length] = '\0'; + *value = reader->value; + + return true; +} + /* * Compare a command token with a null-terminated * string without regard to letter case. @@ -6624,43 +6725,87 @@ static void reset_playable_list(char which) } } -// Specify which Player Models are allowable for selecting -static void load_playable_list(char *buf) +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Read selectable player model names directly from the source + line one item at a time. Apply the complete runtime list and + retain as many whole arguments as the legacy save field holds. +*/ +static void load_playable_list(const char* command_line) { - int i, index; - char *value; - s_model *playermodels = NULL; - ArgList arglist; - char argbuf[MAX_ALLOWSELECT_LEN] = ""; + const char* value; + s_command_argument_reader reader; + s_model* playermodel; + size_t saved_length = 0; + bool save_capacity_reached = false; + int index; + + if(!command_argument_reader_initialize( + &reader, + command_line, + 0 + ) + || !command_argument_reader_next(&reader, &value) + || stricmp(value, "allowselect") != 0) { + return; + } - ParseArgs(&arglist, buf, argbuf); + reset_playable_list(0); + memset(allowselect_args, 0, sizeof(allowselect_args)); + memcpy( + allowselect_args, + "allowselect", + sizeof("allowselect") + ); + saved_length = sizeof("allowselect") - 1; - // avoid to load characters if there isn't an allowselect - if ( stricmp(value = GET_ARG(0), "allowselect") != 0 ) return; + while(command_argument_reader_next(&reader, &value)) { + const size_t value_length = strlen(value); + const size_t remaining_capacity = + sizeof(allowselect_args) - saved_length; - reset_playable_list(0); + if(!save_capacity_reached + && value_length < remaining_capacity + && remaining_capacity - value_length > 1) { + allowselect_args[saved_length++] = ' '; - 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] = ' '; - } - allowselect_args[sizeof(argbuf)-1] = '\0'; + memcpy( + allowselect_args + saved_length, + value, + value_length + ); - 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) - { + saved_length += value_length; + allowselect_args[saved_length] = '\0'; + } else { + save_capacity_reached = true; + } + + playermodel = findmodel((char*)value); + + if(!playermodel) { + borShutdown(1, "Player model '%s' is not loaded.\n", value); + } + + index = get_cached_model_index(playermodel->name); + + if(index == -1) { borShutdown(1, "Player model '%s' is not cached.\n", value); } + model_cache[index].selectable = 1; } - return; + if(save_capacity_reached) { + printf( + "Warning: allowselect exceeds the legacy save field; " + "the active list is complete, but saved continuation data " + "contains only the complete names that fit.\n" + ); + } + } /* @@ -7448,7 +7593,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 +7707,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 +10929,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 +11597,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 +13205,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 +13312,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 +13372,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 +13398,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 +13474,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 +13925,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 +14024,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 +14107,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 +14176,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 +14238,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 +14296,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 @@ -15056,7 +15267,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 +15429,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 +15602,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 +15616,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 +15642,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 +15823,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 +15883,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 +15952,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 +15992,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 +16102,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 +16277,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; @@ -16920,28 +17131,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 +17164,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 +18370,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 +18656,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); } @@ -20948,7 +21158,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); @@ -23728,37 +23938,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 +29346,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 +29413,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 +36232,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 +36453,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 +36528,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 +36790,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 +36866,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 +37449,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 +37468,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 +37487,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 +37508,7 @@ void defense_setup_from_arg( defense_apply_setup_to_property( filename, command, + command_line, &target_defense[i], arglist, target_parameter @@ -46859,21 +47100,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); } diff --git a/engine/openbor.h b/engine/openbor.h index f1a3f5bd9..0daceef49 100644 --- a/engine/openbor.h +++ b/engine/openbor.h @@ -4353,13 +4353,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 +4372,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); @@ -4542,7 +4542,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 +4559,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 +4659,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 +4672,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 +4682,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); From 3ee00c9dff793c948bdccf64ccf29377baa87f2f Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Tue, 11 Aug 2026 15:13:44 -0400 Subject: [PATCH 02/11] Unbind allowselect from fixed storage Store complete allowselect command lines in dynamically sized runtime and per-difficulty save state. Append length-prefixed allowselect data after the legacy fixed save records so current builds can round-trip lists of any practical size. Preserve a whole-argument compatibility mirror for older saves and older engine builds. Load the legacy fixed field when no dynamic extension is present, validate extension lengths against the remaining file, and release all owned state during save resets, level-order resizing, and shutdown. --- engine/openbor.c | 494 +++++++++++++++++++++++++++++++++++++++++------ engine/openbor.h | 5 +- 2 files changed, 443 insertions(+), 56 deletions(-) diff --git a/engine/openbor.c b/engine/openbor.c index 6cb3f6307..092140c27 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] = {""}; @@ -2753,11 +2759,178 @@ void loadfromdefault() } +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Append complete allowselect state to the legacy fixed-record + save file. Older engines ignore the trailing extension and + retain their fixed compatibility mirror. Current engines use + the 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 void restore_saved_allowselect_legacy_arguments(void) +{ + size_t i; + + for(i = 0; i < savelevel_count; i++) + { + set_saved_allowselect_arguments( + i, + savelevel[i].allowSelectArgsLegacy + ); + } +} + +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) + { + restore_saved_allowselect_legacy_arguments(); + return true; + } + + if(magic != SAVE_ALLOWSELECT_EXTENSION_MAGIC) + { + restore_saved_allowselect_legacy_arguments(); + return true; + } + + 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) + { + restore_saved_allowselect_legacy_arguments(); + return true; + } + + 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 +2950,7 @@ void clearHighScore() int saveGameFile() { + size_t i; FILE *handle = NULL; char path[MAX_BUFFER_LEN] = {""}; char tmpname[MAX_BUFFER_LEN] = {""}; @@ -2792,7 +2966,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 +2993,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 +3010,25 @@ 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 + && savelevel[0].compatibleversion != CV_SAVED_GAME) + || !read_saved_allowselect_extension(handle)) { clearSavedGame(); result = 0; @@ -2831,7 +3036,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; } @@ -3407,6 +3612,117 @@ static bool command_argument_reader_next( return true; } +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Own complete allowselect command lines independently from + the packed legacy save record. Keep a whole-argument mirror + only so older save readers retain their historical behavior. +*/ +static void copy_legacy_allowselect_arguments( + char* destination, + size_t capacity, + const char* source +) { + const char* value; + s_command_argument_reader reader; + size_t length = 0; + + if(!destination || !capacity) { + return; + } + + destination[0] = '\0'; + + if(!source + || !source[0] + || !command_argument_reader_initialize(&reader, source, 0)) { + return; + } + + while(command_argument_reader_next(&reader, &value)) { + const size_t value_length = strlen(value); + const size_t separator_length = length ? 1 : 0; + + if(separator_length > capacity - length - 1 + || value_length + > capacity - length - separator_length - 1) { + break; + } + + if(separator_length) { + destination[length++] = ' '; + } + + memcpy(destination + length, value, value_length); + length += value_length; + destination[length] = '\0'; + } +} + +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) { + return NULL; + } + + if(savelevel_allowselect_args + && savelevel_allowselect_args[index]) { + return savelevel_allowselect_args[index]; + } + + return savelevel[index].allowSelectArgsLegacy; +} + +static void set_saved_allowselect_arguments( + size_t index, + const char* source +) { + char* owned_source = NULL; + + if(index >= savelevel_count + || !savelevel + || !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; + + copy_legacy_allowselect_arguments( + savelevel[index].allowSelectArgsLegacy, + sizeof(savelevel[index].allowSelectArgsLegacy), + owned_source + ); +} + /* * Compare a command token with a null-terminated * string without regard to letter case. @@ -6725,24 +7041,81 @@ static void reset_playable_list(char which) } } +/* +- 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; + } + + required_capacity = + *length + separator_length + value_length + 1; + + if(required_capacity > *capacity) { + expanded_capacity = *capacity ? *capacity : 64; + + while(expanded_capacity < required_capacity) { + if(expanded_capacity > SIZE_MAX / 2) { + expanded_capacity = required_capacity; + break; + } + + expanded_capacity *= 2; + } + + *command_line = realloc(*command_line, expanded_capacity); + *capacity = expanded_capacity; + } + + 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 the complete runtime list and - retain as many whole arguments as the legacy save field holds. + 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; - size_t saved_length = 0; - bool save_capacity_reached = false; + char* stored_arguments = NULL; + size_t stored_length = 0; + size_t stored_capacity = 0; int index; - if(!command_argument_reader_initialize( + if(!command_line + || !command_argument_reader_initialize( &reader, command_line, 0 @@ -6753,59 +7126,41 @@ static void load_playable_list(const char* command_line) } reset_playable_list(0); - memset(allowselect_args, 0, sizeof(allowselect_args)); - memcpy( - allowselect_args, - "allowselect", - sizeof("allowselect") + append_command_argument( + &stored_arguments, + &stored_length, + &stored_capacity, + "allowselect" ); - saved_length = sizeof("allowselect") - 1; while(command_argument_reader_next(&reader, &value)) { - const size_t value_length = strlen(value); - const size_t remaining_capacity = - sizeof(allowselect_args) - saved_length; - - if(!save_capacity_reached - && value_length < remaining_capacity - && remaining_capacity - value_length > 1) { - allowselect_args[saved_length++] = ' '; - - memcpy( - allowselect_args + saved_length, - value, - value_length - ); - - saved_length += value_length; - allowselect_args[saved_length] = '\0'; - } else { - save_capacity_reached = true; - } - 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; - } - - if(save_capacity_reached) { - printf( - "Warning: allowselect exceeds the legacy save field; " - "the active list is complete, but saved continuation data " - "contains only the complete names that fit.\n" + append_command_argument( + &stored_arguments, + &stored_length, + &stored_capacity, + value ); } + free(allowselect_args); + allowselect_args = stored_arguments; } /* @@ -22714,9 +23069,26 @@ 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) + ); + restore_saved_allowselect_legacy_arguments(); } if(errormessage) @@ -50980,10 +51352,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(); @@ -51740,8 +52119,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) @@ -52059,8 +52437,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); } @@ -52074,7 +52453,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; } } @@ -52116,7 +52497,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) { @@ -52610,7 +52994,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 0daceef49..245856453 100644 --- a/engine/openbor.h +++ b/engine/openbor.h @@ -123,7 +123,7 @@ 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_LEGACY_ALLOWSELECT_LEN 1024 #define MAX_SELECT_LOADS 512 #define MAX_PAL_SIZE 1024 #define MAX_CACHED_BACKGROUNDS 9 @@ -5036,7 +5036,8 @@ typedef struct int pColourmap[MAX_PLAYERS]; // colour map int selectFlag; // saved a select.txt infos - char allowSelectArgs[MAX_ALLOWSELECT_LEN]; // allowselect arguments + // Fixed compatibility mirror. Current saves append the complete dynamic list. + char allowSelectArgsLegacy[MAX_LEGACY_ALLOWSELECT_LEN]; 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 From 7a46fd6919fdddb6729ddc5f542b7c22c82ea35d Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Tue, 11 Aug 2026 15:32:56 -0400 Subject: [PATCH 03/11] Remove legacy allowselect save storage Delete the fixed 1,024-byte compatibility mirror and fallback path. Require dynamic allowselect save data and advance the saved-game compatibility version so older record layouts are rejected. --- engine/openbor.c | 105 +++++------------------------------------------ engine/openbor.h | 5 +-- 2 files changed, 11 insertions(+), 99 deletions(-) diff --git a/engine/openbor.c b/engine/openbor.c index 092140c27..ba6c173c1 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -2763,27 +2763,13 @@ void loadfromdefault() - Caskey, Damon V. - 2026-08-11 - -- Append complete allowselect state to the legacy fixed-record - save file. Older engines ignore the trailing extension and - retain their fixed compatibility mirror. Current engines use - the length-prefixed values without imposing a list-size limit. +- 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 void restore_saved_allowselect_legacy_arguments(void) -{ - size_t i; - - for(i = 0; i < savelevel_count; i++) - { - set_saved_allowselect_arguments( - i, - savelevel[i].allowSelectArgsLegacy - ); - } -} - static bool write_saved_allowselect_extension(FILE* handle) { const uint64_t magic = SAVE_ALLOWSELECT_EXTENSION_MAGIC; @@ -2848,16 +2834,10 @@ static bool read_saved_allowselect_extension(FILE* handle) char** loaded_values; size_t i; - if(fread(&magic, sizeof(magic), 1, handle) != 1) - { - restore_saved_allowselect_legacy_arguments(); - return true; - } - - if(magic != SAVE_ALLOWSELECT_EXTENSION_MAGIC) + if(fread(&magic, sizeof(magic), 1, handle) != 1 + || magic != SAVE_ALLOWSELECT_EXTENSION_MAGIC) { - restore_saved_allowselect_legacy_arguments(); - return true; + return false; } if(fread(&version, sizeof(version), 1, handle) != 1 @@ -2869,8 +2849,7 @@ static bool read_saved_allowselect_extension(FILE* handle) if(version != SAVE_ALLOWSELECT_EXTENSION_VERSION || entry_count != (uint64_t)savelevel_count) { - restore_saved_allowselect_legacy_arguments(); - return true; + return false; } loaded_values = calloc(savelevel_count, sizeof(*loaded_values)); @@ -3026,7 +3005,6 @@ int loadGameFile() if(fread(savelevel, sizeof(*savelevel), savelevel_count, handle) != savelevel_count || (savelevel_count - && savelevel[0].compatibleversion && savelevel[0].compatibleversion != CV_SAVED_GAME) || !read_saved_allowselect_extension(handle)) { @@ -3612,55 +3590,6 @@ static bool command_argument_reader_next( return true; } -/* -- Caskey, Damon V. -- 2026-08-11 -- -- Own complete allowselect command lines independently from - the packed legacy save record. Keep a whole-argument mirror - only so older save readers retain their historical behavior. -*/ -static void copy_legacy_allowselect_arguments( - char* destination, - size_t capacity, - const char* source -) { - const char* value; - s_command_argument_reader reader; - size_t length = 0; - - if(!destination || !capacity) { - return; - } - - destination[0] = '\0'; - - if(!source - || !source[0] - || !command_argument_reader_initialize(&reader, source, 0)) { - return; - } - - while(command_argument_reader_next(&reader, &value)) { - const size_t value_length = strlen(value); - const size_t separator_length = length ? 1 : 0; - - if(separator_length > capacity - length - 1 - || value_length - > capacity - length - separator_length - 1) { - break; - } - - if(separator_length) { - destination[length++] = ' '; - } - - memcpy(destination + length, value, value_length); - length += value_length; - destination[length] = '\0'; - } -} - static void clear_saved_allowselect_arguments(void) { size_t i; @@ -3677,16 +3606,11 @@ static void clear_saved_allowselect_arguments(void) static const char* get_saved_allowselect_arguments(size_t index) { - if(index >= savelevel_count || !savelevel) { + if(index >= savelevel_count || !savelevel_allowselect_args) { return NULL; } - if(savelevel_allowselect_args - && savelevel_allowselect_args[index]) { - return savelevel_allowselect_args[index]; - } - - return savelevel[index].allowSelectArgsLegacy; + return savelevel_allowselect_args[index]; } static void set_saved_allowselect_arguments( @@ -3695,9 +3619,7 @@ static void set_saved_allowselect_arguments( ) { char* owned_source = NULL; - if(index >= savelevel_count - || !savelevel - || !savelevel_allowselect_args) { + if(index >= savelevel_count || !savelevel_allowselect_args) { return; } @@ -3715,12 +3637,6 @@ static void set_saved_allowselect_arguments( free(savelevel_allowselect_args[index]); savelevel_allowselect_args[index] = owned_source; - - copy_legacy_allowselect_arguments( - savelevel[index].allowSelectArgsLegacy, - sizeof(savelevel[index].allowSelectArgsLegacy), - owned_source - ); } /* @@ -23088,7 +23004,6 @@ void load_levelorder() savelevel_count, sizeof(*savelevel_allowselect_args) ); - restore_saved_allowselect_legacy_arguments(); } if(errormessage) diff --git a/engine/openbor.h b/engine/openbor.h index 245856453..b38a865ee 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_LEGACY_ALLOWSELECT_LEN 1024 #define MAX_SELECT_LOADS 512 #define MAX_PAL_SIZE 1024 #define MAX_CACHED_BACKGROUNDS 9 @@ -5036,8 +5035,6 @@ typedef struct int pColourmap[MAX_PLAYERS]; // colour map int selectFlag; // saved a select.txt infos - // Fixed compatibility mirror. Current saves append the complete dynamic list. - char allowSelectArgsLegacy[MAX_LEGACY_ALLOWSELECT_LEN]; 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 From 4a34d6d506f8124abbb673025fd214f6c3f211de Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Tue, 11 Aug 2026 16:27:28 -0400 Subject: [PATCH 04/11] Read file stream arguments sequentially Remove the fixed 512-byte findarg buffer and read requested file-stream arguments through the shared sequential token reader. Allocate script storage for only the requested argument, preserve missing-value conversions, and move model frame lookahead onto the same token reader. --- engine/openbor.c | 130 ++++++++++++++++++++--------------------- engine/openbor.h | 2 +- engine/openborscript.c | 70 +++++++++++++++++----- 3 files changed, 120 insertions(+), 82 deletions(-) diff --git a/engine/openbor.c b/engine/openbor.c index ba6c173c1..c4bc09ac3 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -3489,6 +3489,53 @@ static bool command_token_reader_next(s_command_token_reader* reader, s_command_ return true; } +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Read one requested command argument sequentially from the + source line. Return a non-owning view so the caller can + allocate only the storage required for that argument. +*/ +bool command_argument_read( + const char* command_line, + size_t argument_index, + const char** value, + size_t* length +) { + s_command_token_reader reader; + s_command_token token; + + assert(command_line); + assert(value); + assert(length); + + reader.cursor = command_line; + + while(argument_index) { + if(!command_token_reader_next(&reader, &token)) { + *value = NULL; + *length = 0; + + return false; + } + + argument_index--; + } + + if(!command_token_reader_next(&reader, &token)) { + *value = NULL; + *length = 0; + + return false; + } + + *value = token.text; + *length = token.length; + + return true; +} + /* - Caskey, Damon V. - 2026-08-11 @@ -4938,67 +4985,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) @@ -15223,7 +15209,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] = { "" }; @@ -19794,6 +19779,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: "); @@ -19808,12 +19796,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; } diff --git a/engine/openbor.h b/engine/openbor.h index b38a865ee..3350e8162 100644 --- a/engine/openbor.h +++ b/engine/openbor.h @@ -4410,6 +4410,7 @@ 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); +bool command_argument_read (const char* command_line, size_t argument_index, const char** value, size_t* length); 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); @@ -4459,7 +4460,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); diff --git a/engine/openborscript.c b/engine/openborscript.c index 46bd94523..2a84a5f33 100644 --- a/engine/openborscript.c +++ b/engine/openborscript.c @@ -34,6 +34,7 @@ #include "ImportCache.h" #include "models.h" #include "scriptcommon.h" +#include Varlist global_var_list; Script *pcurrentscript = NULL; //used by local script functions @@ -10640,9 +10641,20 @@ 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 that argument into exact-sized script storage. + Numeric conversions reuse the same temporary script string. +*/ HRESULT openbor_getfilestreamargument(ScriptVariant **varlist , ScriptVariant **pretvar, int paramCount) { + const char *argument_text = ""; + char *converted_text; ScriptVariant *arg = NULL; LONG filestreamindex, argument; + size_t argument_length = 0; char *argtype = NULL; if(paramCount < 3) { @@ -10669,27 +10681,55 @@ 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) { + if(!command_argument_read( + filestreams[filestreamindex].buf + + filestreams[filestreamindex].pos, + (size_t)argument, + &argument_text, + &argument_length + )) { + argument_text = ""; + } + } + + if(argument_length >= (size_t)INT_MAX) { + printf("File stream argument exceeds the script string capacity.\n"); + return E_FAIL; + } + + ScriptVariant_ChangeType(*pretvar, VT_STR); + (*pretvar)->strVal = StrCache_Pop((int)argument_length); + converted_text = StrCache_Get((*pretvar)->strVal); + memcpy(converted_text, argument_text, argument_length); + converted_text[argument_length] = '\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; } From 663db0c4816d369afacd07ccc6afdb8d00c7db94 Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Tue, 11 Aug 2026 17:53:22 -0400 Subject: [PATCH 05/11] Read @cmd arguments sequentially Consume @cmd function names and parameters directly from the original command line one item at a time. This removes ParseArgs' whole-line and argument-count ceilings from generated script calls. Restore the dedicated per-command-argument buffer to 512 bytes for paths and other bounded values, and append function names directly to the dynamic script buffer. --- engine/openbor.c | 56 +++++++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/engine/openbor.c b/engine/openbor.c index c4bc09ac3..008b60201 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -3544,7 +3544,7 @@ bool command_argument_read( argument. Keep this independent from the legacy command-line, script file-stream, path, and persistent save-field limit. */ -#define MAX_COMMAND_ARGUMENT_LEN 64 +#define MAX_COMMAND_ARGUMENT_LEN 512 /* - Caskey, Damon V. @@ -15220,7 +15220,6 @@ s_model *load_cached_model(char *name, char *owner, char unload) int script_id = -1; int frm_id = -1; int i = 0; - int j = 0; int tempInt = 0; int framecount = 0; int frameset = 0; @@ -15356,9 +15355,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 @@ -20186,6 +20190,11 @@ 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: + { + const char* command_argument = NULL; + s_command_argument_reader command_argument_reader; + bool first_command_argument; + //translate @cmd into script function call if(ani_id < 0) { @@ -20204,11 +20213,17 @@ s_model *load_cached_model(char *name, char *owner, char unload) 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]) + if(command_argument_reader_initialize( + &command_argument_reader, + buf + pos, + 1 + ) + && command_argument_reader_next( + &command_argument_reader, + &command_argument + )) { /* //no_cmd_compatible will try to optimize if(frame==n) @@ -20249,23 +20264,23 @@ s_model *load_cached_model(char *name, char *owner, char unload) 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); - - do //argument and comma + buffer_append(&scriptbuf, call_indent_text, 0xffffff, &sbsize, &scriptlen); + buffer_append(&scriptbuf, command_argument, 0xffffff, &sbsize, &scriptlen); + buffer_append(&scriptbuf, call_open_text, 0xffffff, &sbsize, &scriptlen); + + first_command_argument = true; + while(command_argument_reader_next( + &command_argument_reader, + &command_argument + )) { - 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_argument, 0xffffff, &sbsize, &scriptlen); + first_command_argument = false; } - while(value && value[0]); } buffer_append(&scriptbuf, endcall_text, 0xffffff, &sbsize, &scriptlen); @@ -20277,6 +20292,7 @@ s_model *load_cached_model(char *name, char *owner, char unload) buffer_append(&scriptbuf, endifid_text, 0xffffff, &sbsize, &scriptlen); // put back last chars buffer_append(&scriptbuf, sur_text, 0xffffff, &sbsize, &scriptlen); // put back last chars break; + } default: if(command && command[0]) { From 0a89e1dc25658ea805352b9b7595634662cbaca8 Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Tue, 11 Aug 2026 19:30:33 -0400 Subject: [PATCH 06/11] Unbind script string literal length Represent script string literals as non-owning pointer-and-length views through preprocessing and lexing, then materialize complete decoded values directly in StrCache when CONSTSTR instructions are emitted. Keep fixed token storage for identifiers and diagnostics only. Pass @cmd source tokens directly into its dynamic generated-script buffer so long quoted arguments do not pass through the 512-byte command scratch buffer. Preserve length-aware macro handling, escape behavior, switch-case lifetimes, and legacy unary-not string behavior. --- engine/openbor.c | 52 +++++--- engine/source/preprocessorlib/pp_lexer.c | 67 +++++++--- engine/source/preprocessorlib/pp_lexer.h | 4 +- engine/source/preprocessorlib/pp_parser.c | 88 ++++++++++--- engine/source/scriptlib/Instruction.c | 23 +++- engine/source/scriptlib/Interpreter.c | 3 +- engine/source/scriptlib/Lexer.c | 80 ++++-------- engine/source/scriptlib/Lexer.h | 4 +- engine/source/scriptlib/Parser.c | 47 ++++++- engine/source/scriptlib/ScriptVariant.c | 144 +++++++++++++++++++++- engine/source/scriptlib/ScriptVariant.h | 5 +- 11 files changed, 398 insertions(+), 119 deletions(-) diff --git a/engine/openbor.c b/engine/openbor.c index 008b60201..ab5d2c87c 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -1025,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"); @@ -1039,7 +1041,7 @@ 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; @@ -20191,8 +20193,8 @@ s_model *load_cached_model(char *name, char *owner, char unload) break; case CMD_MODEL_AT_CMD: { - const char* command_argument = NULL; - s_command_argument_reader command_argument_reader; + s_command_token command_token; + s_command_token_reader command_token_reader; bool first_command_argument; //translate @cmd into script function call @@ -20215,15 +20217,13 @@ s_model *load_cached_model(char *name, char *owner, char unload) } scriptbuf[scriptlen - strclen(endifid_text)] = 0; // cut last chars scriptlen = strlen(scriptbuf); - if(command_argument_reader_initialize( - &command_argument_reader, - buf + pos, - 1 - ) - && command_argument_reader_next( - &command_argument_reader, - &command_argument - )) + 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) @@ -20265,20 +20265,32 @@ s_model *load_cached_model(char *name, char *owner, char unload) scriptlen = strlen(scriptbuf); } buffer_append(&scriptbuf, call_indent_text, 0xffffff, &sbsize, &scriptlen); - buffer_append(&scriptbuf, command_argument, 0xffffff, &sbsize, &scriptlen); + buffer_append( + &scriptbuf, + command_token.text, + command_token.length, + &sbsize, + &scriptlen + ); buffer_append(&scriptbuf, call_open_text, 0xffffff, &sbsize, &scriptlen); first_command_argument = true; - while(command_argument_reader_next( - &command_argument_reader, - &command_argument + while(command_token_reader_next( + &command_token_reader, + &command_token )) { if(!first_command_argument) { buffer_append(&scriptbuf, comma_text, 0xffffff, &sbsize, &scriptlen); } - buffer_append(&scriptbuf, command_argument, 0xffffff, &sbsize, &scriptlen); + buffer_append( + &scriptbuf, + command_token.text, + command_token.length, + &sbsize, + &scriptlen + ); first_command_argument = false; } } 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..e53732704 100644 --- a/engine/source/scriptlib/Instruction.c +++ b/engine/source/scriptlib/Instruction.c @@ -21,6 +21,15 @@ void Instruction_InitViaToken(Instruction *pins, OpCode code, Token *pToken ) if(pToken) { *(pins->theToken) = *pToken; + + /* + * Materialize string constants while their non-owning + * source view is guaranteed to remain valid. + */ + if(code == CONSTSTR) + { + Instruction_ConvertConstant(pins); + } } else { @@ -497,7 +506,19 @@ void Instruction_ConvertConstant(Instruction *pins) { } else if(pins->OpCode == CONSTSTR) { pvar = (ScriptVariant *)malloc(sizeof(ScriptVariant)); ScriptVariant_Init(pvar); - ScriptVariant_ParseStringConstant(pvar, pins->theToken->theSource); + + if(pins->theToken->theStringLiteralSource) + { + ScriptVariant_ParseStringLiteral( + pvar, + pins->theToken->theStringLiteralSource, + pins->theToken->theStringLiteralLength + ); + } + else + { + ScriptVariant_ParseStringConstant(pvar, pins->theToken->theSource); + } } else { return; diff --git a/engine/source/scriptlib/Interpreter.c b/engine/source/scriptlib/Interpreter.c index 605e07d22..2d1553983 100644 --- a/engine/source/scriptlib/Interpreter.c +++ b/engine/source/scriptlib/Interpreter.c @@ -1107,7 +1107,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 +1633,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..2ef813a91 100644 --- a/engine/source/scriptlib/Parser.c +++ b/engine/source/scriptlib/Parser.c @@ -805,6 +805,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 +903,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 +1862,32 @@ 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); + ScriptVariant_ParseStringConstant( + pInstruction->theVal, + prefixed_value + ); + 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..262d8b866 100644 --- a/engine/source/scriptlib/ScriptVariant.c +++ b/engine/source/scriptlib/ScriptVariant.c @@ -229,8 +229,114 @@ 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) +void ScriptVariant_ParseStringConstant(ScriptVariant *var, const CHAR *str) { //assert(index0); @@ -250,6 +356,41 @@ void ScriptVariant_ParseStringConstant(ScriptVariant *var, CHAR *str) var->strVal = StrCache_CreateNewFrom(str); } +/* +- Caskey, Damon V. +- 2026-08-11 +- +- Convert a length-delimited source literal directly into a + runtime string constant without fixed intermediate storage. +*/ +void ScriptVariant_ParseStringLiteral( + ScriptVariant *var, + const CHAR *source, + size_t source_length +) +{ + CHAR *decoded; + size_t decoded_length; + + decoded_length = ScriptString_DecodeLiteral( + NULL, + 0, + source, + source_length + ); + decoded = malloc(decoded_length + 1); + + ScriptString_DecodeLiteral( + decoded, + decoded_length + 1, + source, + source_length + ); + ScriptVariant_ParseStringConstant(var, decoded); + + free(decoded); +} + /* * Caskey, Damon V. * Orginal author (Utunels?) and date unknown. @@ -2569,4 +2710,3 @@ void ScriptVariant_Boolean_Not(ScriptVariant *svar ) } - diff --git a/engine/source/scriptlib/ScriptVariant.h b/engine/source/scriptlib/ScriptVariant.h index dc85bb528..95a90b655 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 { @@ -77,7 +78,9 @@ 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); +void ScriptVariant_ParseStringConstant(ScriptVariant *var, const CHAR *str); +void 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); From 8d1de7a808a4ae916ce5a856e2011148bf80066b Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Tue, 11 Aug 2026 20:02:38 -0400 Subject: [PATCH 07/11] Harden script string conversion Replace capacityless ScriptVariant_ToString() calls with explicit-capacity conversion or direct string views. Bound script string inputs and concatenation at 65,535 characters through MAX_SCRIPT_STRING_LENGTH, expose the limit to script, and reject oversized literals during compilation. --- engine/openborscript.c | 202 +++++++++++++---- engine/source/openborscript/constants.c | 20 +- engine/source/openborscript/level.c | 23 +- engine/source/scriptlib/Instruction.c | 50 ++++- engine/source/scriptlib/Instruction.h | 4 +- engine/source/scriptlib/Interpreter.c | 36 ++- engine/source/scriptlib/Parser.c | 39 +++- engine/source/scriptlib/ScriptVariant.c | 277 ++++++++++++++++++------ engine/source/scriptlib/ScriptVariant.h | 19 +- 9 files changed, 530 insertions(+), 140 deletions(-) diff --git a/engine/openborscript.c b/engine/openborscript.c index 2a84a5f33..8c28f0c95 100644 --- a/engine/openborscript.c +++ b/engine/openborscript.c @@ -35,6 +35,7 @@ #include "models.h" #include "scriptcommon.h" #include +#include Varlist global_var_list; Script *pcurrentscript = NULL; //used by local script functions @@ -1340,8 +1341,8 @@ HRESULT openbor_setsystemvariant(ScriptVariant **varlist , ScriptVariant **pretv //drawstring(x, y, font, string, z); HRESULT openbor_drawstring(ScriptVariant **varlist , ScriptVariant **pretvar, int paramCount) { - int i; char buf[MAX_BUFFER_LEN]; + int i; LONG value[4]; *pretvar = NULL; @@ -1368,7 +1369,16 @@ HRESULT openbor_drawstring(ScriptVariant **varlist , ScriptVariant **pretvar, in { value[3] = 0; } - ScriptVariant_ToString(varlist[3], buf); + if(FAILED(ScriptVariant_ToString( + varlist[3], + buf, + sizeof(buf), + NULL + ))) + { + goto drawstring_error; + } + font_printf((int)value[0], (int)value[1], (int)value[2], (int)value[3], "%s", buf); return S_OK; @@ -1381,9 +1391,9 @@ HRESULT openbor_drawstring(ScriptVariant **varlist , ScriptVariant **pretvar, in //drawstringtoscreen(screen, x, y, font, string); HRESULT openbor_drawstringtoscreen(ScriptVariant **varlist , ScriptVariant **pretvar, int paramCount) { + char buf[MAX_BUFFER_LEN]; int i; s_screen *scr; - char buf[MAX_BUFFER_LEN]; LONG value[3]; *pretvar = NULL; @@ -1410,7 +1420,16 @@ HRESULT openbor_drawstringtoscreen(ScriptVariant **varlist , ScriptVariant **pre } } - ScriptVariant_ToString(varlist[4], buf); + if(FAILED(ScriptVariant_ToString( + varlist[4], + buf, + sizeof(buf), + NULL + ))) + { + goto drawstring_error; + } + screen_printf(scr, (int)value[0], (int)value[1], (int)value[2], "%s", buf); return S_OK; @@ -1423,7 +1442,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) @@ -1431,8 +1451,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; + } + + printf("%s", string_view.string); return S_OK; drawstring_error: @@ -8122,12 +8151,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; @@ -8428,8 +8458,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; } @@ -10609,7 +10654,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; @@ -10633,7 +10678,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'; @@ -10707,8 +10761,11 @@ HRESULT openbor_getfilestreamargument(ScriptVariant **varlist , ScriptVariant ** } } - if(argument_length >= (size_t)INT_MAX) { - printf("File stream argument exceeds the script string capacity.\n"); + 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; } @@ -10809,12 +10866,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) { @@ -10868,30 +10926,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; @@ -13123,9 +13211,11 @@ HRESULT openbor_gettextobjproperty(ScriptVariant **varlist , ScriptVariant **pre 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]; + char buf[MAX_STR_VAR_LEN]; LONG ltemp; const char *ctotext = "changetextobjproperty(int index, \"property\", value)"; @@ -13180,9 +13270,18 @@ HRESULT openbor_changetextobjproperty(ScriptVariant **varlist , ScriptVariant ** } case _top_text: { - ScriptVariant_ToString(varlist[2], buf); + if(FAILED(ScriptVariant_ToString( + varlist[2], + buf, + sizeof(buf), + NULL + ))) + { + goto changetextobjproperty_error; + } + level->textobjs[ind].text = malloc(MAX_STR_VAR_LEN); - strncpy(level->textobjs[ind].text, buf, MAX_STR_VAR_LEN); + strcpy(level->textobjs[ind].text, buf); break; } case _top_time: @@ -13243,8 +13342,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; } @@ -13253,7 +13367,7 @@ HRESULT openbor_settextobj(ScriptVariant **varlist , ScriptVariant **pretvar, in { LONG ind; LONG X, Y, Z, F, T = 0; - static char buf[MAX_STR_VAR_LEN]; + 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; @@ -13298,7 +13412,15 @@ HRESULT openbor_settextobj(ScriptVariant **varlist , ScriptVariant **pretvar, in { goto settextobj_error; } - ScriptVariant_ToString(varlist[5], buf); + if(FAILED(ScriptVariant_ToString( + varlist[5], + buf, + sizeof(buf), + NULL + ))) + { + goto settextobj_error; + } if(paramCount >= 7 && FAILED(ScriptVariant_IntegerValue(varlist[6], &T))) { goto settextobj_error; @@ -13314,7 +13436,7 @@ HRESULT openbor_settextobj(ScriptVariant **varlist , ScriptVariant **pretvar, in { level->textobjs[ind].text = (char *)malloc(MAX_STR_VAR_LEN); } - strncpy(level->textobjs[ind].text, buf, MAX_STR_VAR_LEN); + strcpy(level->textobjs[ind].text, buf); return S_OK; 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/scriptlib/Instruction.c b/engine/source/scriptlib/Instruction.c index e53732704..212e31803 100644 --- a/engine/source/scriptlib/Instruction.c +++ b/engine/source/scriptlib/Instruction.c @@ -12,11 +12,22 @@ #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) { @@ -28,13 +39,15 @@ void Instruction_InitViaToken(Instruction *pins, OpCode code, Token *pToken ) */ if(code == CONSTSTR) { - Instruction_ConvertConstant(pins); + return Instruction_ConvertConstant(pins); } } else { pins->theToken->theType = END_OF_TOKENS; } + + return S_OK; } void Instruction_InitViaLabel(Instruction *pins, OpCode code, LPCSTR label ) @@ -431,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, @@ -467,7 +493,7 @@ void Instruction_ConvertConstant(Instruction *pins) { pvar = (ScriptVariant *)malloc(sizeof(ScriptVariant)); if (!pvar) { - return; + return E_FAIL; } ScriptVariant_Init(pvar); @@ -505,11 +531,17 @@ void Instruction_ConvertConstant(Instruction *pins) { } else if(pins->OpCode == CONSTSTR) { pvar = (ScriptVariant *)malloc(sizeof(ScriptVariant)); + + if(!pvar) + { + return E_FAIL; + } + ScriptVariant_Init(pvar); if(pins->theToken->theStringLiteralSource) { - ScriptVariant_ParseStringLiteral( + result = ScriptVariant_ParseStringLiteral( pvar, pins->theToken->theStringLiteralSource, pins->theToken->theStringLiteralLength @@ -517,14 +549,16 @@ void Instruction_ConvertConstant(Instruction *pins) { } else { - ScriptVariant_ParseStringConstant(pvar, pins->theToken->theSource); + 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 2d1553983..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. diff --git a/engine/source/scriptlib/Parser.c b/engine/source/scriptlib/Parser.c index 2ef813a91..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; + } } /****************************************************************************** @@ -1877,10 +1899,17 @@ void Parser_Unary_expr(Parser *pparser ) prefixed_value[0] = '!'; memcpy(prefixed_value + 1, value, value_length + 1); ScriptVariant_Clear(pInstruction->theVal); - ScriptVariant_ParseStringConstant( - pInstruction->theVal, - prefixed_value - ); + 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 diff --git a/engine/source/scriptlib/ScriptVariant.c b/engine/source/scriptlib/ScriptVariant.c index 262d8b866..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; @@ -336,11 +340,29 @@ size_t ScriptString_DecodeLiteral( } // find an existing constant before copy -void ScriptVariant_ParseStringConstant(ScriptVariant *var, const 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) @@ -348,12 +370,14 @@ void ScriptVariant_ParseStringConstant(ScriptVariant *var, const 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; } /* @@ -363,7 +387,7 @@ void ScriptVariant_ParseStringConstant(ScriptVariant *var, const CHAR *str) - Convert a length-delimited source literal directly into a runtime string constant without fixed intermediate storage. */ -void ScriptVariant_ParseStringLiteral( +HRESULT ScriptVariant_ParseStringLiteral( ScriptVariant *var, const CHAR *source, size_t source_length @@ -372,23 +396,45 @@ void ScriptVariant_ParseStringLiteral( 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 ); - ScriptVariant_ParseStringConstant(var, decoded); + if(FAILED(ScriptVariant_ParseStringConstant(var, decoded))) + { + free(decoded); + return E_FAIL; + } free(decoded); + + return S_OK; } /* @@ -599,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; } /* @@ -1900,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); @@ -1910,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; } @@ -2708,5 +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 95a90b655..c74946ca6 100644 --- a/engine/source/scriptlib/ScriptVariant.h +++ b/engine/source/scriptlib/ScriptVariant.h @@ -24,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 @@ -48,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 @@ -79,14 +91,15 @@ void ScriptVariant_Init(ScriptVariant *var); void ScriptVariant_Copy(ScriptVariant *svar, ScriptVariant *rightChild ); // faster in some situations void ScriptVariant_ChangeType(ScriptVariant *var, VARTYPE cvt); size_t ScriptString_DecodeLiteral(CHAR *destination, size_t destination_size, const CHAR *source, size_t source_length); -void ScriptVariant_ParseStringConstant(ScriptVariant *var, const CHAR *str); -void ScriptVariant_ParseStringLiteral(ScriptVariant *var, 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 From 40afed1dbe619c879ff8395e4f6ca38c4a657f6c Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Tue, 11 Aug 2026 21:10:33 -0400 Subject: [PATCH 08/11] Unbind script text display --- engine/openbor.c | 27 ++- engine/openbor.h | 2 +- engine/openborscript.c | 131 ++++++++---- engine/source/gamelib/font.c | 296 +++++++++++++++++++++++---- engine/source/gamelib/font.h | 9 +- engine/source/openborscript/string.c | 16 +- engine/source/utils.c | 47 ++++- engine/source/utils.h | 4 +- 8 files changed, 428 insertions(+), 104 deletions(-) diff --git a/engine/openbor.c b/engine/openbor.c index ab5d2c87c..f064c4ad7 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -50300,7 +50300,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) + ); } } } @@ -51062,10 +51069,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; @@ -51076,13 +51083,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) @@ -51099,7 +51103,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); } @@ -51317,9 +51323,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); } diff --git a/engine/openbor.h b/engine/openbor.h index 3350e8162..9fc57d4f4 100644 --- a/engine/openbor.h +++ b/engine/openbor.h @@ -4968,7 +4968,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); diff --git a/engine/openborscript.c b/engine/openborscript.c index 8c28f0c95..5e86ad46e 100644 --- a/engine/openborscript.c +++ b/engine/openborscript.c @@ -1341,7 +1341,8 @@ HRESULT openbor_setsystemvariant(ScriptVariant **varlist , ScriptVariant **pretv //drawstring(x, y, font, string, z); HRESULT openbor_drawstring(ScriptVariant **varlist , ScriptVariant **pretvar, int paramCount) { - char buf[MAX_BUFFER_LEN]; + ScriptVariantStringView string_view; + char conversion_buffer[SCRIPT_VARIANT_CONVERSION_BUFFER_LENGTH]; int i; LONG value[4]; *pretvar = NULL; @@ -1369,17 +1370,24 @@ HRESULT openbor_drawstring(ScriptVariant **varlist , ScriptVariant **pretvar, in { value[3] = 0; } - if(FAILED(ScriptVariant_ToString( + if(FAILED(ScriptVariant_GetStringView( varlist[3], - buf, - sizeof(buf), - NULL + conversion_buffer, + sizeof(conversion_buffer), + &string_view ))) { goto drawstring_error; } - font_printf((int)value[0], (int)value[1], (int)value[2], (int)value[3], "%s", buf); + 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: @@ -1391,7 +1399,8 @@ HRESULT openbor_drawstring(ScriptVariant **varlist , ScriptVariant **pretvar, in //drawstringtoscreen(screen, x, y, font, string); HRESULT openbor_drawstringtoscreen(ScriptVariant **varlist , ScriptVariant **pretvar, int paramCount) { - char buf[MAX_BUFFER_LEN]; + ScriptVariantStringView string_view; + char conversion_buffer[SCRIPT_VARIANT_CONVERSION_BUFFER_LENGTH]; int i; s_screen *scr; LONG value[3]; @@ -1420,17 +1429,24 @@ HRESULT openbor_drawstringtoscreen(ScriptVariant **varlist , ScriptVariant **pre } } - if(FAILED(ScriptVariant_ToString( + if(FAILED(ScriptVariant_GetStringView( varlist[4], - buf, - sizeof(buf), - NULL + conversion_buffer, + sizeof(conversion_buffer), + &string_view ))) { goto drawstring_error; } - screen_printf(scr, (int)value[0], (int)value[1], (int)value[2], "%s", buf); + screen_print_length( + scr, + (int)value[0], + (int)value[1], + (int)value[2], + string_view.string, + string_view.length + ); return S_OK; drawstring_error: @@ -1461,7 +1477,7 @@ HRESULT openbor_log(ScriptVariant **varlist , ScriptVariant **pretvar, int param goto drawstring_error; } - printf("%s", string_view.string); + writeToLogFileLength(string_view.string, string_view.length); return S_OK; drawstring_error: @@ -13168,7 +13184,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: @@ -13209,13 +13229,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; - char buf[MAX_STR_VAR_LEN]; LONG ltemp; const char *ctotext = "changetextobjproperty(int index, \"property\", value)"; @@ -13270,18 +13332,13 @@ HRESULT openbor_changetextobjproperty(ScriptVariant **varlist , ScriptVariant ** } case _top_text: { - if(FAILED(ScriptVariant_ToString( - varlist[2], - buf, - sizeof(buf), - NULL + if(FAILED(openbor_textobj_set_text( + &level->textobjs[ind], + varlist[2] ))) { goto changetextobjproperty_error; } - - level->textobjs[ind].text = malloc(MAX_STR_VAR_LEN); - strcpy(level->textobjs[ind].text, buf); break; } case _top_time: @@ -13367,7 +13424,6 @@ HRESULT openbor_settextobj(ScriptVariant **varlist , ScriptVariant **pretvar, in { LONG ind; LONG X, Y, Z, F, T = 0; - 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; @@ -13412,16 +13468,15 @@ HRESULT openbor_settextobj(ScriptVariant **varlist , ScriptVariant **pretvar, in { goto settextobj_error; } - if(FAILED(ScriptVariant_ToString( - varlist[5], - buf, - sizeof(buf), - NULL - ))) + if(paramCount >= 7 && FAILED(ScriptVariant_IntegerValue(varlist[6], &T))) { goto settextobj_error; } - if(paramCount >= 7 && FAILED(ScriptVariant_IntegerValue(varlist[6], &T))) + + if(FAILED(openbor_textobj_set_text( + &level->textobjs[ind], + varlist[5] + ))) { goto settextobj_error; } @@ -13432,12 +13487,6 @@ HRESULT openbor_settextobj(ScriptVariant **varlist , ScriptVariant **pretvar, in 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); - } - strcpy(level->textobjs[ind].text, buf); - return S_OK; settextobj_error: @@ -14061,7 +14110,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/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/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 - From 511b71fba56a804a8fba4ab8d2804e2e78c9d98e Mon Sep 17 00:00:00 2001 From: DCurrent Date: Wed, 12 Aug 2026 00:14:00 -0400 Subject: [PATCH 09/11] Decode quoted command arguments Treat matching single and double quotes as grouping syntax when parsing sequential command and file-stream arguments. * Remove opening and closing quote delimiters from decoded output. * Allow double-quoted spans to appear within arguments and concatenate with adjacent unquoted text. * Recognize single-quoted spans only at argument boundaries, preserving apostrophes in ordinary words. * Preserve the opposite quote type as literal content within a quoted span. * Allow quoted file-stream arguments to contain whitespace, comment markers, and newlines. * Support empty quoted arguments. * Track source and decoded lengths separately to avoid unnecessary temporary allocations. * Decode file-stream arguments directly into exact-sized script string storage. * Apply the 65,535-character policy to decoded content. * Reject unterminated quotes and insufficient destination capacity cleanly. File streams can now return complete sentences and multi-line paragraphs as individual arguments without retaining their grouping delimiters. --- engine/openbor.c | 248 ++++++++++++++++++++++++++++++++++------- engine/openbor.h | 27 ++++- engine/openborscript.c | 43 ++++--- 3 files changed, 266 insertions(+), 52 deletions(-) diff --git a/engine/openbor.c b/engine/openbor.c index f064c4ad7..36c7d7fc1 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -3395,6 +3395,7 @@ typedef struct s_command_token { const char* text; size_t length; + size_t value_length; } s_command_token; /* @@ -3406,22 +3407,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; @@ -3448,6 +3500,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; } @@ -3455,18 +3508,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; } @@ -3481,61 +3528,163 @@ 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 a non-owning view so the caller can - allocate only the storage required for that argument. + 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. */ -bool command_argument_read( +e_command_argument_read_result command_argument_read( const char* command_line, size_t argument_index, - const char** value, - size_t* length + s_command_argument_view* argument ) { - s_command_token_reader reader; + s_command_token_reader reader = { + .cursor = command_line + }; + s_command_token token; assert(command_line); - assert(value); - assert(length); + assert(argument); - reader.cursor = command_line; + *argument = (s_command_argument_view){0}; while(argument_index) { if(!command_token_reader_next(&reader, &token)) { - *value = NULL; - *length = 0; - - return false; + return reader.unterminated_quote + ? COMMAND_ARGUMENT_READ_INVALID + : COMMAND_ARGUMENT_READ_END; } argument_index--; } if(!command_token_reader_next(&reader, &token)) { - *value = NULL; - *length = 0; - - return false; + return reader.unterminated_quote + ? COMMAND_ARGUMENT_READ_INVALID + : COMMAND_ARGUMENT_READ_END; } - *value = token.text; - *length = token.length; + argument->source = token.text; + argument->source_length = token.length; + argument->length = token.value_length; - return true; + 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); } /* @@ -3591,6 +3740,14 @@ static bool command_argument_reader_initialize( &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; } @@ -3618,11 +3775,19 @@ static bool command_argument_reader_next( 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.length >= sizeof(reader->value)) { + if(token.value_length >= sizeof(reader->value)) { borShutdown( 1, "Command argument exceeds the maximum length of %zu characters.\n", @@ -3632,8 +3797,15 @@ static bool command_argument_reader_next( return false; } - memcpy(reader->value, token.text, token.length); - reader->value[token.length] = '\0'; + if(!command_token_copy_value( + &token, + reader->value, + sizeof(reader->value) + )) { + *value = NULL; + return false; + } + *value = reader->value; return true; diff --git a/engine/openbor.h b/engine/openbor.h index 9fc57d4f4..030563a35 100644 --- a/engine/openbor.h +++ b/engine/openbor.h @@ -4410,7 +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); -bool command_argument_read (const char* command_line, size_t argument_index, const char** value, size_t* length); + +/* +- 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); diff --git a/engine/openborscript.c b/engine/openborscript.c index 5e86ad46e..7a1163aee 100644 --- a/engine/openborscript.c +++ b/engine/openborscript.c @@ -10716,11 +10716,13 @@ HRESULT openbor_getfilestreamline(ScriptVariant **varlist , ScriptVariant **pret - 2026-08-11 - - Read one requested file-stream argument sequentially and - copy only that argument into exact-sized script storage. - Numeric conversions reuse the same temporary script string. + 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) { - const char *argument_text = ""; + 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; @@ -10766,15 +10768,19 @@ HRESULT openbor_getfilestreamargument(ScriptVariant **varlist , ScriptVariant ** } if(argument >= 0) { - if(!command_argument_read( - filestreams[filestreamindex].buf - + filestreams[filestreamindex].pos, - (size_t)argument, - &argument_text, - &argument_length - )) { - argument_text = ""; + 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) { @@ -10788,8 +10794,19 @@ HRESULT openbor_getfilestreamargument(ScriptVariant **varlist , ScriptVariant ** ScriptVariant_ChangeType(*pretvar, VT_STR); (*pretvar)->strVal = StrCache_Pop((int)argument_length); converted_text = StrCache_Get((*pretvar)->strVal); - memcpy(converted_text, argument_text, argument_length); - converted_text[argument_length] = '\0'; + + 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); From 2db4ae46957f0443cb4230b9addde30b49d04d01 Mon Sep 17 00:00:00 2001 From: DCurrent Date: Wed, 12 Aug 2026 10:49:54 -0400 Subject: [PATCH 10/11] Verify @cmd quotation clousure and reject malformed arguments with graceful error. --- engine/openbor.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/engine/openbor.c b/engine/openbor.c index 36c7d7fc1..9f42ecf3b 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -20467,6 +20467,19 @@ s_model *load_cached_model(char *name, char *owner, char unload) } } + 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); if(no_cmd_compatible) { From 9bdd047add5f2eb8e6450dc3e156266be59dfe53 Mon Sep 17 00:00:00 2001 From: DCurrent Date: Wed, 12 Aug 2026 14:51:06 -0400 Subject: [PATCH 11/11] Guard @cmd compatibility merging Prevent `nocmdcompatible` optimization from corrupting generated animation scripts when same-frame `@cmd` calls are separated by other script content. * Track whether the preceding generated section is eligible for `@cmd` merging. * Invalidate merge eligibility after inline scripts, animation changes, and frame changes. * Verify the exact generated `return;` and frame-closing suffix before removing either. * Remove the complete suffix atomically only after successful validation. * Open a fresh frame condition when the preceding output is not safely mergeable. * Preserve normal merging of adjacent same-frame `@cmd` calls. This prevents intervening `@script` content or unexpected generated output from being truncated while retaining the existing compatibility optimization. --- engine/openbor.c | 97 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 8 deletions(-) diff --git a/engine/openbor.c b/engine/openbor.c index 9f42ecf3b..328648652 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -1047,6 +1047,54 @@ int buffer_append(char **buffer, const char *str, size_t n, size_t *bufferlen, s 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"; @@ -15393,6 +15441,7 @@ 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 tempInt = 0; int framecount = 0; @@ -17298,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. @@ -20152,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: @@ -20326,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); @@ -20367,6 +20420,7 @@ s_model *load_cached_model(char *name, char *owner, char unload) { 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 @@ -20383,6 +20437,7 @@ 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; @@ -20423,19 +20478,42 @@ 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); - } buffer_append(&scriptbuf, call_indent_text, 0xffffff, &sbsize, &scriptlen); buffer_append( &scriptbuf, @@ -20465,6 +20543,8 @@ s_model *load_cached_model(char *name, char *owner, char unload) ); first_command_argument = false; } + + command_emitted = true; } if(command_token_reader.unterminated_quote) @@ -20488,6 +20568,7 @@ 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: