diff --git a/CMakeLists.txt b/CMakeLists.txt index 8eab5a415..f172aae15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ -# CMake 3.15 is required for CMAKE_MSVC_RUNTIME_LIBRARY. -cmake_minimum_required(VERSION 3.15) +# CMake 3.20 is required for C++23. +cmake_minimum_required(VERSION 3.20) project(DashFaction) if(NOT "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") diff --git a/README.md b/README.md index d67bc8248..077081bc6 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,9 @@ Name | Description `debug_event_msg` | toggle tracking of event messages in console `gibs` | toggle gibs (characters exploding into parts) `muzzle_flash` | toggle muzzle flash +`hit_sounds` | toggle whether to play a sound, when you hit players in multiplayer, if enabled by a server +`weapon_screen_shake` | toggle camera shake from weapon fire [may be force enabled by multiplayer servers] +`full_bright_entities` | toggle full-bright entities [may be force disabled by multiplayer servers] ### Server commands diff --git a/common/include/common/config/GameConfig.h b/common/include/common/config/GameConfig.h index 8a78539e2..eab87273f 100644 --- a/common/include/common/config/GameConfig.h +++ b/common/include/common/config/GameConfig.h @@ -58,6 +58,8 @@ struct GameConfig CfgVar muzzle_flash = true; CfgVar glares = true; CfgVar show_enemy_bullets = true; + CfgVar weapon_screen_shake = true; + CfgVar full_bright_entities = false; static constexpr float min_fov = 75.0f; static constexpr float max_fov = 160.0f; @@ -67,6 +69,7 @@ struct GameConfig // Audio CfgVar level_sound_volume = 1.0f; CfgVar eax_sound = true; + CfgVar hit_sounds = true; // Multiplayer static const char default_rf_tracker[]; diff --git a/common/src/config/GameConfig.cpp b/common/src/config/GameConfig.cpp index 90d118919..30d71c809 100644 --- a/common/src/config/GameConfig.cpp +++ b/common/src/config/GameConfig.cpp @@ -167,6 +167,7 @@ bool GameConfig::visit_vars(T&& visitor, bool is_save) result &= visitor(dash_faction_key, "Scoreboard Animations", scoreboard_anim); result &= visitor(dash_faction_key, "Spectate Mode Minimal UI", spectate_mode_minimal_ui); result &= visitor(dash_faction_key, "Level Sound Volume", level_sound_volume); + result &= visitor(dash_faction_key, "Hit Sounds", hit_sounds); result &= visitor(dash_faction_key, "Allow Overwriting Game Files", allow_overwrite_game_files); result &= visitor(dash_faction_key, "Version", dash_faction_version); result &= visitor(dash_faction_key, "Swap Assault Rifle Controls", swap_assault_rifle_controls); @@ -174,6 +175,8 @@ bool GameConfig::visit_vars(T&& visitor, bool is_save) result &= visitor(dash_faction_key, "Glares", glares); result &= visitor(dash_faction_key, "Linear Pitch", linear_pitch); result &= visitor(dash_faction_key, "Show Enemy Bullets", show_enemy_bullets); + result &= visitor(dash_faction_key, "Weapon Screen Shake", weapon_screen_shake); + result &= visitor(dash_faction_key, "Full-Bright Entities", full_bright_entities); result &= visitor(dash_faction_key, "Keep Launcher Open", keep_launcher_open); result &= visitor(dash_faction_key, "Skip Cutscene Control", skip_cutscene_ctrl); result &= visitor(dash_faction_key, "Damage Screen Flash", damage_screen_flash); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 61187460a..cbfb17596 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,9 +4,22 @@ Dash Faction Changelog Version 1.9.2 (not released yet) -------------------------------- [@is-this-c](https://github.com/is-this-c) +- Changes derived from [Alpine Faction's](https://github.com/GooberRF/alpinefaction) source code +- - Improve compatibility with Alpine Faction network protocol +- - In Alpine Faction levels +- - - Handle `AlpineLevelProps::legacy_cyclic_timers` +- - - Allow triggers to activate events in multiplayer +- - - Fix events that broke if their delay parameter was set +- - - Support loading Alpine Faction's `$Lightmap Clamp Floor`, and `$Lightmap Clamp Ceiling`, `$Crater Texture PPM`, and `$Mesh Replacement`from `{map_name}_info.tbl` +- - - Support dynamic lights +- - - Make particle emitters placed in levels respect their active distance parameter - Fix a potential crash after a client quits a game if Directd3D 11 is enabled -- Improve compatibility with Alpine Faction servers - Fix `PgUp`, `PgDown`, `End`, and `Home` on numeric keypads +- Add `af_killsound1.wav` from Alpine Faction +- Add `hit_sounds` command +- Add `weapon_screen_shake` command +- Add `full_bright_entities` command +- Add `remote_server_flags` command Version 1.9.1 (released 2025-07-05) -------------------------------- diff --git a/game_patch/CMakeLists.txt b/game_patch/CMakeLists.txt index ec918654a..e369642b1 100644 --- a/game_patch/CMakeLists.txt +++ b/game_patch/CMakeLists.txt @@ -158,6 +158,7 @@ set(SRCS misc/ui.cpp misc/game.cpp misc/level.cpp + misc/af_options.cpp sound/sound.cpp sound/sound.h sound/sound_ds.cpp @@ -172,7 +173,7 @@ set(SRCS source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SRCS}) add_library(DashFaction SHARED ${SRCS}) -target_compile_features(DashFaction PUBLIC cxx_std_20) +target_compile_features(DashFaction PRIVATE cxx_std_23) set_target_properties(DashFaction PROPERTIES PREFIX "" CXX_EXTENSIONS NO diff --git a/game_patch/graphics/d3d11/gr_d3d11_mesh.cpp b/game_patch/graphics/d3d11/gr_d3d11_mesh.cpp index 7695172f3..ba825fb06 100644 --- a/game_patch/graphics/d3d11/gr_d3d11_mesh.cpp +++ b/game_patch/graphics/d3d11/gr_d3d11_mesh.cpp @@ -9,6 +9,8 @@ #include "../../rf/math/quaternion.h" #include "../../rf/v3d.h" #include "../../rf/character.h" +#include "../../main/main.h" +#include "../../multi/multi.h" #include "gr_d3d11.h" #include "gr_d3d11_mesh.h" #include "gr_d3d11_context.h" @@ -378,7 +380,7 @@ namespace df::gr::d3d11 render_caches_.clear(); } - void MeshRenderer::render_v3d_vif(rf::VifLodMesh *lod_mesh, int lod_index, const rf::Vector3& pos, const rf::Matrix3& orient, const rf::MeshRenderParams& params) + void MeshRenderer::render_v3d_vif(rf::VifLodMesh *lod_mesh, int lod_index, const rf::Vector3& pos, const rf::Matrix3& orient, rf::MeshRenderParams params) { page_in_v3d_mesh(lod_mesh); @@ -389,10 +391,28 @@ namespace df::gr::d3d11 render_context_.set_index_buffer(v3d_ib_.buffer()); auto render_cache = reinterpret_cast(lod_mesh->render_cache); + + const bool ir_scanner = (params.flags & MRF_SCANNER_1) != 0; + if (!ir_scanner) { + rf::Vector3 ambient_light{0.f, 0.f, 0.f}; + light_get_ambient(&ambient_light.x, &ambient_light.y, &ambient_light.z); + ambient_light *= 255.f; + // RF uses some hard-coded lights here but for now let's keep it simple + ambient_light += 40.f; + // Ignore ambient_color from params, it changes sharply and RF uses it only indirectly for + // its hard-coded lights + params.ambient_color.set( + static_cast(std::min(ambient_light.x, 255.f)), + static_cast(std::min(ambient_light.y, 255.f)), + static_cast(std::min(ambient_light.z, 255.f)), + 255 + ); + } + draw_cached_mesh(lod_mesh, *render_cache, params, lod_index); } - void MeshRenderer::render_character_vif(rf::VifLodMesh *lod_mesh, int lod_index, const rf::Vector3& pos, const rf::Matrix3& orient, const rf::CharacterInstance *ci, const rf::MeshRenderParams& params) + void MeshRenderer::render_character_vif(rf::VifLodMesh *lod_mesh, int lod_index, const rf::Vector3& pos, const rf::Matrix3& orient, const rf::CharacterInstance *ci, rf::MeshRenderParams params) { page_in_character_mesh(lod_mesh); auto render_cache = reinterpret_cast(lod_mesh->render_cache); @@ -416,6 +436,18 @@ namespace df::gr::d3d11 } render_cache->update_bone_transforms_buffer(ci, render_context_); render_cache->bind_buffers(render_context_, morphed); + + const bool ir_scanner = (params.flags & MRF_SCANNER_1) != 0; + if (!ir_scanner) { + const bool allow_full_bright = !(get_remote_server_info() + && !get_remote_server_info().value().allow_full_bright_entities); + if (g_game_config.full_bright_entities + && allow_full_bright + && !(params.flags & MRF_FIRST_PERSON)) { + params.ambient_color.set(255, 255, 255, 255); + } + } + draw_cached_mesh(lod_mesh, *render_cache, params, lod_index); } @@ -458,20 +490,7 @@ namespace df::gr::d3d11 } rf::Color color{255, 255, 255}; if (!ir_scanner) { - // Ignore ambient_color from params, it changes sharply and RF uses it only indirectly for - // its hard-coded lights - float ambient_r, ambient_g, ambient_b; - light_get_ambient(&ambient_r, &ambient_g, &ambient_b); - color.set( - static_cast(ambient_r * 255.0f), - static_cast(ambient_g * 255.0f), - static_cast(ambient_b * 255.0f), - 255 - ); - // RF uses some hard-coded lights here but for now let's keep it simple - color.red += 40; - color.green += 40; - color.blue += 40; + color = params.ambient_color; } else { color = params.self_illum; } diff --git a/game_patch/graphics/d3d11/gr_d3d11_mesh.h b/game_patch/graphics/d3d11/gr_d3d11_mesh.h index 84e913b67..f3e02266b 100644 --- a/game_patch/graphics/d3d11/gr_d3d11_mesh.h +++ b/game_patch/graphics/d3d11/gr_d3d11_mesh.h @@ -94,8 +94,8 @@ namespace df::gr::d3d11 public: MeshRenderer(ComPtr device, ShaderManager& shader_manager, StateManager& state_manager, RenderContext& render_context); ~MeshRenderer(); - void render_v3d_vif(rf::VifLodMesh *lod_mesh, int lod_index, const rf::Vector3& pos, const rf::Matrix3& orient, const rf::MeshRenderParams& params); - void render_character_vif(rf::VifLodMesh *lod_mesh, int lod_index, const rf::Vector3& pos, const rf::Matrix3& orient, const rf::CharacterInstance *ci, const rf::MeshRenderParams& params); + void render_v3d_vif(rf::VifLodMesh *lod_mesh, int lod_index, const rf::Vector3& pos, const rf::Matrix3& orient, rf::MeshRenderParams params); + void render_character_vif(rf::VifLodMesh *lod_mesh, int lod_index, const rf::Vector3& pos, const rf::Matrix3& orient, const rf::CharacterInstance *ci, rf::MeshRenderParams params); void clear_vif_cache(rf::VifLodMesh *lod_mesh); void page_in_v3d_mesh(rf::VifLodMesh* lod_mesh); void page_in_character_mesh(rf::VifLodMesh* lod_mesh); diff --git a/game_patch/graphics/gr.cpp b/game_patch/graphics/gr.cpp index 49c49067e..54a6f6829 100644 --- a/game_patch/graphics/gr.cpp +++ b/game_patch/graphics/gr.cpp @@ -82,10 +82,13 @@ float gr_scale_world_fov(float horizontal_fov = 90.0f) horizontal_fov = gr_scale_fov_hor_plus(horizontal_fov); } - const auto& server_info_opt = get_df_server_info(); - if (server_info_opt && server_info_opt.value().max_fov) { - horizontal_fov = std::min(horizontal_fov, server_info_opt.value().max_fov.value()); + if (get_remote_server_info()) { + const RemoteServerInfo& remote_server_info = get_remote_server_info().value(); + if (remote_server_info.max_fov) { + horizontal_fov = std::min(horizontal_fov, remote_server_info.max_fov.value()); + } } + return horizontal_fov; } @@ -122,10 +125,12 @@ ConsoleCommand2 fov_cmd{ g_game_config.save(); } rf::console::print("Horizontal FOV: {:.2f}", gr_scale_world_fov()); - - const auto& server_info_opt = get_df_server_info(); - if (server_info_opt && server_info_opt.value().max_fov) { - rf::console::print("Server FOV limit: {:.2f}", server_info_opt.value().max_fov.value()); + + if (get_remote_server_info()) { + const RemoteServerInfo& remote_server_info = get_remote_server_info().value(); + if (remote_server_info.max_fov) { + rf::console::print("Server FOV limit: {:.2f}", remote_server_info.max_fov.value()); + } } }, "Sets horizontal FOV (field of view) in degrees. Use 0 to enable automatic FOV scaling.", @@ -240,7 +245,7 @@ ConsoleCommand2 nearest_texture_filtering_cmd{ ConsoleCommand2 lod_distance_scale_cmd{ "lod_distance_scale", [](std::optional scale_opt) { - if (scale_opt.has_value()) { + if (scale_opt) { gr_lod_dist_scale = scale_opt.value(); } rf::console::print("LOD distance scale: {:.2f}", gr_lod_dist_scale); diff --git a/game_patch/main/main.cpp b/game_patch/main/main.cpp index a17cb32d4..4063adeb3 100644 --- a/game_patch/main/main.cpp +++ b/game_patch/main/main.cpp @@ -29,6 +29,8 @@ #include "../misc/misc.h" #include "../misc/vpackfile.h" #include "../misc/high_fps.h" +#include "../misc/level.h" +#include "../misc/af_options.h" #include "../input/input.h" #include "../rf/gr/gr.h" #include "../rf/multi.h" @@ -145,6 +147,7 @@ FunHook level_load_hook{ xlog::info("Loading level: {}", level_filename); if (!save_filename.empty()) xlog::info("Restoring game from save file: {}", save_filename); + load_af_level_info_config(level_filename); int ret = level_load_hook.call_target(level_filename, save_filename, error); if (ret != 0) xlog::warn("Loading failed: {}", error); @@ -160,6 +163,9 @@ FunHook level_init_post_hook{ [](bool transition) { level_init_post_hook.call_target(transition); xlog::info("Level loaded: {}{}", rf::level.filename, transition ? " (transition)" : ""); + if (!rf::is_dedicated_server) { + set_levelmod_autotexture_ppm(); + } }, }; diff --git a/game_patch/misc/af_options.cpp b/game_patch/misc/af_options.cpp new file mode 100644 index 000000000..5f71c7adc --- /dev/null +++ b/game_patch/misc/af_options.cpp @@ -0,0 +1,234 @@ +#include "af_options.h" +#include +#include +#include +#include +#include "../rf/file/file.h" +#include + +AlpineLevelInfoConfig g_af_level_info_config; + +// trim leading and trailing whitespace +std::string trim(const std::string& str, bool remove_quotes = false) +{ + auto start = str.find_first_not_of(" \t\n\r"); + auto end = str.find_last_not_of(" \t\n\r"); + if (start == std::string::npos) { + return ""; + } + + std::string trimmed = str.substr(start, end - start + 1); + + // extract quoted value + if (remove_quotes && trimmed.size() >= 2 && trimmed.front() == '"' && trimmed.back() == '"') { + trimmed = trimmed.substr(1, trimmed.size() - 2); + } + + return trimmed; +} + +// Parsing functions for level settings +std::optional parse_float_level(const std::string& value) +{ + try { return std::stof(value); } catch (...) { return std::nullopt; } +} +std::optional parse_int_level(const std::string& value) +{ + try { return std::stoi(value); } catch (...) { return std::nullopt; } +} +std::optional parse_bool_level(const std::string& value) +{ + return value == "1" || value == "true" || value == "True"; +} +std::optional parse_string_level(const std::string& value) +{ + return trim(value, true); +} + +std::optional parse_color_level(const std::string& value) +{ + std::string trimmed_value = trim(value, true); + + try { + // Check if it's a valid hex string (6 or 8 characters) + if (!trimmed_value.empty() && trimmed_value.length() <= 8 && + std::all_of(trimmed_value.begin(), trimmed_value.end(), ::isxdigit)) { + uint32_t color = std::stoul(trimmed_value, nullptr, 16); // Parse hex + + // If it's a 24-bit color (6 characters), add full alpha (0xFF) + if (trimmed_value.length() == 6) + color = (color << 8) | 0xFF; + + return color; + } + + // Check for RGB formats using regex + std::regex rgb_pattern(R"([\{\<]?\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*[\>\}]?)"); + std::smatch matches; + + if (std::regex_match(trimmed_value, matches, rgb_pattern)) { + if (matches.size() == 4) { // First match is the full string, next 3 are R, G, B + int r = std::stoi(matches[1].str()); + int g = std::stoi(matches[2].str()); + int b = std::stoi(matches[3].str()); + + // Ensure values are within the valid range + if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) { + uint32_t color = (r << 24) | (g << 16) | (b << 8) | 0xFF; // Add full alpha + return color; + } + } + } + } + catch (...) { + return std::nullopt; // Return null if parsing fails + } + + return std::nullopt; +} + +std::optional> parse_mesh_replacement(const std::string& value) +{ + std::string trimmed_value = trim(value, true); + std::regex mesh_replacement_pattern("\\{\\s*\"([^\"]+)\"\\s*,\\s*\"([^\"]+)\"\\s*\\}"); + std::smatch matches; + + if (std::regex_match(trimmed_value, matches, mesh_replacement_pattern)) { + if (matches.size() == 3) { // Full match and 2 filename captures + return std::make_pair(trim(matches[1].str(), true), trim(matches[2].str(), true)); + } + } + return std::nullopt; +} + +// Metadata map for level options +const std::unordered_map level_info_metadata = { + {"$Lightmap Clamp Floor", {AlpineLevelInfoId::LightmapClampFloor, parse_color_level}}, + {"$Lightmap Clamp Ceiling", {AlpineLevelInfoId::LightmapClampCeiling, parse_color_level}}, + {"$Ideal Player Count", {AlpineLevelInfoId::IdealPlayerCount, parse_int_level}}, + {"$Author Contact", {AlpineLevelInfoId::AuthorContact, parse_string_level}}, + {"$Author Website", {AlpineLevelInfoId::AuthorWebsite, parse_string_level}}, + {"$Description", {AlpineLevelInfoId::Description, parse_string_level}}, + {"$Player Headlamp Color", {AlpineLevelInfoId::PlayerHeadlampColor, parse_color_level}}, + {"$Player Headlamp Intensity", {AlpineLevelInfoId::PlayerHeadlampIntensity, parse_float_level}}, + {"$Player Headlamp Range", {AlpineLevelInfoId::PlayerHeadlampRange, parse_float_level}}, + {"$Player Headlamp Radius", {AlpineLevelInfoId::PlayerHeadlampRadius, parse_float_level}}, + {"$Chat Menu 1", {AlpineLevelInfoId::ChatMap1, parse_string_level}}, + {"$Chat Menu 2", {AlpineLevelInfoId::ChatMap2, parse_string_level}}, + {"$Chat Menu 3", {AlpineLevelInfoId::ChatMap3, parse_string_level}}, + {"$Chat Menu 4", {AlpineLevelInfoId::ChatMap4, parse_string_level}}, + {"$Chat Menu 5", {AlpineLevelInfoId::ChatMap5, parse_string_level}}, + {"$Chat Menu 6", {AlpineLevelInfoId::ChatMap6, parse_string_level}}, + {"$Chat Menu 7", {AlpineLevelInfoId::ChatMap7, parse_string_level}}, + {"$Chat Menu 8", {AlpineLevelInfoId::ChatMap8, parse_string_level}}, + {"$Chat Menu 9", {AlpineLevelInfoId::ChatMap9, parse_string_level}}, + {"$Crater Texture PPM", {AlpineLevelInfoId::CraterTexturePpm, parse_float_level}} +}; + +// Load level info from filename_info.tbl +void load_af_level_info_config(const std::string& level_filename) +{ + if (g_af_level_info_config.current_level == level_filename) { + xlog::debug("Reusing previously loaded mapname_info.tbl settings for {}", level_filename); + return; + } + + g_af_level_info_config.reset_for_level(level_filename); + + std::string base_filename = level_filename.substr(0, level_filename.size() - 4); + std::string info_filename = base_filename + "_info.tbl"; + auto level_info_file = std::make_unique(); + + if (level_info_file->open(info_filename.c_str()) != 0) { + xlog::debug("Could not open {}", info_filename); + return; + } + + xlog::debug("Successfully opened {}", info_filename); + + // Read entire file content into a single string buffer + std::string file_content; + std::string buffer(2048, '\0'); + int bytes_read; + + while ((bytes_read = level_info_file->read(&buffer[0], buffer.size() - 1)) > 0) { + buffer.resize(bytes_read); + file_content += buffer; + buffer.resize(2048, '\0'); + } + + level_info_file->close(); + + // Process file content line-by-line + std::istringstream file_stream(file_content); + std::string line; + bool found_start = false; + + // Search for #Start marker + while (std::getline(file_stream, line)) { + line = trim(line, false); + if (line == "#Start") { + found_start = true; + break; + } + } + + if (!found_start) { + xlog::warn("No #Start marker found in {}", info_filename); + return; + } + + // Process options until #End marker is found + bool found_end = false; + while (std::getline(file_stream, line)) { + line = trim(line, false); + + if (line == "#End") { + found_end = true; + break; + } + if (line.empty() || line.starts_with("//")) { + continue; + } + + auto delimiter_pos = line.find(':'); + if (delimiter_pos == std::string::npos || line[0] != '$') { + continue; + } + + std::string option_name = trim(line.substr(0, delimiter_pos), false); + std::string option_value = trim(line.substr(delimiter_pos + 1), false); + + // handle mesh replacements + if (option_name == "$Mesh Replacement") { + auto parsed_value = parse_mesh_replacement(option_value); + if (parsed_value) { + g_af_level_info_config.mesh_replacements[string_to_lower(parsed_value->first)] = parsed_value->second; + xlog::debug("Mesh Replacement Added: {} -> {} in {}", parsed_value->first, parsed_value->second, level_filename); + } + else { + xlog::warn("Invalid mesh replacement format in {}", info_filename); + } + + continue; + } + + // handle other level info options + auto meta_it = level_info_metadata.find(option_name); + if (meta_it != level_info_metadata.end()) { + const auto& metadata = meta_it->second; + auto parsed_value = metadata.parse(option_value); + if (parsed_value) { + g_af_level_info_config.level_options[metadata.id] = *parsed_value; + xlog::debug("Parsed and applied {} for {}: {}", option_name, level_filename, option_value); + } + } + else { + xlog::warn("Unknown option {} in {}", option_name, info_filename); + } + } + + if (!found_end) { + xlog::warn("No #End marker found in {}", info_filename); + } +} diff --git a/game_patch/misc/af_options.h b/game_patch/misc/af_options.h new file mode 100644 index 000000000..eac48f942 --- /dev/null +++ b/game_patch/misc/af_options.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +void load_af_level_info_config(const std::string& level_filename); + +enum class AlpineLevelInfoId { + LightmapClampFloor, + LightmapClampCeiling, + IdealPlayerCount, + AuthorContact, + AuthorWebsite, + Description, + PlayerHeadlampColor, + PlayerHeadlampIntensity, + PlayerHeadlampRange, + PlayerHeadlampRadius, + ChatMap1, + ChatMap2, + ChatMap3, + ChatMap4, + ChatMap5, + ChatMap6, + ChatMap7, + ChatMap8, + ChatMap9, + CraterTexturePpm, + _last_variant +}; + +// Represents different configuration values. +using LevelInfoValue = std::variant; + +struct LevelInfoMetadata { + AlpineLevelInfoId id{}; + std::function(const std::string&)> parse{}; +}; + +struct AlpineLevelInfoConfig { + std::string current_level{}; + std::unordered_map level_options{}; + // `mesh_replacements` is lower case. + std::unordered_map mesh_replacements{}; + + void reset_for_level(const std::string& level) { + current_level = level; + level_options.clear(); + mesh_replacements.clear(); + } + + bool is_option_loaded(const AlpineLevelInfoId option_id) const { + return level_options.find(option_id) != level_options.end(); + } +}; + +extern AlpineLevelInfoConfig g_af_level_info_config; diff --git a/game_patch/misc/g_solid.cpp b/game_patch/misc/g_solid.cpp index 908c7bd0a..32d26002f 100644 --- a/game_patch/misc/g_solid.cpp +++ b/game_patch/misc/g_solid.cpp @@ -9,9 +9,11 @@ #include "../rf/os/frametime.h" #include "../rf/gameseq.h" #include "../os/console.h" +#include "../rf/level.h" #include "../bmpman/bmpman.h" #include "../bmpman/fmt_conv_templates.h" #include "level.h" +#include "af_options.h" constexpr auto reference_fps = 30.0f; constexpr auto reference_frametime = 1.0f / reference_fps; @@ -232,10 +234,55 @@ CodeInjection level_load_lightmaps_color_conv_patch{ if (!rf::gr::lock(lightmap->bm_handle, 0, &lock, rf::gr::LOCK_WRITE_ONLY)) return; - // cap minimal color channel value as RF does - if (!DashLevelProps::instance().lightmaps_full_depth) { - for (int i = 0; i < lightmap->w * lightmap->h * 3; ++i) { - lightmap->buf[i] = std::max(lightmap->buf[i], static_cast(4 << 3)); // 32 + // No floor. + uint32_t floor_clamp = 0; + // No ceiling. + uint32_t ceiling_clamp = 0xFFFFFFFF; + bool should_clamp = false; + bool floor_clamp_defined = false; + + if (g_af_level_info_config + .is_option_loaded(AlpineLevelInfoId::LightmapClampFloor)) { + floor_clamp = std::get( + g_af_level_info_config + .level_options + .at(AlpineLevelInfoId::LightmapClampFloor) + ); + floor_clamp_defined = true; + should_clamp = true; + } + + if (g_af_level_info_config + .is_option_loaded(AlpineLevelInfoId::LightmapClampCeiling)) { + ceiling_clamp = std::get( + g_af_level_info_config + .level_options + .at(AlpineLevelInfoId::LightmapClampCeiling) + ); + should_clamp = true; + } + + if (!floor_clamp_defined + && rf::level.version < 300 + && !DashLevelProps::instance().lightmaps_full_depth) { + should_clamp = true; + constexpr int default_floor_clamp = 0x202020FF; + floor_clamp = default_floor_clamp; + } + + if (should_clamp) { + xlog::debug("Applying lightmap clamping"); + + const rf::gr::Color floor = rf::gr::Color::from_u32(floor_clamp); + const rf::gr::Color ceiling = rf::gr::Color::from_u32(ceiling_clamp); + + for (int i = 0; i < lightmap->w * lightmap->h * 3; i += 3) { + lightmap->buf[i] = + std::clamp(lightmap->buf[i], floor.red, ceiling.red); + lightmap->buf[i + 1] = + std::clamp(lightmap->buf[i + 1], floor.green, ceiling.green); + lightmap->buf[i + 2] = + std::clamp(lightmap->buf[i + 2], floor.blue, ceiling.blue); } } diff --git a/game_patch/misc/level.cpp b/game_patch/misc/level.cpp index 87d5c515c..cb0a7ee57 100644 --- a/game_patch/misc/level.cpp +++ b/game_patch/misc/level.cpp @@ -9,8 +9,13 @@ #include "../rf/level.h" #include "../rf/geometry.h" #include "../rf/file/file.h" -#include "level.h" +#include "../rf/player/player.h" +#include "../rf/player/camera.h" #include "../main/main.h" +#include "level.h" +#include "af_options.h" + +static float g_crater_autotexture_ppm = 32.f; CodeInjection level_load_header_version_check_patch{ 0x004615BF, @@ -74,26 +79,43 @@ CallHook level_init_pre_console_output_hook{ }, }; +bool g_level_has_unsupported_event_classes = false; + +CodeInjection level_load_player_create_entity_injection{ + 0x0045C7D9, + [] (auto& regs) { + if (g_level_has_unsupported_event_classes) { + rf::camera_enter_fixed(rf::local_player->cam); + regs.eip = 0x0045C80F; + } + }, +}; + CodeInjection level_read_header_inj{ 0x004616FB, - []() { + [] { DashLevelProps::instance().reset(rf::level.version); + AlpineLevelProps::instance().reset(); + g_level_has_unsupported_event_classes = false; }, }; CodeInjection level_load_chunk_inj{ 0x00460912, - [](auto& regs) { - int chunk_id = regs.eax; + [] (auto& regs) { + const int chunk_id = regs.eax; rf::File& file = addr_as_ref(regs.esp + 0x2B0 - 0x278); - auto chunk_len = addr_as_ref(regs.esp + 0x2B0 - 0x2A0); - - if (chunk_id == dash_level_props_chunk_id) { - auto version = file.read(); - if (version == 1) { + const std::size_t chunk_len = addr_as_ref(regs.esp + 0x2B0 - 0x2A0); + if (chunk_id == dash_level_props_chunk_id + || chunk_id == alpine_props_chunk_id) { + const int chunk_end = file.tell() + chunk_len; + if (chunk_id == dash_level_props_chunk_id) { DashLevelProps::instance().deserialize(file); - } else { - file.seek(chunk_len - 4, rf::File::seek_cur); + } else if (chunk_id == alpine_props_chunk_id) { + AlpineLevelProps::instance().deserialize(file); + } + if (file.tell() != chunk_end) { + file.seek(chunk_end, rf::File::seek_set); } regs.eip = 0x004608EF; } @@ -107,11 +129,24 @@ CodeInjection level_blast_ppm_injection{ int bmh = regs.edi; int w = 256, h = 256; rf::bm::get_dimensions(bmh, &w, &h); - float ppm = std::min(w, h) / 256.0 * 32.0; + float ppm = std::min(w, h) / 256.f * g_crater_autotexture_ppm; solid->set_autotexture_pixels_per_meter(ppm); }, }; +void set_levelmod_autotexture_ppm() { + if (g_af_level_info_config + .is_option_loaded(AlpineLevelInfoId::CraterTexturePpm)) { + g_crater_autotexture_ppm = std::get( + g_af_level_info_config + .level_options + .at(AlpineLevelInfoId::CraterTexturePpm) + ); + } else { + g_crater_autotexture_ppm = 32.f; + } +} + void level_apply_patch() { // Enable loading RFLs with version 300 (produced by AF editor) @@ -129,6 +164,9 @@ void level_apply_patch() // Add level name to "-- Level Initializing --" message level_init_pre_console_output_hook.install(); + // If a level has unsupported event classes, do not automatically spawn in a listen server. + level_load_player_create_entity_injection.install(); + // Load custom rfl chunks level_read_header_inj.install(); level_load_chunk_inj.install(); diff --git a/game_patch/misc/level.h b/game_patch/misc/level.h index 98778e30a..0d04406d1 100644 --- a/game_patch/misc/level.h +++ b/game_patch/misc/level.h @@ -3,27 +3,54 @@ #include #include "../rf/file/file.h" +void set_levelmod_autotexture_ppm(); + +constexpr int alpine_props_chunk_id = 0x0AFBA5ED; constexpr int dash_level_props_chunk_id = 0xDA58FA00; -struct DashLevelProps -{ - // backward compatible defaults +struct AlpineLevelProps { + // Backward-compatible defaults. + bool legacy_cyclic_timers = true; + + static AlpineLevelProps& instance() { + static AlpineLevelProps instance{}; + return instance; + } + + void reset() { + legacy_cyclic_timers = true; + } + + void deserialize(rf::File& file) { + const std::int32_t version = file.read(); + // Forward-compatible deserialization. + if (version >= 1) { + legacy_cyclic_timers = file.read(); + xlog::debug("legacy_cyclic_timers {}", legacy_cyclic_timers); + } + } +}; + +struct DashLevelProps { + // Backward-compatible defaults. bool lightmaps_full_depth = false; - static DashLevelProps& instance() - { - static DashLevelProps instance; + static DashLevelProps& instance() { + static DashLevelProps instance{}; return instance; } - void reset(int version) - { + void reset(int version) { lightmaps_full_depth = version >= 300; } - void deserialize(rf::File& file) - { - lightmaps_full_depth = file.read(); - xlog::debug("lightmaps_full_depth {}", lightmaps_full_depth); + void deserialize(rf::File& file) { + const std::int32_t version = file.read(); + if (version == 1) { + lightmaps_full_depth = file.read(); + xlog::debug("lightmaps_full_depth {}", lightmaps_full_depth); + } } }; + +extern bool g_level_has_unsupported_event_classes; diff --git a/game_patch/misc/player.cpp b/game_patch/misc/player.cpp index a85cf07c7..e380d97fa 100644 --- a/game_patch/misc/player.cpp +++ b/game_patch/misc/player.cpp @@ -8,8 +8,11 @@ #include "../rf/weapon.h" #include "../rf/hud.h" #include "../rf/input.h" +#include "../rf/ui.h" +#include "../rf/level.h" #include "../os/console.h" #include "../main/main.h" +#include "../misc/level.h" #include "../multi/multi.h" #include "../hud/multi_spectate.h" #include @@ -269,6 +272,17 @@ ConsoleCommand2 damage_screen_flash_cmd{ "Toggle damage screen flash effect", }; +ConsoleCommand2 hit_sounds_cmd{ + "hit_sounds", + [] { + g_game_config.hit_sounds = !g_game_config.hit_sounds; + g_game_config.save(); + rf::console::print("Hit sounds is {}", g_game_config.hit_sounds ? "enabled" : "disabled"); + }, + "Toggle whether to play a sound, when you hit players in multiplayer, if enabled by a server", + "hit_sounds", +}; + CallHook player_cockpit_vmesh_render_hook{ 0x004A7907, [](rf::VMesh *vmesh, rf::Vector3 *pos, rf::Matrix3 *orient, void *params) { @@ -292,6 +306,25 @@ CodeInjection sr_load_player_weapon_anims_injection{ }, }; +CodeInjection player_execute_action_spawn_player_injection{ + 0x004A678B, + [] (auto& regs) { + if (g_level_has_unsupported_event_classes) { + const std::string text = std::format( + "You cannot spawn in a level that has unsupported event classes!\nRFL version: {}", + rf::level.version + ); + rf::ui::popup_message( + "Error", + text.c_str(), + nullptr, + false + ); + regs.eip = 0x004A679D; + } + }, +}; + void player_do_patch() { // general hooks @@ -356,4 +389,7 @@ void player_do_patch() // Commands damage_screen_flash_cmd.register_cmd(); + hit_sounds_cmd.register_cmd(); + + player_execute_action_spawn_player_injection.install(); } diff --git a/game_patch/misc/save_restore.cpp b/game_patch/misc/save_restore.cpp index 8b5dd7a00..ea7168b56 100644 --- a/game_patch/misc/save_restore.cpp +++ b/game_patch/misc/save_restore.cpp @@ -257,9 +257,7 @@ FunHook quick_save_hook{ 0x004B6160, []() { quick_save_hook.call_target(); - bool server_side_saving_enabled = rf::is_multi && !rf::is_server && get_df_server_info() - && get_df_server_info().value().saving_enabled; - if (server_side_saving_enabled) { + if (get_remote_server_info() && get_remote_server_info().value().saving) { send_chat_line_packet("/save", nullptr); } }, @@ -269,9 +267,7 @@ FunHook quick_load_hook{ 0x004B6180, []() { quick_load_hook.call_target(); - bool server_side_saving_enabled = rf::is_multi && !rf::is_server && get_df_server_info() - && get_df_server_info().value().saving_enabled; - if (server_side_saving_enabled) { + if (get_remote_server_info() && get_remote_server_info().value().saving) { send_chat_line_packet("/load", nullptr); } }, diff --git a/game_patch/multi/multi.h b/game_patch/multi/multi.h index 4dd347cd7..b667de628 100644 --- a/game_patch/multi/multi.h +++ b/game_patch/multi/multi.h @@ -73,12 +73,29 @@ struct PlayerStatsNew : rf::PlayerLevelStats } }; -struct DashFactionServerInfo -{ - uint8_t version_major = 0; - uint8_t version_minor = 0; - bool saving_enabled = false; - std::optional max_fov; +enum class ServerSoftware { + DashFaction, + AlpineFaction, +}; + +struct RemoteServerInfo { + struct { + uint8_t major = 0; + uint8_t minor = 0; + } version{}; + bool saving = false; + std::optional max_fov{}; + ServerSoftware software = ServerSoftware::DashFaction; + bool allow_full_bright_entities = true; + bool allow_light_maps_only = false; + bool allow_no_screen_shake = true; + bool no_player_collide = false; + bool allow_no_muzzle_flash = true; + bool click_limit = false; + std::optional semiauto_cool_down{}; + bool unlimited_fps = false; + bool gaussian_spread = false; + bool location_pinging = false; }; void multi_level_download_update(); @@ -86,7 +103,7 @@ void multi_do_patch(); void multi_after_full_game_init(); void multi_init_player(rf::Player* player); void send_chat_line_packet(const char* msg, rf::Player* target, rf::Player* sender = nullptr, bool is_team_msg = false); -const std::optional& get_df_server_info(); +const std::optional& get_remote_server_info(); void multi_level_download_do_frame(); void multi_level_download_abort(); void multi_ban_apply_patch(); diff --git a/game_patch/multi/network.cpp b/game_patch/multi/network.cpp index efc97656e..851305b74 100644 --- a/game_patch/multi/network.cpp +++ b/game_patch/multi/network.cpp @@ -36,6 +36,7 @@ #include "../rf/os/timer.h" #include "../rf/geometry.h" #include "../rf/level.h" +#include "../rf/sound/sound.h" #include "../misc/misc.h" #include "../misc/player.h" #include "../object/object.h" @@ -185,6 +186,7 @@ enum packet_type : uint8_t { sound = 0x34, team_score = 0x35, glass_kill = 0x36, + af_damage_notify = 0x52, }; // client -> server @@ -256,10 +258,11 @@ std::array g_client_side_packet_whitelist{ sound, team_score, glass_kill, + af_damage_notify, }; // clang-format on -std::optional g_df_server_info; +std::optional g_remote_server_info{}; CodeInjection process_game_packet_whitelist_filter{ 0x0047918D, @@ -620,7 +623,7 @@ struct df_sign_packet_ext uint8_t version_minor = VERSION_MINOR; }; -struct af_join_req_tail_v2 +struct AFJoinReqV2 { uint32_t signature = ALPINE_FACTION_SIGNATURE; uint8_t version_major = 1; @@ -662,7 +665,7 @@ std::pair, size_t> extend_packet_with_df_signature( std::pair, size_t> extend_packet_with_af_join_req_tail(std::byte* data, size_t len) { - af_join_req_tail_v2 ext{}; + AFJoinReqV2 ext{}; return extend_packet(data, len, ext); } @@ -702,6 +705,33 @@ struct DashFactionJoinAcceptPacketExt template<> struct EnableEnumBitwiseOperators : std::true_type {}; +struct AlpineFactionJoinAcceptPacketExt { + uint32_t af_signature = ALPINE_FACTION_SIGNATURE; + uint8_t version_major = 1; + uint8_t version_minor = 1; + + enum class Flags : uint32_t { + none = 0, + saving_enabled = 1 << 0, + max_fov = 1 << 1, + allow_fb_mesh = 1 << 2, + allow_lmap = 1 << 3, + allow_no_ss = 1 << 4, + no_player_collide = 1 << 5, + allow_no_mf = 1 << 6, + click_limit = 1 << 7, + unlimited_fps = 1 << 8, + gaussian_spread = 1 << 9, + location_pinging = 1 << 10, + } flags = Flags::none; + + float max_fov{}; + int semi_auto_cooldown{}; +}; + +template<> +struct EnableEnumBitwiseOperators : std::true_type {}; + CallHook send_join_req_packet_hook{ 0x0047ABFB, [](const rf::NetAddr* addr, std::byte* data, size_t len) { @@ -728,33 +758,122 @@ CallHook send_join_accept_packet_ho }, }; +bool parse_df_join_accept_tail(const std::byte* const tail) { + DashFactionJoinAcceptPacketExt ext{}; + std::memcpy(&ext, tail, sizeof(ext)); + xlog::debug("Checking for join_accept DF extension: {:#08x}", ext.df_signature); + if (ext.df_signature != DASH_FACTION_SIGNATURE) { + return false; + } + xlog::debug( + "Got DF server info: {} {} {:#x}", + ext.version_major, + ext.version_minor, + static_cast(ext.flags) + ); + RemoteServerInfo remote_server_info{}; + remote_server_info.version.major = ext.version_major; + remote_server_info.version.minor = ext.version_minor; + using Flags = DashFactionJoinAcceptPacketExt::Flags; + remote_server_info.saving = !!(ext.flags & Flags::saving_enabled); + constexpr float default_fov = 90.f; + if (!!(ext.flags & Flags::max_fov) && ext.max_fov >= default_fov) { + remote_server_info.max_fov.emplace(ext.max_fov); + } + remote_server_info.software = ServerSoftware::DashFaction; + g_remote_server_info.emplace(remote_server_info); + return true; +} + +bool parse_af_join_accept_tail(const std::byte* const tail) { + AlpineFactionJoinAcceptPacketExt ext{}; + std::memcpy(&ext, tail, sizeof(ext)); + xlog::debug("Checking for join_accept AF extension: {:#08x}", ext.af_signature); + if (ext.af_signature != ALPINE_FACTION_SIGNATURE) { + return false; + } + xlog::debug( + "Got AF server info: {} {} {:#x}", + ext.version_major, + ext.version_minor, + static_cast(ext.flags) + ); + RemoteServerInfo remote_server_info{}; + remote_server_info.version.major = ext.version_major; + remote_server_info.version.minor = ext.version_minor; + using Flags = AlpineFactionJoinAcceptPacketExt::Flags; + remote_server_info.saving = !!(ext.flags & Flags::saving_enabled); + constexpr float default_fov = 90.f; + if (!!(ext.flags & Flags::max_fov) && ext.max_fov >= default_fov) { + remote_server_info.max_fov.emplace(ext.max_fov); + } + remote_server_info.software = ServerSoftware::AlpineFaction; + remote_server_info.allow_full_bright_entities = !!(ext.flags & Flags::allow_fb_mesh); + remote_server_info.allow_light_maps_only = !!(ext.flags & Flags::allow_lmap); + remote_server_info.allow_no_screen_shake = !!(ext.flags & Flags::allow_no_ss); + remote_server_info.no_player_collide = !!(ext.flags & Flags::no_player_collide); + remote_server_info.allow_no_muzzle_flash = !!(ext.flags & Flags::allow_no_mf); + remote_server_info.click_limit = !!(ext.flags & Flags::click_limit); + remote_server_info.unlimited_fps = !!(ext.flags & Flags::unlimited_fps); + remote_server_info.gaussian_spread = !!(ext.flags & Flags::gaussian_spread); + remote_server_info.location_pinging = !!(ext.flags & Flags::location_pinging); + if (remote_server_info.click_limit) { + remote_server_info.semiauto_cool_down.emplace(ext.semi_auto_cooldown); + } + g_remote_server_info.emplace(remote_server_info); + return true; +} + CodeInjection process_join_accept_injection{ 0x0047A979, - [](auto& regs) { - std::byte* packet = regs.ebp; - auto ext_offset = regs.esi + 5; - DashFactionJoinAcceptPacketExt ext_data; - std::copy(packet + ext_offset, packet + ext_offset + sizeof(DashFactionJoinAcceptPacketExt), - reinterpret_cast(&ext_data)); - xlog::debug("Checking for join_accept DF extension: {:08X}", ext_data.df_signature); - if (ext_data.df_signature == DASH_FACTION_SIGNATURE) { - DashFactionServerInfo server_info; - server_info.version_major = ext_data.version_major; - server_info.version_minor = ext_data.version_minor; - xlog::debug("Got DF server info: {} {} {}", ext_data.version_major, ext_data.version_minor, - static_cast(ext_data.flags)); - server_info.saving_enabled = !!(ext_data.flags & DashFactionJoinAcceptPacketExt::Flags::saving_enabled); - - constexpr float default_fov = 90.0f; - if (!!(ext_data.flags & DashFactionJoinAcceptPacketExt::Flags::max_fov) && ext_data.max_fov >= default_fov) { - server_info.max_fov = ext_data.max_fov; - } - g_df_server_info = std::optional{server_info}; + [] (const auto& regs) { + const std::byte* const packet = regs.ebp; + const int ext_offset = regs.esi + 5; + const std::byte* const tail = packet + ext_offset; + if (!parse_df_join_accept_tail(tail)) { + parse_af_join_accept_tail(tail); } - else { - g_df_server_info.reset(); + } +}; + +ConsoleCommand2 remote_server_flags_cmd{ + "remote_server_flags", + [] { + if (g_remote_server_info) { + const RemoteServerInfo& remote_server_info = g_remote_server_info.value(); + rf::console::print("===================="); + rf::console::print( + "{} Faction {}.{}", + remote_server_info.software == ServerSoftware::AlpineFaction ? "Alpine" : "Dash", + remote_server_info.version.major, + remote_server_info.version.minor + ); + rf::console::print("===================="); + rf::console::print("Teleport: {}", remote_server_info.saving); + if (remote_server_info.max_fov) { + rf::console::print("Max FOV: Some({})", remote_server_info.max_fov.value()); + } else { + rf::console::print("Max FOV: None"); + } + rf::console::print("Allow full-bright entities: {}", remote_server_info.allow_full_bright_entities); + rf::console::print("Allow light maps only: {}", remote_server_info.allow_light_maps_only); + rf::console::print("Allow no screen shake: {}", remote_server_info.allow_no_screen_shake); + rf::console::print("No player collide: {}", remote_server_info.no_player_collide); + rf::console::print("Allow no muzzle flash: {}", remote_server_info.allow_no_muzzle_flash); + rf::console::print("Semiauto fire rate limit: {}", remote_server_info.click_limit); + if (remote_server_info.semiauto_cool_down) { + rf::console::print( + "Semiauto cool-down: Some({})", + remote_server_info.semiauto_cool_down.value() + ); + } else { + rf::console::print("Semiauto cool-down: None"); + } + rf::console::print("Allow unlimited FPS: {}", remote_server_info.unlimited_fps); + rf::console::print("Gaussian spread: {}", remote_server_info.gaussian_spread); + rf::console::print("Ping locations: {}", remote_server_info.location_pinging); } - }, + } }; CodeInjection process_join_accept_send_game_info_req_injection{ @@ -916,7 +1035,7 @@ FunHook multi_stop_hook{ 0x0046E2C0, []() { // Clear server info when leaving - g_df_server_info.reset(); + g_remote_server_info.reset(); multi_stop_hook.call_target(); if (rf::local_player) { reset_player_additional_data(rf::local_player); @@ -924,9 +1043,9 @@ FunHook multi_stop_hook{ }, }; -const std::optional& get_df_server_info() +const std::optional& get_remote_server_info() { - return g_df_server_info; + return g_remote_server_info; } void send_chat_line_packet(const char* msg, rf::Player* target, rf::Player* sender, bool is_team_msg) @@ -1049,10 +1168,70 @@ void __fastcall multi_io_stats_add_new(void *this_, int edx, int size, bool is_s FunHook multi_io_stats_add_hook{0x0047CAC0, multi_io_stats_add_new}; +#pragma pack(push, 1) + +struct af_damage_notify_packet { + RF_GamePacketHeader header; + uint8_t player_id; + uint16_t damage; + uint8_t flags; +}; + +#pragma pack(pop) + +void af_process_damage_notify_packet(const void* const data, const size_t len, const rf::NetAddr& addr) { + // As a client, receive from a server. + if (!rf::is_multi || rf::is_server || rf::is_dedicated_server) { + return; + } + + af_damage_notify_packet damage_notify_packet{}; + if (len < sizeof(damage_notify_packet)) { + return; + } + + std::memcpy(&damage_notify_packet, data, sizeof(damage_notify_packet)); + + const bool died = static_cast(damage_notify_packet.flags & 0x1); + if (g_game_config.hit_sounds) { + constexpr int hit_sound_id = 29; + static const int kill_sound_id = rf::snd_get_handle("af_killsound1.wav", 5.f, 1.f, 1.f); + rf::snd_play(died ? kill_sound_id : hit_sound_id, 0, 0.f, 1.f); + } +} + +bool af_process_packet( + const void* const data, + const int len, + const rf::NetAddr& addr, + const rf::Player* const player +) { + RF_GamePacketHeader header{}; + if (len < static_cast(sizeof(header))) { + return false; + } + + std::memcpy(&header, data, sizeof(header)); + const packet_type packet_ty = static_cast(header.type); + + switch (packet_ty) { + case packet_type::af_damage_notify: { + af_process_damage_notify_packet(data, len, addr); + break; + } + default: { + return false; + } + } + return true; +} + static void process_custom_packet([[maybe_unused]] void* data, [[maybe_unused]] int len, [[maybe_unused]] const rf::NetAddr& addr, [[maybe_unused]] rf::Player* player) { - pf_process_packet(data, len, addr, player); + if (!pf_process_packet(data, len, addr, player)) { + af_process_packet(data, len, addr, player); + } } CodeInjection multi_io_process_packets_injection{ @@ -1255,4 +1434,6 @@ void network_init() // Ignore browsers when calculating player count for info requests game_info_num_players_hook.install(); + + remote_server_flags_cmd.register_cmd(); } diff --git a/game_patch/multi/server.cpp b/game_patch/multi/server.cpp index 30e13ad04..9dbbbe8e0 100644 --- a/game_patch/multi/server.cpp +++ b/game_patch/multi/server.cpp @@ -713,6 +713,17 @@ CodeInjection multi_level_init_injection{ }, }; +CallHook player_create_entity_entity_create_hook{ + 0x004A41D3, + [] (const int type, const char* const name, const int parent_handle, const rf::Vector3& pos, rf::Matrix3& orient, int create_flags, const int mp_character) { + if (get_remote_server_info() && get_remote_server_info().value().no_player_collide) { + constexpr int NO_PLAYER_COLLIDE = 0x4; + create_flags |= NO_PLAYER_COLLIDE; + } + return player_create_entity_entity_create_hook.call_target(type, name, parent_handle, pos, orient, create_flags, mp_character); + } +}; + void server_init() { // Override rcon command whitelist @@ -781,6 +792,8 @@ void server_init() // Shuffle rotation when the last map in the list is loaded multi_level_init_injection.install(); + + player_create_entity_entity_create_hook.install(); } void server_do_frame() diff --git a/game_patch/object/entity.cpp b/game_patch/object/entity.cpp index dd5bfe27a..c18925344 100644 --- a/game_patch/object/entity.cpp +++ b/game_patch/object/entity.cpp @@ -14,6 +14,7 @@ #include "../rf/debris.h" #include "../rf/misc.h" #include "../main/main.h" +#include "../multi/multi.h" rf::Timestamp g_player_jump_timestamp; @@ -276,10 +277,38 @@ static FunHook entity_damage_hook{ }, }; +CallHook entity_fire_primary_weapon_camera_shake_hook{ + 0x00426C79, + [] (rf::Camera* const cp, const float amplitude, const float time_seconds) { + const bool force_screen_shake = get_remote_server_info() + && !get_remote_server_info().value().allow_no_screen_shake; + if (g_game_config.weapon_screen_shake || force_screen_shake) { + entity_fire_primary_weapon_camera_shake_hook.call_target(cp, amplitude, time_seconds); + } + }, +}; + +ConsoleCommand2 weapon_screen_shake_cmd{ + "weapon_screen_shake", + [] { + g_game_config.weapon_screen_shake = !g_game_config.weapon_screen_shake; + g_game_config.save(); + rf::console::print( + "Weapon screen shake is {}", + g_game_config.weapon_screen_shake + ? "enabled" + : "disabled [may be force enabled by multiplayer servers]" + ); + }, + "Toggle camera shake from weapon fire [may be force enabled by multiplayer servers]", +}; + CallHook entity_update_muzzle_flash_light_hook{ 0x0041E814, - [](rf::Entity& ep) { - if (g_game_config.muzzle_flash) { + [] (rf::Entity& ep) { + const bool force_muzzle_flash = get_remote_server_info() + && !get_remote_server_info().value().allow_no_muzzle_flash; + if (g_game_config.muzzle_flash || force_muzzle_flash) { entity_update_muzzle_flash_light_hook.call_target(ep); } }, @@ -362,7 +391,11 @@ void entity_do_patch() // Don't create muzzle flash lights entity_update_muzzle_flash_light_hook.install(); + // Support disabling camera shake from weapon fire. + entity_fire_primary_weapon_camera_shake_hook.install(); + // Commands gibs_cmd.register_cmd(); muzzle_flash_cmd.register_cmd(); + weapon_screen_shake_cmd.register_cmd(); } diff --git a/game_patch/object/event.cpp b/game_patch/object/event.cpp index b35cd8f47..ed166ee06 100644 --- a/game_patch/object/event.cpp +++ b/game_patch/object/event.cpp @@ -5,15 +5,17 @@ #include #include #include +#include #include "../rf/object.h" #include "../rf/event.h" #include "../rf/entity.h" -#include "../rf/level.h" #include "../rf/multi.h" +#include "../rf/level.h" #include "../rf/file/file.h" #include "../rf/player/player.h" #include "../rf/os/console.h" #include "../os/console.h" +#include "../misc/level.h" class EventNameMapper { std::unordered_map event_name_map; @@ -49,8 +51,6 @@ EventNameMapper event_name_mapper; const char* custom_event_names[] = { "AF_Teleport_Player", - "Clone_Entity", - "Anchor_Marker_Orient", }; enum EventType : int { @@ -264,13 +264,14 @@ CodeInjection event_load_level_turn_on_injection{ FunHook event_lookup_type_hook{ 0x004BD700, - [](const rf::String& name) { - auto it = event_name_mapper.find(name.c_str()); - if (!it) { + [] (const rf::String& name) { + const std::optional id = event_name_mapper.find(name); + if (!id) { xlog::warn("Unsupported event class: {}", name); + g_level_has_unsupported_event_classes = true; return -1; } - return it.value(); + return id.value(); }, }; @@ -330,8 +331,105 @@ ConsoleCommand2 debug_event_msg_cmd{ } }; +CodeInjection trigger_activate_linked_objects_activate_event_in_multi_patch{ + 0x004C038C, + [] (auto& regs) { + if (rf::level.version >= 300) { + const rf::Object* const obj = regs.esi; + const rf::Event* const event = static_cast(obj); + static const std::unordered_set blocked_event_types{{ + rf::EventType::Load_Level, + rf::EventType::Drop_Point_Marker, + rf::EventType::Go_Undercover, + rf::EventType::Win_PS2_Demo, + rf::EventType::Endgame, + rf::EventType::Defuse_Nuke, + rf::EventType::Play_Video, + }}; + // RF does not allow triggers to activate events in multiplayer. + // In AF levels, we allow it, unless it is an event that would be + // problematic in multiplayer. + const rf::EventType event_type = static_cast(event->event_type); + if (!blocked_event_types.contains(event_type)) { + // Allow activation. + regs.eip = 0x004C03C2; + } else { + // Do not allow activation. + regs.eip = 0x004C03D3; + } + } + } +}; + +CodeInjection EventUnhide__process_patch{ + 0x004BCDF0, + [] (const auto& regs) { + rf::Event* const event = regs.ecx; + rf::Event__process(event); + } +}; + +CodeInjection EventMakeInvulnerable__process_patch{ + 0x004BC8F0, + [] (const auto& regs) { + rf::Event* const event = regs.ecx; + rf::Event__process(event); + } +}; + +bool af_is_forward_exempt(const rf::EventType event_type) { + // These Alpine Faction events are forward exempt. + // They are not however implemented. + static const std::unordered_set forward_exempt_events{{ + rf::EventType::Set_Variable, + rf::EventType::Switch_Random, + rf::EventType::Difficulty_Gate, + rf::EventType::Sequence, + rf::EventType::Clear_Queued, + rf::EventType::Remove_Link, + rf::EventType::Add_Link, + rf::EventType::Valid_Gate, + rf::EventType::Goal_Gate, + rf::EventType::Scope_Gate, + rf::EventType::Inside_Gate, + rf::EventType::AF_When_Dead, + rf::EventType::Gametype_Gate, + rf::EventType::When_Picked_Up, + rf::EventType::Set_Entity_Flag, + rf::EventType::Light_State, + rf::EventType::World_HUD_Sprite, + rf::EventType::Set_Light_Color, + }}; + return forward_exempt_events.find(event_type) != forward_exempt_events.end(); +} + +CodeInjection event_type_forwards_messages_patch{ + 0x004B8C44, + [] (auto& regs) { + if (rf::level.version >= 300) { + const rf::EventType event_type = static_cast(regs.eax); + if (af_is_forward_exempt(event_type) + || (event_type == rf::EventType::Cyclic_Timer + && !AlpineLevelProps::instance().legacy_cyclic_timers)) { + // Do not forward messages. + regs.al = false; + regs.eip = 0x004B8C5D; + } + } + } +}; + void apply_event_patches() { + event_type_forwards_messages_patch.install(); + + // In AF levels, fix events that are broken, if `delay_timestamp` is set. + EventUnhide__process_patch.install(); + EventMakeInvulnerable__process_patch.install(); + + // In AF levels, allow triggers to activate events in multiplayer. + trigger_activate_linked_objects_activate_event_in_multi_patch.install(); + // Allow custom mesh (not used in clutter.tbl or items.tbl) in Switch_Model event switch_model_event_custom_mesh_patch.install(); switch_model_event_obj_lighting_and_physics_fix.install(); diff --git a/game_patch/object/obj_light.cpp b/game_patch/object/obj_light.cpp index 44dcf450c..b1c2dd58b 100644 --- a/game_patch/object/obj_light.cpp +++ b/game_patch/object/obj_light.cpp @@ -2,15 +2,18 @@ #include #include #include +#include #include #include "../rf/object.h" #include "../rf/item.h" #include "../rf/clutter.h" #include "../rf/gr/gr.h" #include "../rf/multi.h" +#include "../rf/level.h" #include "../rf/crt.h" #include "../os/console.h" #include "../main/main.h" +#include "../multi/multi.h" float obj_light_scale = 1.0; @@ -141,6 +144,44 @@ ConsoleCommand2 mesh_static_lighting_cmd{ "Toggle mesh static lighting calculation", }; +CodeInjection init_mesh_dynamic_light_data_patch{ + 0x0052DBD0, + [] (const auto& regs) { + const bool allow_full_bright = !(get_remote_server_info() + && !get_remote_server_info().value().allow_full_bright_entities); + if (g_game_config.full_bright_entities && allow_full_bright) { + // For Direct3d 11, we set in `gr_d3d11_mesh.cpp`. + rf::Vector3& ambient_light = addr_as_ref(0x01C3D548); + ambient_light = rf::Vector3{255.f, 255.f, 255.f}; + } + }, +}; + +ConsoleCommand2 full_bright_entities_cmd{ + "full_bright_entities", + [] { + g_game_config.full_bright_entities = !g_game_config.full_bright_entities; + g_game_config.save(); + rf::console::print( + "Full-bright entities is {}", + g_game_config.full_bright_entities + ? "enabled [may be force disabled by multiplayer servers]" + : "disabled" + ); + }, + "Toggle full-bright entities [may be force disabled by multiplayer servers]", +}; + +CodeInjection LevelLight__load_support_dynamic_lights_patch{ + 0x0045F500, + [] (auto& regs) { + // Will crash dedicated servers. + if (rf::level.version >= 300 && !rf::is_dedicated_server) { + regs.eip = 0x0045F507; + } + } +}; + void obj_light_apply_patch() { // Fix/improve items and clutters static lighting calculation: fix matrices being zero and use static lights @@ -156,4 +197,10 @@ void obj_light_apply_patch() // Commands mesh_static_lighting_cmd.register_cmd(); + full_bright_entities_cmd.register_cmd(); + + // Support full-bright entities. + init_mesh_dynamic_light_data_patch.install(); + + LevelLight__load_support_dynamic_lights_patch.install(); } diff --git a/game_patch/object/object.cpp b/game_patch/object/object.cpp index b941b7e71..0dcce7afa 100644 --- a/game_patch/object/object.cpp +++ b/game_patch/object/object.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "../rf/object.h" #include "../rf/clutter.h" @@ -10,6 +11,7 @@ #include "../rf/level.h" #include "../rf/geometry.h" #include "../rf/math/ix.h" +#include "../misc/af_options.h" #include "object.h" #include "object_private.h" @@ -187,8 +189,15 @@ CodeInjection sort_clutter_patch{ FunHook obj_create_mesh_hook{ 0x00489FE0, - [](rf::Object* objp, const char* name, rf::VMeshType type) { - rf::VMesh* mesh = obj_create_mesh_hook.call_target(objp, name, type); + [] (rf::Object* const objp, const char* name, const rf::VMeshType type) { + const auto mesh_replacement_iter = g_af_level_info_config + .mesh_replacements + .find(string_to_lower(name)); + if (mesh_replacement_iter != g_af_level_info_config.mesh_replacements.end()) { + xlog::debug("Replacing mesh {} with {}", name, mesh_replacement_iter->second); + name = mesh_replacement_iter->second.c_str(); + } + rf::VMesh* const mesh = obj_create_mesh_hook.call_target(objp, name, type); if (mesh && (rf::level.flags & rf::LEVEL_LOADED) != 0) { obj_light_init_object(objp); } diff --git a/game_patch/object/particle.cpp b/game_patch/object/particle.cpp index 58a7c33bc..56df6722c 100644 --- a/game_patch/object/particle.cpp +++ b/game_patch/object/particle.cpp @@ -7,6 +7,9 @@ #include "../rf/particle_emitter.h" #include "../rf/geometry.h" #include "../rf/multi.h" +#include "../rf/level.h" +#include "../rf/player/camera.h" +#include "../rf/player/player.h" FunHook particle_emitter_create_hook{ 0x00497CA0, @@ -34,6 +37,50 @@ CodeInjection particle_update_accel_patch{ }, }; +CallHook particle_create_level_emitter_hook{ + 0x00496DBC, + [] ( + const int pool_id, + rf::ParticleCreateInfo& pci, + rf::GRoom* const room, + rf::Vector3* const a4, + const int parent_obj, + rf::Particle** const result, + rf::ParticleEmitter* const emitter + ) { + // Make particle emitters placed into levels respect their active distance parameter. + if (rf::level.version >= 300 + && !rf::is_server + && !rf::is_dedicated_server + && !parent_obj + && emitter->uid > 0 + ) { + const rf::Vector3 camera_pos = rf::camera_get_pos(rf::local_player->cam); + const float dist = (emitter->pos - camera_pos).len(); + if (emitter->active_distance != 0.f && emitter->active_distance <= dist) { + return; + } + } + particle_create_level_emitter_hook.call_target( + pool_id, + pci, + room, + a4, + parent_obj, + result, + emitter + ); + }, +}; + FunHook particle_create_hook{ 0x00496840, [](int pool_id, rf::ParticleCreateInfo& pci, rf::GRoom* room, rf::Vector3 *a4, int parent_obj, rf::Particle** result, rf::ParticleEmitter* emitter) { @@ -108,6 +155,9 @@ void particle_do_patch() // the searched list write_mem(0x00495158, asm_opcodes::jmp_rel_short); + // Support respecting a particle emitter's active distance parameter. + particle_create_level_emitter_hook.install(); + // Recude particle emitters maximal spawn rate particle_emitter_create_hook.install(); diff --git a/game_patch/object/weapon.cpp b/game_patch/object/weapon.cpp index 55d89ed5d..ef7bbea5b 100644 --- a/game_patch/object/weapon.cpp +++ b/game_patch/object/weapon.cpp @@ -126,6 +126,40 @@ CallHook weapon_hit_wall_obj_apply_r }, }; +CodeInjection player_fire_primary_weapon_semi_auto_patch{ + 0x004A50BB, + [] (auto& regs) { + const rf::Entity* const entity = regs.esi; + if (get_remote_server_info() + && get_remote_server_info().value().click_limit + && !entity->ai.next_fire_primary.elapsed()) { + regs.eip = 0x004A58B8; + } + }, +}; + +std::optional get_fire_wait_override() { + return get_remote_server_info().and_then([] (const RemoteServerInfo& remote_server_info) { + return remote_server_info.semiauto_cool_down; + }); +} + +CodeInjection entity_fire_primary_weapon_semi_auto_patch{ + 0x004259B8, + [] (auto& regs) { + // HACKFIX: Override fire wait for stock semiauto weapons. + auto& fire_wait = regs.eax; + const int weapon_type = regs.ebx; + const rf::Entity* const entity = regs.esi; + if (get_fire_wait_override() + && rf::obj_is_player(entity) + && rf::weapon_is_semi_automatic(weapon_type) + && fire_wait == 500) { + fire_wait = get_fire_wait_override().value(); + } + }, +}; + void apply_weapon_patches() { // Fix crashes caused by too many records in weapons.tbl file @@ -157,4 +191,7 @@ void apply_weapon_patches() // Fix rockets not making damage after hitting a detail brush weapon_hit_wall_obj_apply_radius_damage_hook.install(); + + player_fire_primary_weapon_semi_auto_patch.install(); + entity_fire_primary_weapon_semi_auto_patch.install(); } diff --git a/game_patch/rf/event.h b/game_patch/rf/event.h index 154bd675c..1d7ed5884 100644 --- a/game_patch/rf/event.h +++ b/game_patch/rf/event.h @@ -24,6 +24,143 @@ namespace rf }; static_assert(sizeof(Event) == 0x2B8); + static auto& Event__process = addr_as_ref(0x004B8CE0); + + static auto& event_type_forwards_messages = addr_as_ref(0x004B8C40); + static auto& event_lookup_from_uid = addr_as_ref(0x004B6820); static auto& event_names = addr_as_ref(0x005A1A3C); + + // There are 90 built-in events. + enum EventType : int { + Play_Sound, + Slay_Object, + Remove_Object, + Invert, + Teleport, + Goto, + Goto_Player, + Look_At, + Shoot_At, + Shoot_Once, + Explode, + Play_Animation, + Play_Custom_Animation, + Heal, + Armor, + Message, + When_Dead, + Continuous_Damage, + Shake_Player, + Give_Item_To_Player, + Cyclic_Timer, + Switch_Model, + Load_Level, + Spawn_Object, + Make_Invulnerable, + Make_Walk, + Make_Fly, + Drop_Point_Marker, + Follow_Waypoints, + Follow_Player, + Set_Friendliness, + Set_Light_State, + Switch, + Swap_Textures, + Set_AI_Mode, + Goal_Create, + Goal_Check, + Goal_Set, + Attack, + Particle_State, + Set_Liquid_Depth, + Music_Start, + Music_Stop, + Bolt_State, + Set_Gravity, + Alarm_Siren, + Alarm, + Go_Undercover, + Delay, + Monitor_State, + UnHide, + Push_Region_State, + When_Hit, + Headlamp_State, + Item_Pickup_State, + Cutscene, + Strip_Player_Weapons, + Fog_State, + Detach, + Skybox_State, + Force_Monitor_Update, + Black_Out_Player, + Turn_Off_Physics, + Teleport_Player, + Holster_Weapon, + Holster_Player_Weapon, + Modify_Rotating_Mover, + Clear_Endgame_If_Killed, + Win_PS2_Demo, + Enable_Navpoint, + Play_Vclip, + Endgame, + Mover_Pause, + Countdown_Begin, + Countdown_End, + When_Countdown_Over, + Activate_Capek_Shield, + When_Enter_Vehicle, + When_Try_Exit_Vehicle, + Fire_Weapon_No_Anim, + Never_Leave_Vehicle, + Drop_Weapon, + Ignite_Entity, + When_Cutscene_Over, + When_Countdown_Reaches, + Display_Fullscreen_Image, + Defuse_Nuke, + When_Life_Reaches, + When_Armor_Reaches, + Reverse_Mover, + // Alpine Faction 1.0 events. + Set_Variable = 100, + Clone_Entity, + Set_Player_World_Collide, + Switch_Random, + Difficulty_Gate, + HUD_Message, + Play_Video, + Set_Level_Hardness, + Sequence, + Clear_Queued, + Remove_Link, + Route_Node, + Add_Link, + Valid_Gate, + Goal_Math, + Goal_Gate, + Scope_Gate, + Inside_Gate, + Anchor_Marker, + Force_Unhide, + Set_Difficulty, + Set_Fog_Far_Clip, + AF_When_Dead, + Gametype_Gate, + When_Picked_Up, + Set_Skybox, + Set_Life, + Set_Debris, + Set_Fog_Color, + Set_Entity_Flag, + AF_Teleport_Player, + Set_Item_Drop, + // Alpine Faction 1.1 events. + AF_Heal, + Anchor_Marker_Orient, + Light_State, + World_HUD_Sprite, + Set_Light_Color, + }; } diff --git a/game_patch/rf/gr/gr.h b/game_patch/rf/gr/gr.h index 76dbd3d93..12b6aee1f 100644 --- a/game_patch/rf/gr/gr.h +++ b/game_patch/rf/gr/gr.h @@ -26,6 +26,15 @@ namespace rf::gr this->alpha = a; } + static Color from_u32(const uint32_t color) { + return Color{ + static_cast((color >> 24) & 0xFF), + static_cast((color >> 16) & 0xFF), + static_cast((color >> 8) & 0xFF), + static_cast(color & 0xFF) + }; + } + bool operator==(const Color& other) const = default; }; diff --git a/game_patch/rf/object.h b/game_patch/rf/object.h index 3e14722c8..3f031324f 100644 --- a/game_patch/rf/object.h +++ b/game_patch/rf/object.h @@ -127,7 +127,7 @@ namespace rf static auto& obj_flag_dead = addr_as_ref(0x0048AB40); static auto& obj_find_root_bone_pos = addr_as_ref(0x0048AC70); static auto& obj_update_liquid_status = addr_as_ref(0x00486C30); - static auto& obj_is_player = addr_as_ref(0x004895D0); + static auto& obj_is_player = addr_as_ref(0x004895D0); static auto& obj_hide = addr_as_ref(0x0048A570); static auto& obj_light_free = addr_as_ref(0x0048B370); diff --git a/game_patch/rf/particle_emitter.h b/game_patch/rf/particle_emitter.h index ae539ccc2..ed0f12a96 100644 --- a/game_patch/rf/particle_emitter.h +++ b/game_patch/rf/particle_emitter.h @@ -136,7 +136,7 @@ struct ParticleEmitter float time_to_change; float current_state_time; bool active; - int field_144; + int active_distance; ParticleEmitter *next; ParticleEmitter *prev; ParticleEmitter *next_entity_emitter; diff --git a/game_patch/rf/sound/sound.h b/game_patch/rf/sound/sound.h index 750a27320..e40d30893 100644 --- a/game_patch/rf/sound/sound.h +++ b/game_patch/rf/sound/sound.h @@ -88,7 +88,8 @@ namespace rf static auto snd_change_3d = addr_as_ref(0x005058C0); static auto snd_calculate_2d_from_3d_info = addr_as_ref(0x00505740); static auto snd_update_sounds = addr_as_ref(0x00505EC0); - + static auto snd_get_handle = addr_as_ref(0x005054B0); + static auto snd_pc_play = addr_as_ref(0x005439D0); static auto snd_pc_play_looping = addr_as_ref(0x00543A80); static auto snd_pc_play_3d = addr_as_ref(0x00544180); diff --git a/game_patch/rf/v3d.h b/game_patch/rf/v3d.h index 3d363d7ca..dfc8dba95 100644 --- a/game_patch/rf/v3d.h +++ b/game_patch/rf/v3d.h @@ -145,5 +145,6 @@ namespace rf MRF_SCANNER_1 = 1, MRF_SCANNER_2 = 8, MRF_AMBIENT_COLOR = 0x80, + MRF_FIRST_PERSON = 0x400, }; } diff --git a/resources/CMakeLists.txt b/resources/CMakeLists.txt index 355e34032..c9227ed02 100644 --- a/resources/CMakeLists.txt +++ b/resources/CMakeLists.txt @@ -75,7 +75,7 @@ add_packfile(dashfaction.vpp images/pls_airlockmat01-mip2.tga images/consolebutton2A.tga images/gore1.tga # https://www.filterforge.com/filters/7459.html - images/bigboom.vbm # Dummy bitmap to silence warning + images/dummy/bigboom.vbm # Dummy bitmap to silence warning fonts/biggerfont.vf fonts/regularfont.ttf @@ -95,9 +95,10 @@ add_packfile(dashfaction.vpp meshes/meatchunk4.v3m meshes/meatchunk5.v3m + audio/af_killsound1.wav # Dummy audio files to silence warnings - audio/fp_shotgun_reload.wav - "audio/Laser loop.wav" + audio/dummy/fp_shotgun_reload.wav + "audio/dummy/Laser loop.wav" standard_vs:${CMAKE_BINARY_DIR}/shaders/standard_vs.bin character_vs:${CMAKE_BINARY_DIR}/shaders/character_vs.bin diff --git a/resources/audio/af_killsound1.wav b/resources/audio/af_killsound1.wav new file mode 100644 index 000000000..3c135b21b Binary files /dev/null and b/resources/audio/af_killsound1.wav differ diff --git a/resources/audio/Laser loop.wav b/resources/audio/dummy/Laser loop.wav similarity index 100% rename from resources/audio/Laser loop.wav rename to resources/audio/dummy/Laser loop.wav diff --git a/resources/audio/fp_shotgun_reload.wav b/resources/audio/dummy/fp_shotgun_reload.wav similarity index 100% rename from resources/audio/fp_shotgun_reload.wav rename to resources/audio/dummy/fp_shotgun_reload.wav diff --git a/resources/images/bigboom.vbm b/resources/images/bigboom.vbm deleted file mode 100644 index 8956ddb47..000000000 Binary files a/resources/images/bigboom.vbm and /dev/null differ diff --git a/resources/images/dummy/bigboom.vbm b/resources/images/dummy/bigboom.vbm new file mode 100644 index 000000000..e6d360c16 Binary files /dev/null and b/resources/images/dummy/bigboom.vbm differ diff --git a/resources/licensing-info.txt b/resources/licensing-info.txt index 1274e7332..ff5dca6e1 100644 --- a/resources/licensing-info.txt +++ b/resources/licensing-info.txt @@ -18,6 +18,16 @@ during each Dash Faction release process by the owner of the project. Dash Faction uses some open-source third-party components. Their licenses can be found below. +--------------------------------- +Assets +--------------------------------- + +soneproject: +* af_killsound1.wav + Title: ecofuture3 + Source: https://freesound.org/s/346425 + License: https://creativecommons.org/licenses/by/3.0 + --------------------------------- Contributors ---------------------------------