From c8011f57b9900c350c7f458d869a4d070c10227f Mon Sep 17 00:00:00 2001 From: Goober Date: Mon, 9 Sep 2024 00:24:18 -0230 Subject: [PATCH 1/6] Add tbl file allow list, allow loading of list members from user_maps, populate with 7 tbl files that should be safe to mod without cheat potential --- docs/CHANGELOG.md | 1 + game_patch/misc/vpackfile.cpp | 31 +++++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1ec2594d..4e07ba8f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -42,6 +42,7 @@ Version 1.9.0 (not released yet) - Add Kill Reward settings for dedicated servers - Do not load unnecessary VPPs in dedicated server mode - Add level filename to "Level Initializing" console message +- Allow clientside mods to edit table files that can't be used to cheat (strings, hud, hud_personas, personas, credits, endgame, ponr) Version 1.8.0 (released 2022-09-17) ----------------------------------- diff --git a/game_patch/misc/vpackfile.cpp b/game_patch/misc/vpackfile.cpp index 2b93ab15..af0044d2 100644 --- a/game_patch/misc/vpackfile.cpp +++ b/game_patch/misc/vpackfile.cpp @@ -46,8 +46,11 @@ static bool g_is_overriding_disabled = false; const char* mod_file_allow_list[] = { "reticle_0.tga", + "reticle_1.tga", "scope_ret_0.tga", + "scope_ret_1.tga", "reticle_rocket_0.tga", + "reticle_rocket_1.tga", }; static bool is_mod_file_in_whitelist(const char* Filename) @@ -60,6 +63,26 @@ static bool is_mod_file_in_whitelist(const char* Filename) #endif // MOD_FILE_WHITELIST +// allow list of table files that can't be used for cheating, but for which modding in clientside mods has utility +// Examples include translation packs (translated strings, endgame, etc.) and custom HUD mods that change coords of HUD elements +const char* tbl_mod_allow_list[] = { + "strings.tbl", + "hud.tbl", + "hud_personas.tbl", + "personas.tbl", + "credits.tbl", + "endgame.tbl", + "ponr.tbl", +}; + +static bool is_tbl_file_in_allowlist(const char* Filename) +{ + for (unsigned i = 0; i < std::size(tbl_mod_allow_list); ++i) + if (!stricmp(tbl_mod_allow_list[i], Filename)) + return true; + return false; +} + #if CHECK_PACKFILE_CHECKSUM static unsigned hash_file(const char* Filename) @@ -298,12 +321,16 @@ static bool is_lookup_table_entry_override_allowed(rf::VPackfileEntry* old_entry // Allow overriding by packfiles from game root and from mods return true; } + if (!old_entry->parent->is_user_maps && is_tbl_file_in_allowlist(new_entry->name)) { + // Allow overriding of tbl files from user_maps if they are on allow list + return true; + } if (!old_entry->parent->is_user_maps && !stricmp(rf::file_get_ext(new_entry->name), ".tbl")) { - // Always skip overriding tbl files from game by user_maps + // Skip overriding of all other tbl files from user_maps return false; } #ifdef MOD_FILE_WHITELIST - if (is_mod_file_in_whitelist(new_entry->file_name)) { + if (is_mod_file_in_whitelist(new_entry->name)) { // Always allow overriding for specific files return true; } From aff7c240db628a8da9287608789ce3c6c865252a Mon Sep 17 00:00:00 2001 From: Goober Date: Mon, 9 Sep 2024 00:28:05 -0230 Subject: [PATCH 2/6] Make tbl file allow list respect player turning off "allow overwrite game files" option --- game_patch/misc/vpackfile.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/game_patch/misc/vpackfile.cpp b/game_patch/misc/vpackfile.cpp index af0044d2..42d79941 100644 --- a/game_patch/misc/vpackfile.cpp +++ b/game_patch/misc/vpackfile.cpp @@ -321,6 +321,9 @@ static bool is_lookup_table_entry_override_allowed(rf::VPackfileEntry* old_entry // Allow overriding by packfiles from game root and from mods return true; } + if (!g_game_config.allow_overwrite_game_files) { + return false; + } if (!old_entry->parent->is_user_maps && is_tbl_file_in_allowlist(new_entry->name)) { // Allow overriding of tbl files from user_maps if they are on allow list return true; @@ -335,9 +338,6 @@ static bool is_lookup_table_entry_override_allowed(rf::VPackfileEntry* old_entry return true; } #endif - if (!g_game_config.allow_overwrite_game_files) { - return false; - } g_is_modded_game = true; return true; } From ecead8c8e07ca636c96d3d9c2e5917029c5c4a14 Mon Sep 17 00:00:00 2001 From: Goober Date: Mon, 9 Sep 2024 00:42:26 -0230 Subject: [PATCH 3/6] Fix potential future issue with mod_file_whitelist (currently disabled feature) --- game_patch/misc/vpackfile.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/game_patch/misc/vpackfile.cpp b/game_patch/misc/vpackfile.cpp index 42d79941..65f24c54 100644 --- a/game_patch/misc/vpackfile.cpp +++ b/game_patch/misc/vpackfile.cpp @@ -321,6 +321,12 @@ static bool is_lookup_table_entry_override_allowed(rf::VPackfileEntry* old_entry // Allow overriding by packfiles from game root and from mods return true; } +#ifdef MOD_FILE_WHITELIST + if (is_mod_file_in_whitelist(new_entry->name)) { + // Always allow overriding for specific files + return true; + } +#endif if (!g_game_config.allow_overwrite_game_files) { return false; } @@ -332,12 +338,6 @@ static bool is_lookup_table_entry_override_allowed(rf::VPackfileEntry* old_entry // Skip overriding of all other tbl files from user_maps return false; } -#ifdef MOD_FILE_WHITELIST - if (is_mod_file_in_whitelist(new_entry->name)) { - // Always allow overriding for specific files - return true; - } -#endif g_is_modded_game = true; return true; } From 636fa8aca4c1e52b9f04619eb6d9f9c4057ae6a0 Mon Sep 17 00:00:00 2001 From: Goober Date: Thu, 19 Sep 2024 16:40:52 -0230 Subject: [PATCH 4/6] support client_mods directory, revamp replacement allowance order --- game_patch/misc/misc.cpp | 7 +- game_patch/misc/misc.h | 1 + game_patch/misc/vpackfile.cpp | 126 ++++++++++++++++++++++++++++---- game_patch/rf/file/file.h | 1 + game_patch/rf/file/packfile.h | 6 +- launcher/DashFactionLauncher.rc | 2 +- launcher/OptionsMiscDlg.cpp | 2 +- 7 files changed, 126 insertions(+), 19 deletions(-) diff --git a/game_patch/misc/misc.cpp b/game_patch/misc/misc.cpp index 8fb5f446..53a91c1e 100644 --- a/game_patch/misc/misc.cpp +++ b/game_patch/misc/misc.cpp @@ -45,6 +45,11 @@ bool g_in_mp_game = false; bool g_jump_to_multi_server_list = false; std::optional g_join_mp_game_seq_data; +bool tc_mod_is_loaded() +{ + return rf::mod_param.found(); +} + CodeInjection critical_error_hide_main_wnd_patch{ 0x0050BA90, []() { @@ -381,7 +386,7 @@ CodeInjection vfile_read_stack_corruption_fix{ CodeInjection game_set_file_paths_injection{ 0x004B1810, []() { - if (rf::mod_param.found()) { + if (tc_mod_is_loaded()) { std::string mod_dir = "mods\\"; mod_dir += rf::mod_param.get_arg(); rf::file_add_path(mod_dir.c_str(), ".bik", false); diff --git a/game_patch/misc/misc.h b/game_patch/misc/misc.h index d48a4ead..020481c3 100644 --- a/game_patch/misc/misc.h +++ b/game_patch/misc/misc.h @@ -10,3 +10,4 @@ void start_join_multi_game_sequence(const rf::NetAddr& addr, const std::string& bool multi_join_game(const rf::NetAddr& addr, const std::string& password); void ui_get_string_size(int* w, int* h, const char* s, int s_len, int font_num); void g_solid_render_ui(); +bool tc_mod_is_loaded(); diff --git a/game_patch/misc/vpackfile.cpp b/game_patch/misc/vpackfile.cpp index 77f52c48..fd654151 100644 --- a/game_patch/misc/vpackfile.cpp +++ b/game_patch/misc/vpackfile.cpp @@ -16,10 +16,12 @@ #include #include "vpackfile.h" #include "../main/main.h" +#include "../misc/misc.h" #include "../rf/file/file.h" #include "../rf/file/packfile.h" #include "../rf/crt.h" #include "../rf/multi.h" +#include "../rf/os/os.h" #include "../os/console.h" #define CHECK_PACKFILE_CHECKSUM 0 // slow (1 second on SSD on first load after boot) @@ -76,14 +78,31 @@ const char* tbl_mod_allow_list[] = { "ponr.tbl", }; +static bool is_tbl_file(const char* Filename) +{ + if (stricmp(rf::file_get_ext(Filename), ".tbl") == 0) { + return true; + } + return false; +} + static bool is_tbl_file_in_allowlist(const char* Filename) { for (unsigned i = 0; i < std::size(tbl_mod_allow_list); ++i) - if (!stricmp(tbl_mod_allow_list[i], Filename)) + if (is_tbl_file(Filename) && !stricmp(tbl_mod_allow_list[i], Filename)) return true; return false; } +static bool is_tbl_file_a_hud_messages_file(const char* Filename) +{ + // check if the input file ends with "_text.tbl" + if (strlen(Filename) >= 9 && stricmp(Filename + strlen(Filename) - 9, "_text.tbl") == 0) { + return true; + } + return false; +} + #if CHECK_PACKFILE_CHECKSUM static unsigned hash_file(const char* Filename) @@ -202,6 +221,7 @@ static int vpackfile_add_new(const char* filename, const char* dir) xlog::error("Failed to open packfile {}", full_path); return 0; } + xlog::info("Opened packfile {}", full_path); auto packfile = std::make_unique(); std::strncpy(packfile->filename, filename, sizeof(packfile->filename) - 1); @@ -210,8 +230,13 @@ static int vpackfile_add_new(const char* filename, const char* dir) packfile->path[sizeof(packfile->path) - 1] = '\0'; packfile->field_a0 = 0; packfile->num_files = 0; - // this is set to true for user_maps - packfile->is_user_maps = rf::vpackfile_loading_user_maps; + // packfile->is_user_maps = rf::vpackfile_loading_user_maps; // this is set to true for user_maps + // check for is_user_maps based on dir (like is_client_mods) instead of 0x01BDB21C + packfile->is_user_maps = (dir && (stricmp(dir, "user_maps\\multi\\") == 0 || stricmp(dir, "user_maps\\single\\") == 0)); + packfile->is_client_mods = (dir && stricmp(dir, "client_mods\\") == 0); + + xlog::debug("Packfile {} is from {}user_maps, {}client_mods", filename, packfile->is_user_maps ? "" : "NOT ", + packfile->is_client_mods ? "" : "NOT "); // Process file header char buf[0x800]; @@ -314,31 +339,62 @@ static int vpackfile_build_file_list_new(const char* ext_filter, char*& filename static bool is_lookup_table_entry_override_allowed(rf::VPackfileEntry* old_entry, rf::VPackfileEntry* new_entry) { + // dirs are loaded in this order: base game, user_maps, client_mods, mods if (g_is_overriding_disabled) { // Don't allow overriding files after game is initialized because it can lead to crashes return false; } - if (!new_entry->parent->is_user_maps) { - // Allow overriding by packfiles from game root and from mods + // Allow overriding by packfiles from game root and from mods (not user_maps or client_mods) + else if (!new_entry->parent->is_user_maps && !new_entry->parent->is_client_mods) { return true; } #ifdef MOD_FILE_WHITELIST - if (is_mod_file_in_whitelist(new_entry->name)) { + else if (is_mod_file_in_whitelist(new_entry->name)) { // Always allow overriding for specific files return true; } #endif - if (!g_game_config.allow_overwrite_game_files) { - return false; - } - if (!old_entry->parent->is_user_maps && is_tbl_file_in_allowlist(new_entry->name)) { - // Allow overriding of tbl files from user_maps if they are on allow list + // Process if in client_mods + else if (new_entry->parent->is_client_mods) { + if (is_tbl_file_in_allowlist(new_entry->name) || is_tbl_file_a_hud_messages_file(new_entry->name)) { + // allow overriding of tbl files from client_mods if they are on allow list or are _text.tbl + g_is_modded_game = true; // goober todo: confirm what this is used for. If anticheat, can remove from here + return true; + } + else if (is_tbl_file(new_entry->name)) { + // deny all other tbl overrides + return false; + } + g_is_modded_game = true; return true; } - if (!old_entry->parent->is_user_maps && !stricmp(rf::file_get_ext(new_entry->name), ".tbl")) { - // Skip overriding of all other tbl files from user_maps - return false; + // Process if in user_maps + else if (new_entry->parent->is_user_maps) { + if (old_entry->parent->is_user_maps) { + // allow files from user_maps to override other files in user_maps, even if the switch is off + // files from user_maps can't override files from client_mods because client_mods is parsed later + return true; + } + else if (!g_game_config.allow_overwrite_game_files) + { + // if the switch is off, no overwriting with files from user_maps, except for other files in user_maps + return false; + } + else if (is_tbl_file_in_allowlist(new_entry->name) || is_tbl_file_a_hud_messages_file(new_entry->name)) + { + // allow overriding of tbl files from user_maps if they are on allow list or are _text.tbl + g_is_modded_game = true; // goober todo: confirm what this is used for. If anticheat, can remove from here + return true; + } + else if (is_tbl_file(new_entry->name)) + { + // deny all other tbl overrides + return false; + } + g_is_modded_game = true; + return true; } + // resolve warning by having a default option, even though there should be no way to hit it g_is_modded_game = true; return true; } @@ -556,6 +612,47 @@ static void vpackfile_cleanup_new() g_packfiles.clear(); } +static void load_vpp_files_from_directory(const char* files, const char* directory) +{ + WIN32_FIND_DATA find_file_data; + HANDLE hFind = FindFirstFile(files, &find_file_data); + + if (hFind != INVALID_HANDLE_VALUE) { + do { + if (!(find_file_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { + // Call vpackfile_add_new function directly + if (vpackfile_add_new(find_file_data.cFileName, directory) == 0) { + xlog::warn("Failed to load additional VPP file: {}", find_file_data.cFileName); + } + } + } while (FindNextFile(hFind, &find_file_data) != 0); + FindClose(hFind); + } + else { + xlog::info("No VPP files found in {}.", directory); + } +} + +static void load_additional_packfiles_new() +{ + // load VPP files from user_maps\single and user_maps\multi first + load_vpp_files_from_directory("user_maps\\single\\*.vpp", "user_maps\\single\\"); + load_vpp_files_from_directory("user_maps\\multi\\*.vpp", "user_maps\\multi\\"); + + // then load VPP files from client_mods + load_vpp_files_from_directory("client_mods\\*.vpp", "client_mods\\"); + + // lastly load VPP files from the loaded TC mod if applicable + if (tc_mod_is_loaded()) { + std::string mod_name = rf::mod_param.get_arg(); + std::string mod_dir = "mods\\" + mod_name + "\\"; + std::string mod_file = mod_dir + "*.vpp"; + xlog::info("Loading packfiles from mod: {}.", mod_name); + //rf::game_add_path(mod_dir.c_str(), ".vpp"); + load_vpp_files_from_directory(mod_file.c_str(), mod_dir.c_str()); + } +} + void vpackfile_apply_patches() { // VPackfile handling implemetation getting rid of all limits @@ -566,6 +663,7 @@ void vpackfile_apply_patches() AsmWriter(0x0052C220).jmp(vpackfile_find_new); AsmWriter(0x0052BB60).jmp(vpackfile_init_new); AsmWriter(0x0052BC80).jmp(vpackfile_cleanup_new); + AsmWriter(0x004B15E0).jmp(load_additional_packfiles_new); // Don't return success from vpackfile_open if offset points out of file contents vpackfile_open_check_seek_result_injection.install(); diff --git a/game_patch/rf/file/file.h b/game_patch/rf/file/file.h index 7bf11a7f..f96a0d45 100644 --- a/game_patch/rf/file/file.h +++ b/game_patch/rf/file/file.h @@ -98,6 +98,7 @@ namespace rf static auto& file_get_ext = addr_as_ref(0x005143F0); static auto& file_add_path = addr_as_ref(0x00514070); + //static auto& game_add_path = addr_as_ref(0x004B1330); static auto& root_path = addr_as_ref(0x018060E8); } diff --git a/game_patch/rf/file/packfile.h b/game_patch/rf/file/packfile.h index 6ecc89a7..0045cb16 100644 --- a/game_patch/rf/file/packfile.h +++ b/game_patch/rf/file/packfile.h @@ -20,7 +20,9 @@ namespace rf #endif uint32_t file_size; #ifdef DASH_FACTION + // track whether the packfile was from user_maps or client_mods bool is_user_maps; + bool is_client_mods; #endif }; #ifndef DASH_FACTION @@ -46,6 +48,6 @@ namespace rf using VPackfileAddEntries_Type = uint32_t(VPackfile* packfile, const void* buf, unsigned num_files_in_block, unsigned* num_added); static auto& vpackfile_add_entries = addr_as_ref(0x0052BD40); - - static auto& vpackfile_loading_user_maps = addr_as_ref(0x01BDB21C); + // handled directly in vpackfile_add_new + //static auto& vpackfile_loading_user_maps = addr_as_ref(0x01BDB21C); } diff --git a/launcher/DashFactionLauncher.rc b/launcher/DashFactionLauncher.rc index daa54882..1e267f6d 100644 --- a/launcher/DashFactionLauncher.rc +++ b/launcher/DashFactionLauncher.rc @@ -154,7 +154,7 @@ STYLE DS_SHELLFONT | WS_CHILD | WS_VISIBLE | DS_CONTROL FONT 8, "MS Shell Dlg" BEGIN AUTOCHECKBOX "Fast Start",IDC_FAST_START_CHECK,0,0,66,10 - AUTOCHECKBOX "Allow overriding game files by packages in user_maps",IDC_ALLOW_OVERWRITE_GAME_CHECK,0,12,260,10 + AUTOCHECKBOX "Allow clientside mods to be loaded from user_maps",IDC_ALLOW_OVERWRITE_GAME_CHECK,0,12,260,10 AUTOCHECKBOX "Run game at reduced speed when window doesn't have focus",IDC_REDUCED_SPEED_IN_BG_CHECK,0,24,260,10 AUTOCHECKBOX "Beep when another player joins multiplayer game and window doesn't have focus", IDC_PLAYER_JOIN_BEEP_CHECK,0,36,260,18,BS_AUTOCHECKBOX | BS_MULTILINE | WS_TABSTOP | BS_TOP diff --git a/launcher/OptionsMiscDlg.cpp b/launcher/OptionsMiscDlg.cpp index e6eb7840..8fa64b50 100644 --- a/launcher/OptionsMiscDlg.cpp +++ b/launcher/OptionsMiscDlg.cpp @@ -25,7 +25,7 @@ void OptionsMiscDlg::InitToolTip() { m_tool_tip.Create(*this); m_tool_tip.AddTool(GetDlgItem(IDC_FAST_START_CHECK), "Skip game intro videos and go straight to Main Menu"); - m_tool_tip.AddTool(GetDlgItem(IDC_ALLOW_OVERWRITE_GAME_CHECK), "Enable this if you want to modify game content by putting mods into user_maps folder. Can have side effect of level packfiles modyfing common textures/sounds."); + m_tool_tip.AddTool(GetDlgItem(IDC_ALLOW_OVERWRITE_GAME_CHECK), "Allows files in user_maps folders to override core game files. When left disabled, only files in client_mods can do this. Enabling this can unintentionally mean stock game assets get replaced by installed levels."); m_tool_tip.AddTool(GetDlgItem(IDC_AUTOSAVE_CHECK), "Automatically save the game after a level transition"); } From 3350c6bbdc6c7fd0ced9b8e0bd5768f9744fe684 Mon Sep 17 00:00:00 2001 From: Goober Date: Thu, 19 Sep 2024 16:42:39 -0230 Subject: [PATCH 5/6] update changelog --- docs/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 21a66352..27c299c9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -43,6 +43,7 @@ Version 1.9.0 (not released yet) - Do not load unnecessary VPPs in dedicated server mode - Add level filename to "Level Initializing" console message - Allow clientside mods to edit table files that can't be used to cheat (strings, hud, hud_personas, personas, credits, endgame, ponr) +- Add support for `client_mods` folder for loading clientside mods and made launcher switch restore legacy behavior - Properly handle WM_PAINT in dedicated server, may improve performance (DF bug) Version 1.8.0 (released 2022-09-17) From 84534ef4b3a898880fe2e749794d3ac3e69174d2 Mon Sep 17 00:00:00 2001 From: Goober Date: Sun, 22 Sep 2024 15:46:57 -0230 Subject: [PATCH 6/6] use std::aray to fix use after free --- game_patch/misc/vpackfile.cpp | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/game_patch/misc/vpackfile.cpp b/game_patch/misc/vpackfile.cpp index fd654151..13394e03 100644 --- a/game_patch/misc/vpackfile.cpp +++ b/game_patch/misc/vpackfile.cpp @@ -68,36 +68,37 @@ static bool is_mod_file_in_whitelist(const char* Filename) // allow list of table files that can't be used for cheating, but for which modding in clientside mods has utility // Examples include translation packs (translated strings, endgame, etc.) and custom HUD mods that change coords of HUD elements -const char* tbl_mod_allow_list[] = { +constexpr std::array tbl_mod_allow_list = { "strings.tbl", "hud.tbl", "hud_personas.tbl", "personas.tbl", "credits.tbl", "endgame.tbl", - "ponr.tbl", + "ponr.tbl" }; -static bool is_tbl_file(const char* Filename) +static bool is_tbl_file(const char* filename) { - if (stricmp(rf::file_get_ext(Filename), ".tbl") == 0) { + // confirm we're working with a tbl file + if (stricmp(rf::file_get_ext(filename), ".tbl") == 0) { return true; } return false; } -static bool is_tbl_file_in_allowlist(const char* Filename) +static bool is_tbl_file_in_allowlist(const char* filename) { - for (unsigned i = 0; i < std::size(tbl_mod_allow_list); ++i) - if (is_tbl_file(Filename) && !stricmp(tbl_mod_allow_list[i], Filename)) - return true; - return false; + // compare the input file against the tbl file allowlist + return is_tbl_file(filename) && std::ranges::any_of(tbl_mod_allow_list, [filename](std::string_view allowed_tbl) { + return stricmp(allowed_tbl.data(), filename) == 0; + }); } -static bool is_tbl_file_a_hud_messages_file(const char* Filename) +static bool is_tbl_file_a_hud_messages_file(const char* filename) { // check if the input file ends with "_text.tbl" - if (strlen(Filename) >= 9 && stricmp(Filename + strlen(Filename) - 9, "_text.tbl") == 0) { + if (strlen(filename) >= 9 && stricmp(filename + strlen(filename) - 9, "_text.tbl") == 0) { return true; } return false;