diff --git a/game_patch/CMakeLists.txt b/game_patch/CMakeLists.txt index 4c7671c4..6f633708 100644 --- a/game_patch/CMakeLists.txt +++ b/game_patch/CMakeLists.txt @@ -169,6 +169,8 @@ set(SRCS misc/camera.cpp misc/ui.cpp misc/game.cpp + misc/dashoptions.h + misc/dashoptions.cpp sound/sound.cpp sound/sound.h sound/sound_ds.cpp diff --git a/game_patch/hud/multi_scoreboard.cpp b/game_patch/hud/multi_scoreboard.cpp index 86cff2a0..dffcc450 100644 --- a/game_patch/hud/multi_scoreboard.cpp +++ b/game_patch/hud/multi_scoreboard.cpp @@ -4,6 +4,7 @@ #include #include "multi_scoreboard.h" #include "../multi/multi.h" +#include "../misc/dashoptions.h" #include "../rf/gr/gr.h" #include "../rf/gr/gr_font.h" #include "../rf/multi.h" @@ -46,7 +47,10 @@ int draw_scoreboard_header(int x, int y, int w, rf::NetGameType game_type, bool int cur_y = y; if (!dry_run) { rf::gr::set_color(0xFF, 0xFF, 0xFF, 0xFF); - static int score_rflogo_bm = rf::bm::load("score_rflogo.tga", -1, false); + // load custom scoreboard logo from dashoptions.tbl if specified + static int score_rflogo_bm = rf::bm::load(g_dash_options_config.is_option_loaded(DashOptionID::ScoreboardLogo) + ? g_dash_options_config.scoreboard_logo.value().c_str() + : "score_rflogo.tga", -1, false); rf::gr::bitmap(score_rflogo_bm, x_center - 170, cur_y); } cur_y += 30; diff --git a/game_patch/misc/dashoptions.cpp b/game_patch/misc/dashoptions.cpp new file mode 100644 index 00000000..f79a55c3 --- /dev/null +++ b/game_patch/misc/dashoptions.cpp @@ -0,0 +1,606 @@ +#include "dashoptions.h" +#include +#include +#include +#include +#include +#include "../os/console.h" +#include "../rf/file/file.h" +#include "../rf/gr/gr.h" +#include "../rf/sound/sound.h" +#include "../rf/geometry.h" +#include "../rf/misc.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +DashOptionsConfig g_dash_options_config; + +void mark_option_loaded(DashOptionID id) +{ + g_dash_options_config.options_loaded[static_cast(id)] = true; +} + +namespace dashopt +{ +std::unique_ptr dashoptions_file; + +// trim leading and trailing whitespace +std::string trim(const std::string& str) +{ + auto start = std::find_if_not(str.begin(), str.end(), [](unsigned char ch) { return std::isspace(ch); }); + auto end = std::find_if_not(str.rbegin(), str.rend(), [](unsigned char ch) { return std::isspace(ch); }).base(); + + if (start >= end) { + return ""; // Return empty string if nothing remains after trimming + } + return std::string(start, end); +} + +// for values provided in quotes, dump the quotes before storing the values +std::optional extract_quoted_value(const std::string& value) +{ + std::string trimmed_value = trim(value); + + // check if trimmed value starts and ends with quotes, if so, extract the content inside them + if (trimmed_value.size() >= 2 && trimmed_value.front() == '"' && trimmed_value.back() == '"') { + std::string extracted_value = + trimmed_value.substr(1, trimmed_value.size() - 2); + return extracted_value; + } + + // if not wrapped in quotes, assume valid + xlog::debug("String value is not enclosed in quotes, accepting it anyway: '{}'", trimmed_value); + return trimmed_value; +} + +void open_url(const std::string& url) +{ + try { + if (url.empty()) { + xlog::error("URL is empty"); + return; + } + xlog::info("Opening URL: {}", url); + HINSTANCE result = ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOW); + if (reinterpret_cast(result) <= 32) { + xlog::error("Failed to open URL. Error code: {}", reinterpret_cast(result)); + } + } + catch (const std::exception& ex) { + xlog::error("Exception occurred while trying to open URL: {}", ex.what()); + } +} + +//consolidated logic for parsing colors +std::tuple extract_color_components(uint32_t color) +{ + return std::make_tuple((color >> 24) & 0xFF, // red + (color >> 16) & 0xFF, // green + (color >> 8) & 0xFF, // blue + color & 0xFF // alpha + ); +} + +CallHook fpgun_ar_ammo_digit_color_hook{ + 0x004ABC03, + [](int red, int green, int blue, int alpha) { + std::tie(red, green, blue, alpha) = extract_color_components(g_dash_options_config.ar_ammo_color.value()); + fpgun_ar_ammo_digit_color_hook.call_target(red, green, blue, alpha); + } +}; + +CallHook precision_rifle_scope_color_hook{ + 0x004AC850, [](int red, int green, int blue, int alpha) { + std::tie(red, green, blue, alpha) = extract_color_components(g_dash_options_config.pr_scope_color.value()); + precision_rifle_scope_color_hook.call_target(red, green, blue, alpha); + } +}; + +CallHook sniper_rifle_scope_color_hook{ + 0x004AC458, [](int red, int green, int blue, int alpha) { + std::tie(red, green, blue, alpha) = extract_color_components(g_dash_options_config.sr_scope_color.value()); + sniper_rifle_scope_color_hook.call_target(red, green, blue, alpha); + } +}; + +CallHook rail_gun_fire_glow_hook{ + 0x004AC00E, [](int red, int green, int blue, int alpha) { + std::tie(red, green, blue, alpha) = extract_color_components(g_dash_options_config.rail_glow_color.value()); + rail_gun_fire_glow_hook.call_target(red, green, blue, alpha); + } +}; + +CallHook rail_gun_fire_flash_hook{ + 0x004AC04A, [](int red, int green, int blue, int alpha) { + std::tie(red, green, blue, std::ignore) = + extract_color_components(g_dash_options_config.rail_flash_color.value()); + rail_gun_fire_flash_hook.call_target(red, green, blue, alpha); + } +}; + +// consolidated logic for handling geo mesh changes +int handle_geomod_shape_create(const char* filename, const std::optional& config_value, + CallHook& hook) +{ + std::string original_filename{filename}; + std::string modded_filename = config_value.value_or(original_filename); + return hook.call_target(modded_filename.c_str()); +} + +// Set default geo mesh +CallHook default_geomod_shape_create_hook{ + 0x004374CF, [](const char* filename) -> int { + return handle_geomod_shape_create(filename, + g_dash_options_config.geomodmesh_default, default_geomod_shape_create_hook); + } +}; + +// Set driller double geo mesh +CallHook driller_double_geomod_shape_create_hook{ + 0x004374D9, [](const char* filename) -> int { + return handle_geomod_shape_create(filename, + g_dash_options_config.geomodmesh_driller_double, driller_double_geomod_shape_create_hook); + } +}; + +// Set driller single geo mesh +CallHook driller_single_geomod_shape_create_hook{ + 0x004374E3, [](const char* filename) -> int { + return handle_geomod_shape_create(filename, + g_dash_options_config.geomodmesh_driller_single, driller_single_geomod_shape_create_hook); + } +}; + +// Set apc geo mesh +CallHook apc_geomod_shape_create_hook{ + 0x004374ED, [](const char* filename) -> int { + return handle_geomod_shape_create(filename, + g_dash_options_config.geomodmesh_apc, apc_geomod_shape_create_hook); + } +}; + +void apply_geomod_mesh_patch() +{ + // array of geomod mesh options + std::array, 4> geomod_mesh_hooks = { + {{DashOptionID::GeomodMesh_Default, [] { default_geomod_shape_create_hook.install(); }}, + {DashOptionID::GeomodMesh_DrillerDouble, [] { driller_double_geomod_shape_create_hook.install(); }}, + {DashOptionID::GeomodMesh_DrillerSingle, [] { driller_single_geomod_shape_create_hook.install(); }}, + {DashOptionID::GeomodMesh_APC, [] { apc_geomod_shape_create_hook.install(); }} + }}; + + bool any_option_loaded = false; + + // install only the hooks for the ones that were set + for (const auto& [option_id, install_fn] : geomod_mesh_hooks) { + if (g_dash_options_config.is_option_loaded(option_id)) { + install_fn(); + any_option_loaded = true; + } + } + + // if any one was set, apply the necessary patches + if (any_option_loaded) { + AsmWriter(0x00437543).call(0x004ECED0); // Replace the call to load v3d instead of embedded + rf::geomod_shape_init(); // Reinitialize geomod shapes + } +} + +// consolidated logic for handling geomod smoke emitter overrides +int handle_geomod_emitter_change(const char* emitter_name, + const std::optional& new_emitter_name_opt, CallHook& hook) { + std::string original_emitter_name{emitter_name}; + std::string new_emitter_name = new_emitter_name_opt.value_or(original_emitter_name); + return hook.call_target(new_emitter_name.c_str()); +} + +// Override default geomod smoke emitter +CallHook default_geomod_emitter_get_index_hook{ + 0x00437150, [](const char* emitter_name) -> int { + return handle_geomod_emitter_change(emitter_name, + g_dash_options_config.geomodemitter_default, default_geomod_emitter_get_index_hook); + } +}; + +// Override driller geomod smoke emitter +CallHook driller_geomod_emitter_get_index_hook{ + 0x0043715F, [](const char* emitter_name) -> int { + return handle_geomod_emitter_change(emitter_name, + g_dash_options_config.geomodemitter_driller, driller_geomod_emitter_get_index_hook); + } +}; + +// replace ice geomod region texture +CallHook ice_geo_crater_bm_load_hook { + { + 0x004673B5, // chunks + 0x00466BEF, // crater + }, + [](const char* filename, int path_id, bool generate_mipmaps) -> int { + std::string original_filename{filename}; + std::string new_filename = g_dash_options_config.geomodtexture_ice.value_or(original_filename); + return ice_geo_crater_bm_load_hook.call_target(new_filename.c_str(), path_id, generate_mipmaps); + } +}; + +// Consolidated logic for handling level filename overrides +inline void handle_level_name_change(const char* level_name, + const std::optional& new_level_name_opt, CallHook& hook) { + std::string new_level_name = new_level_name_opt.value_or(level_name); + hook.call_target(new_level_name.c_str()); +} + +// Override first level filename for new game menu +CallHook first_load_level_hook{ + 0x00443B15, [](const char* level_name) { + handle_level_name_change(level_name, g_dash_options_config.first_level_filename, first_load_level_hook); + } +}; + +// Override training level filename for new game menu +CallHook training_load_level_hook{ + 0x00443A85, [](const char* level_name) { + handle_level_name_change(level_name, g_dash_options_config.training_level_filename, training_load_level_hook); + } +}; + +// Implement demo_extras_summoner_trailer_click using FunHook +FunHook extras_summoner_trailer_click_hook{ + 0x0043EC80, [](int x, int y) { + xlog::debug("Summoner trailer button clicked"); + int action = g_dash_options_config.sumtrailer_button_action.value_or(0); + switch (action) { + case 1: + // open URL + if (g_dash_options_config.sumtrailer_button_url) { + const std::string& url = *g_dash_options_config.sumtrailer_button_url; + open_url(url); + } + break; + case 2: + // disable button + break; + default: + // play bink video, is case 0 but also default + std::string trailer_path = g_dash_options_config.sumtrailer_button_bik_filename.value_or("sumtrailer.bik"); + xlog::debug("Playing BIK file: {}", trailer_path); + rf::snd_pause(true); + rf::bink_play(trailer_path.c_str()); + rf::snd_pause(false); + break; + } + } +}; + +void handle_summoner_trailer_button() +{ + if (int action = g_dash_options_config.sumtrailer_button_action.value_or(-1); action != -1) { + //xlog::debug("Action ID: {}", g_dash_options_config.sumtrailer_button_action.value_or(-1)); + if (action == 3) { + // action 3 means remove the button + AsmWriter(0x0043EE14).nop(5); + } + else { + // otherwise, install the hook + extras_summoner_trailer_click_hook.install(); + } + } +} + +void apply_dashoptions_patches() +{ + xlog::debug("Applying Dash Options patches"); + // avoid unnecessary hooks by hooking only if corresponding options are specified + + if (g_dash_options_config.use_stock_game_players_config.value_or(false)) { + AsmWriter(0x004A8F99).jmp(0x004A9010); + AsmWriter(0x004A8DCC).jmp(0x004A8E53); + } + + if (g_dash_options_config.is_option_loaded(DashOptionID::AssaultRifleAmmoColor)) { + fpgun_ar_ammo_digit_color_hook.install(); + } + + if (g_dash_options_config.is_option_loaded(DashOptionID::PrecisionRifleScopeColor)) { + precision_rifle_scope_color_hook.install(); + } + + if (g_dash_options_config.is_option_loaded(DashOptionID::SniperRifleScopeColor)) { + sniper_rifle_scope_color_hook.install(); + } + + if (g_dash_options_config.is_option_loaded(DashOptionID::RailDriverFireGlowColor)) { + rail_gun_fire_glow_hook.install(); + } + + if (g_dash_options_config.is_option_loaded(DashOptionID::RailDriverFireFlashColor)) { + rail_gun_fire_flash_hook.install(); + } + + // whether should apply is determined in helper function + apply_geomod_mesh_patch(); + + if (g_dash_options_config.is_option_loaded(DashOptionID::GeomodEmitter_Default)) { + default_geomod_emitter_get_index_hook.install(); + } + + if (g_dash_options_config.is_option_loaded(DashOptionID::GeomodEmitter_Driller)) { + driller_geomod_emitter_get_index_hook.install(); + } + + if (g_dash_options_config.is_option_loaded(DashOptionID::GeomodTexture_Ice)) { + ice_geo_crater_bm_load_hook.install(); + } + + if (g_dash_options_config.is_option_loaded(DashOptionID::FirstLevelFilename)) { + first_load_level_hook.install(); + } + + if (g_dash_options_config.is_option_loaded(DashOptionID::TrainingLevelFilename)) { + training_load_level_hook.install(); + } + + if (g_dash_options_config.disable_multiplayer_button.value_or(false)) { + AsmWriter(0x0044391F).nop(5); // multi + } + + if (g_dash_options_config.disable_singleplayer_buttons.value_or(false)) { + AsmWriter(0x00443906).nop(5); // save + AsmWriter(0x004438ED).nop(5); // load + AsmWriter(0x004438D4).nop(5); // new game + } + + if (g_dash_options_config.is_option_loaded(DashOptionID::SumTrailerButtonAction)) { + handle_summoner_trailer_button(); + } +} + +template +void set_option(DashOptionID option_id, std::optional& option_field, const std::string& option_value) +{ + try { + if constexpr (std::is_same_v) { + option_field = extract_quoted_value(option_value).value_or(option_value); + } + else if constexpr (std::is_same_v) { + // hex color values + option_field = std::stoul(extract_quoted_value(option_value).value_or(option_value), nullptr, 16); + } + else if constexpr (std::is_same_v) { + option_field = std::stof(option_value); + } + else if constexpr (std::is_same_v) { + option_field = std::stoi(option_value); + } + else if constexpr (std::is_same_v) { + // accept every reasonable representation of "true" for a boolean + option_field = + (option_value == "1" || option_value == "true" || option_value == "True" || option_value == "TRUE"); + } + + // mark option as loaded + mark_option_loaded(option_id); + xlog::debug("Parsed value has been saved: {}", option_field.value()); + } + catch (const std::exception& e) { + xlog::debug("Failed to parse value for option: {}. Error: {}", option_value, e.what()); + } +} + +// identify custom options and parse where found +void process_dashoption_line(const std::string& option_name, const std::string& option_value) +{ + static const std::unordered_map option_map = { + {"$Scoreboard Logo", DashOptionID::ScoreboardLogo}, + {"$Default Geomod Mesh", DashOptionID::GeomodMesh_Default}, + {"$Driller Double Geomod Mesh", DashOptionID::GeomodMesh_DrillerDouble}, + {"$Driller Single Geomod Mesh", DashOptionID::GeomodMesh_DrillerSingle}, + {"$APC Geomod Mesh", DashOptionID::GeomodMesh_APC}, + {"$Default Geomod Smoke Emitter", DashOptionID::GeomodEmitter_Default}, + {"$Driller Geomod Smoke Emitter", DashOptionID::GeomodEmitter_Driller}, + {"$Ice Geomod Texture", DashOptionID::GeomodTexture_Ice}, + {"$First Level Filename", DashOptionID::FirstLevelFilename}, + {"$Training Level Filename", DashOptionID::TrainingLevelFilename}, + {"$Disable Multiplayer Button", DashOptionID::DisableMultiplayerButton}, + {"$Disable Singleplayer Buttons", DashOptionID::DisableSingleplayerButtons}, + {"$Use Base Game Players Config", DashOptionID::UseStockPlayersConfig}, + {"$Ignore Swap Assault Rifle Controls", DashOptionID::IgnoreSwapAssaultRifleControls}, + {"$Ignore Swap Grenade Controls", DashOptionID::IgnoreSwapGrenadeControls}, + {"$Assault Rifle Ammo Counter Color", DashOptionID::AssaultRifleAmmoColor}, + {"$Precision Rifle Scope Color", DashOptionID::PrecisionRifleScopeColor}, + {"$Sniper Rifle Scope Color", DashOptionID::SniperRifleScopeColor}, + {"$Rail Driver Fire Glow Color", DashOptionID::RailDriverFireGlowColor}, + {"$Rail Driver Fire Flash Color", DashOptionID::RailDriverFireFlashColor}, + {"$Summoner Trailer Button Action", DashOptionID::SumTrailerButtonAction}, + {"+Summoner Trailer Button URL", DashOptionID::SumTrailerButtonURL}, + {"+Summoner Trailer Button Bink Filename", DashOptionID::SumTrailerButtonBikFile}}; + + // save option values for options that are found + auto it = option_map.find(option_name); + if (it != option_map.end()) { + switch (it->second) { + case DashOptionID::ScoreboardLogo: + set_option(it->second, g_dash_options_config.scoreboard_logo, option_value); + break; + case DashOptionID::GeomodMesh_Default: + set_option(it->second, g_dash_options_config.geomodmesh_default, option_value); + break; + case DashOptionID::GeomodMesh_DrillerDouble: + set_option(it->second, g_dash_options_config.geomodmesh_driller_double, option_value); + break; + case DashOptionID::GeomodMesh_DrillerSingle: + set_option(it->second, g_dash_options_config.geomodmesh_driller_single, option_value); + break; + case DashOptionID::GeomodMesh_APC: + set_option(it->second, g_dash_options_config.geomodmesh_apc, option_value); + break; + case DashOptionID::GeomodEmitter_Default: + set_option(it->second, g_dash_options_config.geomodemitter_default, option_value); + break; + case DashOptionID::GeomodEmitter_Driller: + set_option(it->second, g_dash_options_config.geomodemitter_driller, option_value); + break; + case DashOptionID::GeomodTexture_Ice: + set_option(it->second, g_dash_options_config.geomodtexture_ice, option_value); + break; + case DashOptionID::FirstLevelFilename: + set_option(it->second, g_dash_options_config.first_level_filename, option_value); + break; + case DashOptionID::TrainingLevelFilename: + set_option(it->second, g_dash_options_config.training_level_filename, option_value); + break; + case DashOptionID::DisableMultiplayerButton: + set_option(it->second, g_dash_options_config.disable_multiplayer_button, option_value); + break; + case DashOptionID::DisableSingleplayerButtons: + set_option(it->second, g_dash_options_config.disable_singleplayer_buttons, option_value); + break; + case DashOptionID::UseStockPlayersConfig: + set_option(it->second, g_dash_options_config.use_stock_game_players_config, option_value); + break; + case DashOptionID::IgnoreSwapAssaultRifleControls: + set_option(it->second, g_dash_options_config.ignore_swap_assault_rifle_controls, option_value); + break; + case DashOptionID::IgnoreSwapGrenadeControls: + set_option(it->second, g_dash_options_config.ignore_swap_grenade_controls, option_value); + break; + case DashOptionID::AssaultRifleAmmoColor: + set_option(it->second, g_dash_options_config.ar_ammo_color, option_value); + break; + case DashOptionID::PrecisionRifleScopeColor: + set_option(it->second, g_dash_options_config.pr_scope_color, option_value); + break; + case DashOptionID::SniperRifleScopeColor: + set_option(it->second, g_dash_options_config.sr_scope_color, option_value); + break; + case DashOptionID::RailDriverFireGlowColor: + set_option(it->second, g_dash_options_config.rail_glow_color, option_value); + break; + case DashOptionID::RailDriverFireFlashColor: + set_option(it->second, g_dash_options_config.rail_flash_color, option_value); + break; + case DashOptionID::SumTrailerButtonAction: + set_option(it->second, g_dash_options_config.sumtrailer_button_action, option_value); + break; + case DashOptionID::SumTrailerButtonURL: + set_option(it->second, g_dash_options_config.sumtrailer_button_url, option_value); + break; + case DashOptionID::SumTrailerButtonBikFile: + set_option(it->second, g_dash_options_config.sumtrailer_button_bik_filename, option_value); + break; + default: + xlog::debug("Unrecognized DashOptionID: {}", it->first); + } + } + else { + xlog::warn("Ignoring unsupported option: {}", option_name); + } +} + +bool open_file(const std::string& file_path) +{ + dashoptions_file = std::make_unique(); + if (dashoptions_file->open(file_path.c_str()) != 0) { + xlog::debug("Failed to open {}", file_path); + return false; + } + xlog::debug("Successfully opened {}", file_path); + return true; +} + +void close_file() +{ + if (dashoptions_file) { + xlog::debug("Closing file."); + dashoptions_file->close(); + dashoptions_file.reset(); + + // after parsing is done, apply patches + apply_dashoptions_patches(); + } +} + +void parse() +{ + std::string line; + bool in_options_section = false; // track section, eventually this should be enum and support multiple sections + + xlog::debug("Start parsing dashoptions.tbl"); + + while (true) { + std::string buffer(2048, '\0'); // handle lines up to 2048 bytes, should be plenty + int bytes_read = dashoptions_file->read(&buffer[0], buffer.size() - 1); + + if (bytes_read <= 0) { + xlog::debug("End of file or read error in dashoptions.tbl."); + break; + } + + buffer.resize(bytes_read); // trim unused space if fewer bytes were read + std::istringstream file_stream(buffer); + + while (std::getline(file_stream, line)) { + line = trim(line); + xlog::debug("Parsing line: '{}'", line); + + // could be expanded to support multiple sections + if (line == "#General") { + xlog::debug("Entering General section"); + in_options_section = true; + continue; + } + else if (line == "#End") { + xlog::debug("Exiting General section"); + in_options_section = false; + break; // stop, reached the end of section + } + + // skip anything outside the options section + if (!in_options_section) { + xlog::debug("Skipping line outside of General section"); + continue; + } + + // skip empty lines and comments + if (line.empty() || line.find("//") == 0) { + xlog::debug("Skipping empty or comment line"); + continue; + } + + // valid option lines start with $ or + and contain a delimiter : + if ((line[0] != '$' && line[0] != '+') || line.find(':') == std::string::npos) { + xlog::debug("Skipping malformed line: '{}'", line); + continue; + } + + // parse valid options lines + auto delimiter_pos = line.find(':'); + std::string option_name = trim(line.substr(0, delimiter_pos)); + std::string option_value = trim(line.substr(delimiter_pos + 1)); + + process_dashoption_line(option_name, option_value); + } + } +} + +void load_dashoptions_config() +{ + xlog::debug("Mod launched, attempting to load Dash Options configuration"); + + if (!open_file("dashoptions.tbl")) { + return; + } + + parse(); + close_file(); + + rf::console::print("Dash Options configuration loaded"); +} +} diff --git a/game_patch/misc/dashoptions.h b/game_patch/misc/dashoptions.h new file mode 100644 index 00000000..fddb6bf7 --- /dev/null +++ b/game_patch/misc/dashoptions.h @@ -0,0 +1,93 @@ +#pragma once + +#include +#include +#include +#include + +enum class DashOptionID +{ + ScoreboardLogo, + GeomodMesh_Default, + GeomodMesh_DrillerDouble, + GeomodMesh_DrillerSingle, + GeomodMesh_APC, + GeomodEmitter_Default, + GeomodEmitter_Driller, + GeomodTexture_Ice, + FirstLevelFilename, + TrainingLevelFilename, + DisableMultiplayerButton, + DisableSingleplayerButtons, + UseStockPlayersConfig, + IgnoreSwapAssaultRifleControls, + IgnoreSwapGrenadeControls, + AssaultRifleAmmoColor, + PrecisionRifleScopeColor, + SniperRifleScopeColor, + RailDriverFireGlowColor, + RailDriverFireFlashColor, + SumTrailerButtonAction, + SumTrailerButtonURL, + SumTrailerButtonBikFile, + _optioncount // dummy option for determining total num of options +}; + +// convert enum to its underlying type +constexpr std::size_t to_index(DashOptionID option_id) +{ + return static_cast(option_id); +} + +// total number of options in DashOptionID +constexpr std::size_t option_count = to_index(DashOptionID::_optioncount) + 1; + +struct DashOptionsConfig +{ + //std::optional float_something; // template for float + //std::optional int_something; // template for int + + //core options + std::optional scoreboard_logo; + std::optional geomodmesh_default; + std::optional geomodmesh_driller_double; + std::optional geomodmesh_driller_single; + std::optional geomodmesh_apc; + std::optional geomodemitter_default; + std::optional geomodemitter_driller; + std::optional geomodtexture_ice; + std::optional first_level_filename; + std::optional training_level_filename; + std::optional disable_multiplayer_button; + std::optional disable_singleplayer_buttons; + std::optional use_stock_game_players_config; + std::optional ignore_swap_assault_rifle_controls; + std::optional ignore_swap_grenade_controls; + std::optional ar_ammo_color; + std::optional pr_scope_color; + std::optional sr_scope_color; + std::optional rail_glow_color; + std::optional rail_flash_color; + std::optional sumtrailer_button_action; + + //extended options + //from sumtrailer_button_action + std::optional sumtrailer_button_url; + std::optional sumtrailer_button_bik_filename; + + // track core options that are loaded + std::array options_loaded = {}; + + // check if specific core option is loaded + bool is_option_loaded(DashOptionID option_id) const + { + return options_loaded[to_index(option_id)]; + } +}; + +extern DashOptionsConfig g_dash_options_config; + +namespace dashopt +{ +void load_dashoptions_config(); +} diff --git a/game_patch/misc/misc.cpp b/game_patch/misc/misc.cpp index 8fb5f446..b21063bf 100644 --- a/game_patch/misc/misc.cpp +++ b/game_patch/misc/misc.cpp @@ -8,6 +8,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include "dashoptions.h" #include "misc.h" #include "../sound/sound.h" #include "../os/console.h" @@ -18,6 +24,7 @@ #include "../rf/gameseq.h" #include "../rf/os/os.h" #include "../rf/misc.h" +#include "../rf/parse.h" #include "../rf/vmesh.h" #include "../rf/level.h" #include "../rf/file/file.h" @@ -396,6 +403,17 @@ CallHook level_init_pre_console_output_hook{ }, }; +CodeInjection all_table_files_loaded_injection{ + 0x004B249E, + []() { + // after all other tbl files have been loaded, load dashoptions.tbl and parse it + // only if a TC mod is loaded + if (rf::mod_param.found()) { + dashopt::load_dashoptions_config(); + } + } +}; + void misc_init() { // Window title (client and server) @@ -503,6 +521,9 @@ void misc_init() // Add level name to "-- Level Initializing --" message level_init_pre_console_output_hook.install(); + // Load dashoptionstbl + all_table_files_loaded_injection.install(); + // Apply patches from other files apply_main_menu_patches(); apply_save_restore_patches(); diff --git a/game_patch/misc/player.cpp b/game_patch/misc/player.cpp index 59f77df3..bf16dd5f 100644 --- a/game_patch/misc/player.cpp +++ b/game_patch/misc/player.cpp @@ -8,8 +8,10 @@ #include "../rf/weapon.h" #include "../rf/hud.h" #include "../rf/input.h" +#include "../rf/os/os.h" #include "../os/console.h" #include "../main/main.h" +#include "../misc/dashoptions.h" #include "../multi/multi.h" #include "../hud/multi_spectate.h" #include @@ -83,10 +85,14 @@ bool should_swap_weapon_alt_fire(rf::Player* player) return false; } - if (g_game_config.swap_assault_rifle_controls && entity->ai.current_primary_weapon == rf::assault_rifle_weapon_type) + if (g_game_config.swap_assault_rifle_controls && + !g_dash_options_config.ignore_swap_assault_rifle_controls && + entity->ai.current_primary_weapon == rf::assault_rifle_weapon_type) return true; - if (g_game_config.swap_grenade_controls && entity->ai.current_primary_weapon == rf::grenade_weapon_type) + if (g_game_config.swap_grenade_controls && + !g_dash_options_config.ignore_swap_grenade_controls && + entity->ai.current_primary_weapon == rf::grenade_weapon_type) return true; return false; @@ -148,6 +154,10 @@ ConsoleCommand2 swap_assault_rifle_controls_cmd{ g_game_config.save(); rf::console::print("Swap assault rifle controls: {}", g_game_config.swap_assault_rifle_controls ? "enabled" : "disabled"); + if (g_dash_options_config.ignore_swap_assault_rifle_controls) { + rf::console::print("Note: This setting is disabled in the {} mod and will have no effect.", + rf::mod_param.get_arg()); + } }, "Swap Assault Rifle controls", }; @@ -159,6 +169,10 @@ ConsoleCommand2 swap_grenade_controls_cmd{ g_game_config.save(); rf::console::print("Swap grenade controls: {}", g_game_config.swap_grenade_controls ? "enabled" : "disabled"); + if (g_dash_options_config.ignore_swap_grenade_controls) { + rf::console::print("Note: This setting is disabled in the {} mod and will have no effect.", + rf::mod_param.get_arg()); + } }, "Swap grenade controls", }; diff --git a/game_patch/rf/misc.h b/game_patch/rf/misc.h index 6472ac7f..f50d011a 100644 --- a/game_patch/rf/misc.h +++ b/game_patch/rf/misc.h @@ -10,4 +10,8 @@ namespace rf static auto& default_player_weapon = addr_as_ref(0x007C7600); static auto& get_file_checksum = addr_as_ref(0x00436630); -} + static auto& geomod_shape_init = addr_as_ref(0x004374C0); + static auto& geomod_shape_create = addr_as_ref(0x00437500); + static auto& geomod_shape_shutdown = addr_as_ref(0x00437460); + static auto& bink_play = addr_as_ref(0x00520A90); + } diff --git a/resources/CMakeLists.txt b/resources/CMakeLists.txt index 66badfe8..a1e82b8d 100644 --- a/resources/CMakeLists.txt +++ b/resources/CMakeLists.txt @@ -36,6 +36,7 @@ add_packfile(dashfaction.vpp images/ammo_bar_power_1.tga images/ammo_signal_green_1.tga images/ammo_signal_red_1.tga + images/assault_digits.vbm images/bullet_icon_1.tga images/bullet_icon_50cal_1.tga images/bullet_icon_556_1.tga diff --git a/resources/images/assault_digits.vbm b/resources/images/assault_digits.vbm new file mode 100644 index 00000000..9c81e57b Binary files /dev/null and b/resources/images/assault_digits.vbm differ