Skip to content

Commit

Permalink
Add random level rotation option for dedicated servers (#330)
Browse files Browse the repository at this point in the history
* Add "vote rand" for random nextmap vote, and add "map_rand" to server command list

* update changelog, add new command to readme, add new command to rcon whitelist

* Added $DF Randomize Rotation to dedicated server config, fixed logic error with map_rand command, updated readme, updated changelog

* fix typo in readme

* Adjusted option name, optimized and consolidated code, swapped srand for rand

* Add alias vote random, fix changelog typo

* add "shuffle_maps" command to shuffle maplist, make readme more clear, update changelog

* better formatting for shuffle_level_array function

* avoid stomping current level index

* optimize random selection logic and properly handle case of just 1 map in the rotation

* Add DF Dynamic Rotation dedicated server config option, minor formatting improvement

* removed "shuffle_maps" command (unnecessary)

* Fix ordering of $DF options in readme, and made order of options in dedicated server config file make more sense

* shuffle rotation on server start, deleted Random Level Order option, introduced vastly improved RNG method (initialized in main.cpp so can be used elsewhere)

* utilize std::shuffle for shuffle_level_array

* adjusted random methods to consolidate, optimize, and improve type conversion safety

* improve get_rand_level_filename logic, clean up code

* fix type conversion regression, remove debug logging and unneeded var

* fix infinite loop

* Update game_patch/multi/server.cpp

Co-authored-by: is-this-c <87069698+is-this-c@users.noreply.github.com>

* reverting change causing error

* resolve pointer issue

* remove unneeded variable

* use g_ naming convention for rng

* Fix code structure

* Rename dynamic rotation to random rotation

Also reduce log level

* Fix changelog entries for random rotation being in wrong section

---------

Co-authored-by: Goober <gooberCP@gmail.com>
Co-authored-by: is-this-c <87069698+is-this-c@users.noreply.github.com>
  • Loading branch information
3 people authored Feb 9, 2025
1 parent 6775ae0 commit 95162ef
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Name | Description
`map_ext` | extend round
`map_rest` | restart current level
`map_next` | load next level
`map_rand` | load random level from rotation
`map_prev` | load previous level
`kill_limit value` | set kill limit
`time_limit value` | set time limit
Expand Down Expand Up @@ -152,6 +153,8 @@ Configuration example:
$DF Vote Restart: true
// Enable vote next
$DF Vote Next: true
// Enable vote random
$DF Vote Random: true
// Enable vote previous
$DF Vote Previous: true
// Duration of player invulnerability after respawn in ms (default is the same as in stock RF - 1500)
Expand Down Expand Up @@ -196,6 +199,8 @@ Configuration example:
$DF Force Player Character: "enviro_parker"
// Maximal horizontal FOV that clients can use for level rendering (unlimited by default)
//$DF Max FOV: 125
// Shuffle the list of levels when the server starts and at the end of each full rotation
$DF Random Rotation: false
// If enabled a private message with player statistics is sent after each round.
//$DF Send Player Stats Message: true
// Send a chat message to players when they join the server ($PLAYER is replaced by player name)
Expand Down
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ Version 1.9.0 (not released yet)
- Add `spectate_mode_follow_killer` command (when player you are spectating dies, spectate their killer)
- Remove level editor popups that stop user from navigating between modes until rebuilding
- Add `muzzle_flash` command
- Add `map_rand` command to change to a random level on the rotation
- Add `vote rand` vote type to initiate a level change to a random level on the rotation
- Add `$DF Random Rotation` option in dedicated server config

[@is-this-c](https://github.com/is-this-c)
- Support `©` and `` in TrueType fonts
Expand Down
11 changes: 11 additions & 0 deletions game_patch/main/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

GameConfig g_game_config;
HMODULE g_hmodule;
std::mt19937 g_rng;

CallHook<void()> rf_init_hook{
0x004B27CD,
Expand Down Expand Up @@ -269,6 +270,13 @@ void init_logging()
xlog::info("Command line: {}", GetCommandLineA());
}

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));
}

void log_system_info()
{
try {
Expand Down Expand Up @@ -338,6 +346,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
5 changes: 5 additions & 0 deletions 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;

#ifdef _WINDOWS_
extern HMODULE g_hmodule;
#endif
18 changes: 18 additions & 0 deletions game_patch/multi/commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "../rf/level.h"
#include "../rf/player/player.h"
#include "../rf/crt.h"
#include "server.h"
#include "multi.h"
#include <patch_common/AsmWriter.h>
#include <patch_common/CallHook.h>
Expand Down Expand Up @@ -43,6 +44,11 @@ void load_prev_level()
}
}

void load_rand_level()
{
rf::multi_change_level(get_rand_level_filename());
}

bool validate_is_server()
{
if (!rf::is_server) {
Expand Down Expand Up @@ -97,6 +103,17 @@ ConsoleCommand2 map_next_cmd{
"Load next level",
};

ConsoleCommand2 map_rand_cmd{
"map_rand",
[]() {
if (validate_is_server() && validate_not_limbo()) {
rf::multi_chat_say("\xA6 Loading random level from rotation", false);
load_rand_level();
}
},
"Load random level from rotation",
};

ConsoleCommand2 map_prev_cmd{
"map_prev",
[]() {
Expand Down Expand Up @@ -194,6 +211,7 @@ void init_server_commands()
map_ext_cmd.register_cmd();
map_rest_cmd.register_cmd();
map_next_cmd.register_cmd();
map_rand_cmd.register_cmd();
map_prev_cmd.register_cmd();

AsmWriter(0x0047B6F0).jmp(ban_cmd_handler_hook);
Expand Down
52 changes: 52 additions & 0 deletions game_patch/multi/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const char* g_rcon_cmd_whitelist[] = {
"map_ext",
"map_rest",
"map_next",
"map_rand",
"map_prev",
};

Expand Down Expand Up @@ -74,6 +75,7 @@ void load_additional_server_config(rf::Parser& parser)
parse_vote_config("Vote Extend", g_additional_server_config.vote_extend, parser);
parse_vote_config("Vote Restart", g_additional_server_config.vote_restart, parser);
parse_vote_config("Vote Next", g_additional_server_config.vote_next, parser);
parse_vote_config("Vote Random", g_additional_server_config.vote_rand, parser);
parse_vote_config("Vote Previous", g_additional_server_config.vote_previous, parser);
if (parser.parse_optional("$DF Spawn Protection Duration:")) {
g_additional_server_config.spawn_protection_duration_ms = parser.parse_uint();
Expand Down Expand Up @@ -169,6 +171,10 @@ void load_additional_server_config(rf::Parser& parser)
}
}

if (parser.parse_optional("$DF Random Rotation:")) {
g_additional_server_config.random_rotation = parser.parse_bool();
}

if (parser.parse_optional("$DF Send Player Stats Message:")) {
g_additional_server_config.stats_message_enabled = parser.parse_bool();
}
Expand Down Expand Up @@ -208,6 +214,11 @@ CodeInjection dedicated_server_load_config_patch{
auto& parser = *reinterpret_cast<rf::Parser*>(regs.esp - 4 + 0x4C0 - 0x470);
load_additional_server_config(parser);

// if random rotation is on, shuffle rotation on server launch
if (g_additional_server_config.random_rotation) {
shuffle_level_array();
}

// Insert server name in window title when hosting dedicated server
std::string wnd_name;
wnd_name.append(rf::netgame.name.c_str());
Expand Down Expand Up @@ -310,6 +321,32 @@ static void send_private_message_with_stats(rf::Player* player)
send_chat_line_packet(str.c_str(), player);
}

void shuffle_level_array()
{
std::ranges::shuffle(rf::netgame.levels, g_rng);
xlog::info("Shuffled level rotation");
}

const char* get_rand_level_filename()
{
const std::size_t num_levels = rf::netgame.levels.size();

if (num_levels <= 1) {
// nowhere else to go, we're staying here!
return rf::level_filename_to_load.c_str();
}

std::uniform_int_distribution<std::size_t> dist_levels(0, num_levels - 1);
std::size_t rand_level_index = dist_levels(g_rng);

// avoid selecting current level filename (unless it appears more than once on map list)
if (rf::netgame.levels[rand_level_index] == rf::level_filename_to_load) {
rand_level_index = (rand_level_index + 1) % num_levels;
}

return rf::netgame.levels[rand_level_index].c_str();
}

bool handle_server_chat_command(std::string_view server_command, rf::Player* sender)
{
auto [cmd_name, cmd_arg] = strip_by_space(server_command);
Expand Down Expand Up @@ -664,6 +701,18 @@ CodeInjection multi_limbo_init_injection{
},
};

CodeInjection multi_level_init_injection{
0x0046E450,
[]() {
if (g_additional_server_config.random_rotation && rf::netgame.current_level_index ==
rf::netgame.levels.size() - 1 && rf::netgame.levels.size() > 1) {
// if this is the last level in the list and dynamic rotation is on, shuffle
xlog::info("Reached end of level rotation, shuffling");
shuffle_level_array();
}
},
};

void server_init()
{
// Override rcon command whitelist
Expand Down Expand Up @@ -729,6 +778,9 @@ void server_init()

// Reduce limbo duration if server is empty
multi_limbo_init_injection.install();

// Shuffle rotation when the last map in the list is loaded
multi_level_init_injection.install();
}

void server_do_frame()
Expand Down
2 changes: 2 additions & 0 deletions game_patch/multi/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ bool check_server_chat_command(const char* msg, rf::Player* sender);
bool server_is_saving_enabled();
void server_reliable_socket_ready(rf::Player* player);
bool server_weapon_items_give_full_ammo();
const char* get_rand_level_filename();
void shuffle_level_array();
3 changes: 3 additions & 0 deletions game_patch/multi/server_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ struct ServerAdditionalConfig
VoteConfig vote_extend;
VoteConfig vote_restart;
VoteConfig vote_next;
VoteConfig vote_rand;
VoteConfig vote_previous;
int spawn_protection_duration_ms = 1500;
std::optional<float> spawn_life;
Expand All @@ -50,6 +51,7 @@ struct ServerAdditionalConfig
std::optional<float> max_fov;
int anticheat_level = 0;
bool stats_message_enabled = true;
bool random_rotation = false;
std::string welcome_message;
bool weapon_items_give_full_ammo = false;
float kill_reward_health = 0.0f;
Expand All @@ -69,6 +71,7 @@ void init_server_commands();
void extend_round_time(int minutes);
void restart_current_level();
void load_next_level();
void load_rand_level();
void load_prev_level();
void server_vote_on_limbo_state_enter();
void process_delayed_kicks();
Expand Down
26 changes: 26 additions & 0 deletions game_patch/multi/votes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,30 @@ struct VoteNext : public Vote
}
};

struct VoteRandom : public Vote
{
[[nodiscard]] std::string get_title() const override
{
return "LOAD RANDOM LEVEL";
}

void on_accepted() override
{
send_chat_line_packet("\xA6 Vote passed: loading random level from rotation", nullptr);
load_rand_level();
}

[[nodiscard]] bool is_allowed_in_limbo_state() const override
{
return false;
}

[[nodiscard]] const VoteConfig& get_config() const override
{
return g_additional_server_config.vote_rand;
}
};

struct VotePrevious : public Vote
{
[[nodiscard]] std::string get_title() const override
Expand Down Expand Up @@ -449,6 +473,8 @@ void handle_vote_command(std::string_view vote_name, std::string_view vote_arg,
g_vote_mgr.StartVote<VoteRestart>(vote_arg, sender);
else if (vote_name == "next")
g_vote_mgr.StartVote<VoteNext>(vote_arg, sender);
else if (vote_name == "random" || vote_name == "rand")
g_vote_mgr.StartVote<VoteRandom>(vote_arg, sender);
else if (vote_name == "previous" || vote_name == "prev")
g_vote_mgr.StartVote<VotePrevious>(vote_arg, sender);
else if (vote_name == "yes" || vote_name == "y")
Expand Down

0 comments on commit 95162ef

Please sign in to comment.