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

Expose and expand respawn point selection logic options for dedicated server operators #284

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2241e03
Initial commit
GooberRF Oct 3, 2024
dd3a809
Add rng to main
GooberRF Oct 3, 2024
2d0ac02
Merge branch '_rng' into newrespawns
GooberRF Oct 3, 2024
7dd89db
support with these defaults:
GooberRF Oct 4, 2024
809ca71
refactor some code and remove unneeded code
GooberRF Oct 4, 2024
77d295c
handle edge cases
GooberRF Oct 4, 2024
2fbfe50
update logging
GooberRF Oct 5, 2024
22476ec
Goob
GooberRF Oct 5, 2024
5a17790
use g_ convention
GooberRF Oct 7, 2024
462cfd3
Merge branch '_rng' into newrespawns
GooberRF Oct 7, 2024
6dac0d5
update _g for rng
GooberRF Oct 7, 2024
3c1e629
clean up
GooberRF Oct 7, 2024
ea4b6d3
clean up
GooberRF Oct 11, 2024
eb41ebd
add +Respect Team Spawns, fix bug in team modes with avoid enemies on…
GooberRF Oct 11, 2024
274fbc2
refactor spawn point creation code, remove unneeded logging
GooberRF Oct 11, 2024
c280d9d
commit and push
GooberRF Oct 11, 2024
4a527c5
clean up
GooberRF Oct 11, 2024
c0b9d96
only reset new spawn point array if using new spawn logic
GooberRF Oct 11, 2024
663a923
overhaul logic
GooberRF Oct 25, 2024
2214e0c
overhaul2
GooberRF Oct 25, 2024
b6bd23a
update readme and changelog
GooberRF Oct 25, 2024
aa71999
cleanup
GooberRF Oct 25, 2024
7197a40
fix logic for team modes where spawn points have both team flags set
GooberRF Oct 25, 2024
de4aab3
Merge branch 'master' into newrespawns
GooberRF Oct 25, 2024
1cbf69d
update changelog
GooberRF Oct 25, 2024
658a4b0
update changelog
GooberRF Oct 25, 2024
259c42f
Merge branch 'newrespawns' of https://github.com/GooberRF/dashfaction…
GooberRF Oct 25, 2024
9b4457a
add +Allowed Respawn Items
GooberRF Oct 26, 2024
4aeec48
team associate item spawns based on proximity to ctf flags
GooberRF Oct 26, 2024
27f7cfd
guess at center of map and point generated spawn points at it
GooberRF Oct 26, 2024
caf59ed
cleanup
GooberRF Oct 26, 2024
35c0c9b
add threshold to items as spawns
GooberRF Oct 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ Configuration example:
$DF Vote Next: true
// Enable vote previous
$DF Vote Previous: true
// Use new improved spawn point selection logic. Full randomness if optional settings are turned off
$DF Use New Spawn Logic: false
// Attempt to avoid spawning players at the same location twice in a row
+Try Avoid Last: true
// Instead of randomness, always spawn players as far as possible from any enemies
+Try Avoid Enemies: false
// If off, disregard team-specific flags on spawn points (only relevant in team modes)
+Respect Team Spawns: true
// Duration of player invulnerability after respawn in ms (default is the same as in stock RF - 1500)
$DF Spawn Protection Duration: 1500
// Initial player life (health) after spawn
Expand Down
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ 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 improved and more flexible respawn point selection method for multiplayer
- Adjust behaviour in multi if map has no spawn points, now spawns players at Player Start position

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 @@ -26,6 +26,7 @@ struct PlayerAdditionalData
std::map<std::string, PlayerNetGameSaveData> saves;
rf::Vector3 last_teleport_pos;
rf::TimestampRealtime last_teleport_timestamp;
int last_spawn_point_index = -1;
GooberRF marked this conversation as resolved.
Show resolved Hide resolved
};

void find_player(const StringMatcher& query, std::function<void(rf::Player*)> consumer);
Expand Down
271 changes: 266 additions & 5 deletions game_patch/multi/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <algorithm>
#include <limits>
#include <format>
#include <numeric>
#include <windows.h>
#include <winsock2.h>
#include "server.h"
Expand Down Expand Up @@ -42,9 +43,16 @@ const char* g_rcon_cmd_whitelist[] = {
"map_prev",
};

std::vector<std::shared_ptr<RespawnPoint>> respawn_points;
GooberRF marked this conversation as resolved.
Show resolved Hide resolved

ServerAdditionalConfig g_additional_server_config;
std::string g_prev_level;

static bool multi_is_team_game_type()
{
return rf::multi_get_game_type() != rf::NG_TYPE_DM;
}

void parse_vote_config(const char* vote_name, VoteConfig& config, rf::Parser& parser)
{
std::string vote_option_name = std::format("$DF {}:", vote_name);
Expand Down Expand Up @@ -74,6 +82,20 @@ void load_additional_server_config(rf::Parser& 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 Previous", g_additional_server_config.vote_previous, parser);

if (parser.parse_optional("$DF Use New Spawn Logic:")) {
g_additional_server_config.random_spawns.enabled = parser.parse_bool();
if (parser.parse_optional("+Try Avoid Last:")) {
g_additional_server_config.random_spawns.try_avoid_last = parser.parse_bool();
}
if (parser.parse_optional("+Try Avoid Enemies:")) {
g_additional_server_config.random_spawns.try_avoid_enemies = parser.parse_bool();
}
if (parser.parse_optional("+Respect Team Spawns:")) {
g_additional_server_config.random_spawns.respect_team_spawns = parser.parse_bool();
}
}

if (parser.parse_optional("$DF Spawn Protection Duration:")) {
g_additional_server_config.spawn_protection_duration_ms = parser.parse_uint();
}
Expand Down Expand Up @@ -554,6 +576,241 @@ static bool check_player_ac_status([[maybe_unused]] rf::Player* player)
return true;
}

// add a new spawn point
FunHook<void(const char*, uint8_t, const rf::Vector3*, rf::Matrix3*, bool, bool, bool)> multi_respawn_create_point_hook{
0x00470190,
[](const char* name, uint8_t team, const rf::Vector3* pos, rf::Matrix3* orient, bool RedTeam, bool blue_team,
bool bot) {
if (g_additional_server_config.random_spawns.enabled) {
respawn_points.emplace_back(std::make_shared<RespawnPoint>(
RespawnPoint{
.name = name,
.team = team,
.position = *pos,
.orientation = *orient,
.redTeam = RedTeam,
.blueTeam = blue_team,
.bot = bot
}
));

// tick up spawn point counter
rf::multi_respawn_num_points += 1;

// log spawn point creation info (avoid unneeded var if not debugging)
#ifdef DEBUG
const auto& respawn_point = respawn_points.back();

xlog::debug("New spawn point! Name: {}, Team: {}, RedTeam: {}, BlueTeam: {}, Bot: {}", respawn_point->name,
respawn_point->team, respawn_point->redTeam, respawn_point->blueTeam, respawn_point->bot);
if (pos) {
xlog::debug("Position: ({}, {}, {})", pos->x, pos->y, pos->z);
}
xlog::debug("Current num spawn points: {}", rf::multi_respawn_num_points);
#endif
}
else {
// if new spawn logic is off, maintain stock behaviour
multi_respawn_create_point_hook.call_target(name, team, pos, orient, RedTeam, blue_team, bot);
}
}
};

// new spawn point selection logic
std::shared_ptr<RespawnPoint> select_respawn_point_new(rf::Player* pp)
{
if (respawn_points.empty()) {
xlog::warn("No spawn points are available!");
return {nullptr};
}

auto& pdata = get_player_additional_data(pp);
int team = pp->team;
int last_index = pdata.last_spawn_point_index;
bool is_team_game = multi_is_team_game_type();
bool avoid_last = g_additional_server_config.random_spawns.try_avoid_last;
bool avoid_enemies = g_additional_server_config.random_spawns.try_avoid_enemies;
bool respect_team_spawns = g_additional_server_config.random_spawns.respect_team_spawns;

xlog::debug(
"Giving {} a spawn. They are on team {}, last index was {}. We {}avoiding last, and {}avoiding enemies.",
pp->name, (team ? "blue" : "red"), last_index, (avoid_last ? "" : "NOT "), (avoid_enemies ? "" : "NOT "));

// in team modes, restrict available spawns to our team
std::vector<std::size_t> valid_team_spawns;
if (is_team_game && respect_team_spawns) {
for (std::size_t i = 0; i < respawn_points.size(); ++i) {
const auto& spawn_point = respawn_points[i];
// Filter for matching team spawn points
if ((team == 0 && spawn_point->redTeam) || (team == 1 && spawn_point->blueTeam)) {
valid_team_spawns.push_back(i);
}
}

if (valid_team_spawns.empty()) {
xlog::warn("No spawn points are available for the {} team!", (team ? "blue" : "red"));
}
}

// avoiding enemies
if (avoid_enemies && (!valid_team_spawns.empty() || !is_team_game)) {
std::optional<std::size_t> furthest_spawn_index;
std::optional<std::size_t> second_furthest_spawn_index;
float max_distance = 0.0f;
float second_max_distance = 0.0f;
auto player_list = SinglyLinkedList{rf::player_list};
bool has_enemies = false;

if (is_team_game) {
// in tdm/ctf, check if the other team has players
for (auto& player : player_list) {
if (!rf::player_is_dead(&player) && &player != pp && player.team != team) {
has_enemies = true;
break;
}
}
}
else {
// in dm, any other player is an enemy
for (auto& player : player_list) {
if (!rf::player_is_dead(&player) && &player != pp) {
has_enemies = true;
break;
}
}
}

if (has_enemies) {
std::vector<std::size_t> spawn_pool;

// check if we have team spawns (we only will in team modes)
if (respect_team_spawns && !valid_team_spawns.empty()) {
spawn_pool = valid_team_spawns;
}
else {
spawn_pool.resize(respawn_points.size());
std::iota(spawn_pool.begin(), spawn_pool.end(),
0);
}

for (std::size_t i : spawn_pool) {
const auto& spawn_point = respawn_points[i];
rf::Vector3 spawn_position = spawn_point->position;

float min_distance_to_enemy = std::numeric_limits<float>::max();

for (auto& player : player_list) {
if ((is_team_game && player.team != team) || (!is_team_game && &player != pp)) {
if (!rf::player_is_dead(&player)) {
rf::Entity* entity = rf::entity_from_handle(player.entity_handle);
if (entity) {
float distance = (spawn_position - entity->pos).len();
min_distance_to_enemy = std::min(min_distance_to_enemy, distance);
}
}
}
}

if (min_distance_to_enemy > max_distance) {
if (avoid_last && i == last_index) {
second_max_distance = max_distance;
second_furthest_spawn_index = furthest_spawn_index;
continue;
}

second_max_distance = max_distance;
second_furthest_spawn_index = furthest_spawn_index;

max_distance = min_distance_to_enemy;
furthest_spawn_index = i;
}
}

if (furthest_spawn_index) {
if (avoid_last && *furthest_spawn_index == last_index && second_furthest_spawn_index) {
pdata.last_spawn_point_index = *second_furthest_spawn_index;
return respawn_points[*second_furthest_spawn_index];
}

pdata.last_spawn_point_index = *furthest_spawn_index;
return respawn_points[*furthest_spawn_index];
}
}
}

// use RNG and avoid last
if (!valid_team_spawns.empty()) {
if (avoid_last && valid_team_spawns.size() > 1) {
valid_team_spawns.erase(std::remove(valid_team_spawns.begin(), valid_team_spawns.end(), last_index),
valid_team_spawns.end());
xlog::debug("Excluding last spawn point index: {} from selection.", last_index);
}

std::uniform_int_distribution<std::size_t> team_range(0, valid_team_spawns.size() - 1);
std::size_t random_team_index = team_range(g_rng);

pdata.last_spawn_point_index = valid_team_spawns[random_team_index];
return {respawn_points[valid_team_spawns[random_team_index]]};
}

// RNG fallback logic if DM or team game with no valid team spawns
std::uniform_int_distribution<std::size_t> random_range(0, respawn_points.size() - 1);
std::size_t random_index = random_range(g_rng);

if (avoid_last && respawn_points.size() > 1 && random_index == last_index) {
random_index = (random_index + 1) % respawn_points.size();
}

pdata.last_spawn_point_index = random_index;
return {respawn_points[random_index]};
}

CallHook<rf::Entity*(rf::Player*, int, rf::Vector3*, rf::Matrix3*, int)> player_create_entity_hook{
{
0x0048087D, // in spawn_player_server_side, used by servers
0x0045C807 // in load_level, used by single player and listen servers (initial spawn)
},
[](rf::Player* pp, int entity_type, rf::Vector3* pos, rf::Matrix3* orient, int mp_character) -> rf::Entity* {
xlog::debug("Processing player_create_entity request. Player: {}, Team: {}, Entity Type: {}, MP Character: {}",
pp->name, pp->team, entity_type, mp_character);

// in single player don't touch anything
if (mp_character == -1) {
return player_create_entity_hook.call_target(pp, entity_type, pos, orient, mp_character);
}

// assume no valid spawn until proven otherwise
bool valid_respawn_found = false;

// do we have any spawn points?
if (rf::multi_respawn_num_points > 0) {
if (g_additional_server_config.random_spawns.enabled) {
// new spawn logic is enabled, try to find a valid respawn point
if (auto random_respawn = select_respawn_point_new(pp)) {
xlog::debug("Spawning {} at ({}, {}, {})",
pp->name, random_respawn->position.x, random_respawn->position.y, random_respawn->position.z);
*pos = random_respawn->position;
*orient = random_respawn->orientation;
valid_respawn_found = true; // valid respawn was found using the new logic
}
}
else {
// old logic has a spawn point for us
valid_respawn_found = true;
}
}

// use player start position if no spawn points were found
if (!valid_respawn_found) {
*pos = rf::level.player_start_pos;
*orient = rf::level.player_start_orient;
xlog::warn("No spawn point found. Spawning {} at the Player Start.", pp->name);
}

return player_create_entity_hook.call_target(pp, entity_type, pos, orient, mp_character);
}
};

FunHook<void(rf::Player*)> multi_spawn_player_server_side_hook{
0x00480820,
[](rf::Player* player) {
Expand Down Expand Up @@ -588,11 +845,6 @@ static float get_weapon_shot_stats_delta(rf::Weapon* wp)
return 1.0f / num_projectiles;
}

static bool multi_is_team_game_type()
{
return rf::multi_get_game_type() != rf::NG_TYPE_DM;
}

static void maybe_increment_weapon_hits_stat(int hit_obj_handle, rf::Weapon *wp)
{
rf::Entity* attacker_ep = rf::entity_from_handle(wp->parent_handle);
Expand Down Expand Up @@ -656,6 +908,11 @@ CodeInjection multi_limbo_init_injection{
xlog::trace("Wait between levels shortened because server is empty");
addr_as_ref<int>(regs.esp) = 100;
}

// clear list of respawn points on map end
if (g_additional_server_config.random_spawns.enabled) {
respawn_points.clear();
}
},
};

Expand Down Expand Up @@ -710,6 +967,10 @@ void server_init()
multi_on_new_player_injection.install();
AsmWriter(0x0047B061, 0x0047B064).add(asm_regs::esp, 0x14);

// Support new spawn logic
multi_respawn_create_point_hook.install();
player_create_entity_hook.install();

// Support forcing player character
multi_spawn_player_server_side_hook.install();

Expand Down
Loading