Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add critical hits to multiplayer #283

Closed
wants to merge 13 commits into from
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,22 @@ Configuration example:
$DF Hitsounds: true
// Sound used for hit notification
+Sound ID: 29
// max sound packets per second - keep it low to save bandwidth
// Max sound packets per second - keep it low to save bandwidth
+Rate Limit: 10
// Enable critical hits
$DF Critical Hits: false
// Sound used for hit notification
+Sound ID: 35
// Max sound packets per second - keep it low to save bandwidth
+Rate Limit: 10
// Duration of damage amp reward on a critical hit
+Reward Duration: 1500
// Percentage chance of a critical hit
+Base Chance Percent: 0.1
// Enable dynamic chance bonus based on damage dealt in current life
+Use Dynamic Chance Bonus: true
// Amount of damage to deal in current life for the max dynamic chance bonus (+ 0.1)
+Dynamic Chance Damage Ceiling: 1200
// Replace all "Shotgun" items with "rail gun" items when loading RFLs
$DF Item Replacement: "Shotgun" "rail gun"
// If enabled players are given full ammo when picking up weapon items, can be useful with the Weapons Stay standard option
Expand Down
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- Properly handle WM_PAINT in dedicated server, may improve performance (DF bug)
- Add `$DF Critical Hits` option in dedicated server config

Version 1.8.0 (released 2022-09-17)
-----------------------------------
Expand Down
12 changes: 12 additions & 0 deletions game_patch/main/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@
GameConfig g_game_config;
HMODULE g_hmodule;

std::mt19937 g_rng;

void initialize_random_generator()
{
// seed rng with the current time
auto seed = std::chrono::steady_clock::now().time_since_epoch().count();
g_rng.seed(static_cast<unsigned long>(seed));
}

CallHook<void()> rf_init_hook{
0x004B27CD,
[]() {
Expand Down Expand Up @@ -338,6 +347,9 @@ extern "C" DWORD __declspec(dllexport) Init([[maybe_unused]] void* unused)
init_logging();
init_crash_handler();

// Init random number generator
initialize_random_generator();

// Enable Data Execution Prevention
if (!SetProcessDEPPolicy(PROCESS_DEP_ENABLE))
xlog::warn("SetProcessDEPPolicy failed (error {})", GetLastError());
Expand Down
7 changes: 6 additions & 1 deletion game_patch/main/main.h
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
#pragma once

#include <random>
#include <common/config/GameConfig.h>

extern GameConfig g_game_config;

// random number generator
extern std::mt19937 g_rng;
void initialize_random_generator();

#ifdef _WINDOWS_
extern HMODULE g_hmodule;
#endif
1 change: 1 addition & 0 deletions game_patch/misc/player.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ struct PlayerAdditionalData
bool is_browser = false;
bool is_muted = false;
int last_hitsound_sent_ms = 0;
int last_critsound_sent_ms = 0;
std::map<std::string, PlayerNetGameSaveData> saves;
rf::Vector3 last_teleport_pos;
rf::TimestampRealtime last_teleport_timestamp;
Expand Down
7 changes: 7 additions & 0 deletions game_patch/multi/multi.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#pragma once

#include <optional>
#include <xlog/xlog.h>
#include "server_internal.h"
#include "../rf/player/player.h"
#include "../rf/os/timer.h"

struct PlayerStatsNew : rf::PlayerLevelStats
{
Expand All @@ -13,6 +16,7 @@ struct PlayerStatsNew : rf::PlayerLevelStats
float num_shots_fired;
float damage_received;
float damage_given;
float damage_given_current_life;

void inc_kills()
{
Expand All @@ -25,6 +29,7 @@ struct PlayerStatsNew : rf::PlayerLevelStats
{
++num_deaths;
current_streak = 0;
damage_given_current_life = 0;
}

void add_shots_hit(float add)
Expand All @@ -47,6 +52,7 @@ struct PlayerStatsNew : rf::PlayerLevelStats
void add_damage_given(float damage)
{
damage_given += damage;
damage_given_current_life += damage;
}

[[nodiscard]] float calc_accuracy() const
Expand All @@ -67,6 +73,7 @@ struct PlayerStatsNew : rf::PlayerLevelStats
max_streak = 0;
damage_received = 0;
damage_given = 0;
damage_given_current_life = 0;
}
};

Expand Down
84 changes: 77 additions & 7 deletions game_patch/multi/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "../main/main.h"
#include <common/utils/list-utils.h>
#include "../rf/player/player.h"
#include "../rf/misc.h"
#include "../rf/multi.h"
#include "../rf/parse.h"
#include "../rf/weapon.h"
Expand Down Expand Up @@ -96,6 +97,28 @@ void load_additional_server_config(rf::Parser& parser)
}
}

if (parser.parse_optional("$DF Critical Hits:")) {
g_additional_server_config.critical_hits.enabled = parser.parse_bool();
if (parser.parse_optional("+Attacker Sound ID:")) {
g_additional_server_config.critical_hits.sound_id = parser.parse_uint();
}
if (parser.parse_optional("+Rate Limit:")) {
g_additional_server_config.critical_hits.rate_limit = parser.parse_uint();
}
if (parser.parse_optional("+Reward Duration:")) {
g_additional_server_config.critical_hits.reward_duration = parser.parse_uint();
}
if (parser.parse_optional("+Base Chance Percent:")) {
g_additional_server_config.critical_hits.base_chance = parser.parse_float();
}
if (parser.parse_optional("+Use Dynamic Chance Bonus:")) {
g_additional_server_config.critical_hits.dynamic_scale = parser.parse_bool();
}
if (parser.parse_optional("+Dynamic Chance Damage Ceiling:")) {
g_additional_server_config.critical_hits.dynamic_damage_for_max_bonus = parser.parse_float();
}
}

while (parser.parse_optional("$DF Item Replacement:")) {
rf::String old_item;
rf::String new_item;
Expand Down Expand Up @@ -369,26 +392,39 @@ CodeInjection detect_browser_player_patch{
},
};

void send_hit_sound_packet(rf::Player* target)
void send_sound_packet(rf::Player* target, int& last_sent_time, int rate_limit, int sound_id)
{
// rate limiting - max 5 per second
// Rate limiting - max `rate_limit` times per second
int now = rf::timer_get(1000);
auto& pdata = get_player_additional_data(target);
if (now - pdata.last_hitsound_sent_ms < 1000 / g_additional_server_config.hit_sounds.rate_limit) {
if (now - last_sent_time < 1000 / rate_limit) {
return;
}
pdata.last_hitsound_sent_ms = now;
last_sent_time = now;

// Send sound packet
RF_SoundPacket packet;
packet.header.type = RF_GPT_SOUND;
packet.header.size = sizeof(packet) - sizeof(packet.header);
packet.sound_id = g_additional_server_config.hit_sounds.sound_id;
packet.sound_id = sound_id;
// FIXME: it does not work on RF 1.21
packet.pos.x = packet.pos.y = packet.pos.z = std::numeric_limits<float>::quiet_NaN();
rf::multi_io_send(target, &packet, sizeof(packet));
}

void send_hit_sound_packet(rf::Player* target)
{
auto& pdata = get_player_additional_data(target);
send_sound_packet(target, pdata.last_hitsound_sent_ms, g_additional_server_config.hit_sounds.rate_limit,
g_additional_server_config.hit_sounds.sound_id);
}

void send_critical_hit_packet(rf::Player* target)
{
auto& pdata = get_player_additional_data(target);
send_sound_packet(target, pdata.last_critsound_sent_ms, g_additional_server_config.critical_hits.rate_limit,
g_additional_server_config.critical_hits.sound_id);
}

FunHook<float(rf::Entity*, float, int, int, int)> entity_damage_hook{
0x0041A350,
[](rf::Entity* damaged_ep, float damage, int killer_handle, int damage_type, int killer_uid) {
Expand All @@ -400,6 +436,40 @@ FunHook<float(rf::Entity*, float, int, int, int)> entity_damage_hook{
if (damage == 0.0f) {
return 0.0f;
}

// Check if this is a crit
if (g_additional_server_config.critical_hits.enabled) {
float base_chance = g_additional_server_config.critical_hits.base_chance;
float bonus_chance = 0.0f;

// calculate bonus chance
if (g_additional_server_config.critical_hits.dynamic_scale && killer_player->stats) {
auto* killer_stats = static_cast<PlayerStatsNew*>(killer_player->stats);

bonus_chance = 0.1f * std::min(killer_stats->damage_given_current_life /
g_additional_server_config.critical_hits.dynamic_damage_for_max_bonus, 1.0f);
}

float critical_hit_chance = base_chance + bonus_chance;
xlog::debug("Critical hit chance: {:.2f}", critical_hit_chance);

std::uniform_real_distribution<float> dist_crit(0.0f, 1.0f);
float random_value = dist_crit(g_rng);
if (random_value < critical_hit_chance) {

// Apply amp modifier to the critical hit
damage *= (!rf::multi_powerup_has_player(killer_player, 1)) ? rf::g_multi_damage_modifier : 1.0f;

// On a crit, add amp for a duration
int amp_time_to_add = rf::multi_powerup_get_time_until(killer_player, 1) +
g_additional_server_config.critical_hits.reward_duration;

rf::multi_powerup_add(killer_player, 1, amp_time_to_add);

// Notify with sound
send_critical_hit_packet(killer_player);
}
}
}

float real_damage = entity_damage_hook.call_target(damaged_ep, damage, killer_handle, damage_type, killer_uid);
Expand Down Expand Up @@ -674,7 +744,7 @@ void server_init()
// Detect if player joining to the server is a browser
detect_browser_player_patch.install();

// Hit sounds
// Critical hits and hit sounds
entity_damage_hook.install();

// Item replacements
Expand Down
12 changes: 12 additions & 0 deletions game_patch/multi/server_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ struct HitSoundsConfig
int rate_limit = 10;
};

struct CriticalHitsConfig
{
bool enabled = false;
int sound_id = 35;
int rate_limit = 10;
int reward_duration = 1500;
float base_chance = 0.1f;
bool dynamic_scale = true;
float dynamic_damage_for_max_bonus = 1200.0f;
};

struct ServerAdditionalConfig
{
VoteConfig vote_kick;
Expand All @@ -38,6 +49,7 @@ struct ServerAdditionalConfig
std::optional<float> spawn_life;
std::optional<float> spawn_armor;
HitSoundsConfig hit_sounds;
CriticalHitsConfig critical_hits;
std::map<std::string, std::string> item_replacements;
std::string default_player_weapon;
std::optional<int> default_player_weapon_ammo;
Expand Down
5 changes: 4 additions & 1 deletion game_patch/rf/misc.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ namespace rf
static auto& default_player_weapon = addr_as_ref<String>(0x007C7600);

static auto& get_file_checksum = addr_as_ref<unsigned(const char* filename)>(0x00436630);
}

static auto& g_multi_damage_modifier = addr_as_ref<float>(0x0059F7E0);

}
4 changes: 4 additions & 0 deletions game_patch/rf/multi.h
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ namespace rf
static auto& multi_kill_local_player = addr_as_ref<void()>(0x004757A0);
static auto& send_game_info_req_packet = addr_as_ref<void(const NetAddr& addr)>(0x0047B450);
static auto& multi_entity_is_female = addr_as_ref<bool(int mp_character_idx)>(0x004762C0);
static auto& multi_powerup_add = addr_as_ref<void(Player* pp, int powerup_type, int time_ms)>(0x00480050);
static auto& multi_powerup_has_player = addr_as_ref<bool(Player* pp, int powerup_type)>(0x004802B0);
static auto& multi_powerup_get_time_until = addr_as_ref<int(Player* pp, int powerup_type)>(0x004802D0);


static auto& netgame = addr_as_ref<NetGameInfo>(0x0064EC28);
static auto& is_multi = addr_as_ref<bool>(0x0064ECB9);
Expand Down