Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion common/include/common/rfproto.h
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ enum RF_GameType
RF_GT_BM = 0x08,
RF_GT_TBM = 0x09,
RF_GT_LMS = 0x0A,
// Sentinel: must always be the last entry.
RF_GT_WO = 0x0B,
// Sentinel: must always be the last entry. Keep in sync with rf::NetGameType.
RF_GT_UNK
};

Expand Down
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Version 1.4.0 (Lupin): Not yet released
- Bagman (`BAG`)
- Team Bagman (`TBAG`)
- Last Miner Standing (`LMS`)
- Wipeout (`WO`)
- Add sprays with bindable `Spray` control
- `cl_sprays` console command to toggle local display and dedicated server `[sprays]` config section
- `spray` console command to select spray, and in-game spray picker in advanced options
Expand Down
2 changes: 2 additions & 0 deletions game_patch/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ set(SRCS
multi/rounds.cpp
multi/lms.h
multi/lms.cpp
multi/wipeout.h
multi/wipeout.cpp
multi/sprays.h
multi/sprays.cpp
multi/kill.cpp
Expand Down
23 changes: 22 additions & 1 deletion game_patch/hud/multi_hud.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "../multi/multi.h"
#include "../multi/gametype.h"
#include "../multi/bagman.h"
#include "../multi/wipeout.h"
#include "../input/input.h"
#include "../rf/input.h"
#include "../rf/hud.h"
Expand Down Expand Up @@ -932,6 +933,21 @@ void multi_hud_render_team_scores()
}
}

// Wipeout: next to each team's flag icon, show how many of that team are
// currently alive vs. waiting (dead/awaiting-respawn or a late joiner). The
// round-win score itself is drawn on the right by the standard path below.
if (game_type == rf::NG_TYPE_WO) {
rf::gr::set_color(53, 207, 22, 255);
std::string red_label = std::format("({} Alive, {} Waiting)",
wipeout_count_team_alive(rf::TEAM_RED), wipeout_count_team_waiting(rf::TEAM_RED));
std::string blue_label = std::format("({} Alive, {} Waiting)",
wipeout_count_team_alive(rf::TEAM_BLUE), wipeout_count_team_waiting(rf::TEAM_BLUE));
std::string red_fit = hud_fit_string(red_label, max_miniflag_label_w, nullptr, font_id);
std::string blue_fit = hud_fit_string(blue_label, max_miniflag_label_w, nullptr, font_id);
rf::gr::string(miniflag_label_x, red_miniflag_label_y, red_fit.c_str(), font_id);
rf::gr::string(miniflag_label_x, blue_miniflag_label_y, blue_fit.c_str(), font_id);
}

if (multi_is_team_game_type() && !is_hill_score && !is_run) {
float miniflag_scale = g_big_team_scores_hud ? 1.5f : 1.0f;
rf::gr::set_color(255, 255, 255, 255);
Expand Down Expand Up @@ -974,6 +990,11 @@ void multi_hud_render_team_scores()
red_score = bagman_get_red_team_score();
blue_score = bagman_get_blue_team_score();
}
else if (game_type == rf::NG_TYPE_WO) {
rf::gr::set_color(53, 207, 22, 255);
red_score = wipeout_get_red_team_score();
blue_score = wipeout_get_blue_team_score();
}
}

auto red_score_str = std::to_string(red_score);
Expand Down Expand Up @@ -1068,7 +1089,7 @@ CodeInjection multi_hud_render_team_scores_new_gamemodes_patch {
const bool is_ffa_with_list = game_type == rf::NG_TYPE_BAG
|| game_type == rf::NG_TYPE_LMS
|| (game_type == rf::NG_TYPE_DM && g_alpine_game_config.show_mini_scoreboard_dm);
if (gt_is_koth() || gt_is_dc() || gt_is_rev() || gt_is_run() || gt_is_esc() || gt_is_tbag() || is_ffa_with_list) {
if (gt_is_koth() || gt_is_dc() || gt_is_rev() || gt_is_run() || gt_is_esc() || gt_is_tbag() || gt_is_wipeout() || is_ffa_with_list) {
regs.eip = 0x00476E06; // multi_hud_render_team_scores
}
}
Expand Down
11 changes: 11 additions & 0 deletions game_patch/hud/multi_scoreboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "../multi/multi.h"
#include "../multi/gametype.h"
#include "../multi/bagman.h"
#include "../multi/wipeout.h"
#include "../misc/alpine_options.h"
#include "../misc/alpine_settings.h"
#include "../rf/player/control_config.h"
Expand Down Expand Up @@ -230,6 +231,16 @@ int draw_scoreboard_header(int x, int y, int w, rf::NetGameType game_type, bool
red_score = bagman_get_red_team_score();
blue_score = bagman_get_blue_team_score();
}
else if (game_type == rf::NG_TYPE_WO) {
static int hud_flag_red_bm = rf::bm::load("hud_flag_red.tga", -1, true);
static int hud_flag_blue_bm = rf::bm::load("hud_flag_blue.tga", -1, true);
int flag_bm_w, flag_bm_h;
rf::bm::get_dimensions(hud_flag_red_bm, &flag_bm_w, &flag_bm_h);
rf::gr::bitmap(hud_flag_red_bm, x + w * 2 / 6 - flag_bm_w / 2, cur_y);
rf::gr::bitmap(hud_flag_blue_bm, x + w * 4 / 6 - flag_bm_w / 2, cur_y);
red_score = wipeout_get_red_team_score();
blue_score = wipeout_get_blue_team_score();
}
else if (game_type == rf::NG_TYPE_REV || game_type == rf::NG_TYPE_ESC) {
static int hud_flag_red_bm = rf::bm::load("hud_flag_red.tga", -1, true);
static int hud_flag_blue_bm = rf::bm::load("hud_flag_blue.tga", -1, true);
Expand Down
23 changes: 23 additions & 0 deletions game_patch/misc/player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "../rf/gr/gr_light.h"
#include "../rf/os/os.h"
#include "../rf/os/frametime.h"
#include "../rf/gameseq.h"
#include "../os/console.h"
#include "../main/main.h"
#include "../misc/alpine_options.h"
Expand Down Expand Up @@ -523,10 +524,32 @@ CallHook player_is_dying_red_bars_hook{0x00432A5F, player_is_dying_and_not_spect
CallHook player_is_dying_scoreboard_hook{0x00437C01, player_is_dying_and_not_spectating};
CallHook player_is_dying_scoreboard2_hook{0x00437C36, player_is_dying_and_not_spectating};

// Report our locally-selected mp character to the server whenever it changes,
// so server-driven spawns (round modes) and the first post-join spawn use it
// instead of the default.
void multi_character_sync_client_do_frame() {
static int last_reported = -1;

if (rf::is_server || !rf::is_multi || !rf::local_player) {
last_reported = -1;
return;
}
if (rf::gameseq_get_state() != rf::GameState::GS_GAMEPLAY) {
return;
}
Comment thread
GooberRF marked this conversation as resolved.

const int cur = rf::local_player->settings.multi_character;
if (cur != last_reported) {
af_send_character_request(cur);
last_reported = cur;
}
}

FunHook<void()> players_do_frame_hook{
0x004A26D0,
[]() {
players_do_frame_hook.call_target();
multi_character_sync_client_do_frame();
if (multi_spectate_is_spectating()) {
rf::Player* target = multi_spectate_get_target_player();
rf::hud_do_frame(target);
Expand Down
47 changes: 45 additions & 2 deletions game_patch/multi/alpine_packets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,12 @@ void serialize_payload(const SprayReqPayload& payload, std::byte* buf, size_t& o
offset += sizeof(payload.normal);
}

// af_req_character
void serialize_payload(const CharacterPayload& payload, std::byte* buf, size_t& offset)
{
buf[offset++] = static_cast<std::byte>(payload.character_index);
}

// af_req_server_cfg
void serialize_payload(const std::monostate& payload, const std::byte* const buf, const size_t& offset)
{
Expand Down Expand Up @@ -669,7 +675,7 @@ void af_send_spray_request(uint16_t texture_id, const rf::Vector3& pos, const rf
}

// send client request packet
void af_send_client_req_packet(const af_client_req_packet& packet)
void af_send_client_req_packet(const af_client_req_packet& packet, bool is_reliable)
{
// Send: client -> server
if (!rf::is_multi || rf::is_server) {
Expand All @@ -690,7 +696,26 @@ void af_send_client_req_packet(const af_client_req_packet& packet)
std::visit([&](const auto& payload) { serialize_payload(payload, buf, offset); }, packet.payload);

int total_len = static_cast<int>(offset);
af_send_packet(rf::local_player, buf, total_len, false);
af_send_packet(rf::local_player, buf, total_len, is_reliable);
}

// Reliably report the locally-selected mp character to the server.
void af_send_character_request(int character_index)
{
if (!rf::is_multi || rf::is_server) {
return;
}
if (character_index < 0 || character_index >= rf::num_multi_characters) {
return; // never report a selection outside the valid range
}

af_client_req_packet packet{};
packet.header.type = static_cast<uint8_t>(af_packet_type::af_client_req);
packet.header.size = sizeof(uint8_t) + sizeof(uint8_t); // req_type + character index
packet.req_type = af_client_req_type::af_req_character;
packet.payload = CharacterPayload{static_cast<uint8_t>(character_index)};

af_send_client_req_packet(packet, true); // reliable
}

// process client request packet
Expand Down Expand Up @@ -761,6 +786,24 @@ static void af_process_client_req_packet(const void* data, size_t len, const rf:
}
break;
}
case af_client_req_type::af_req_character: {
if (remaining < sizeof(uint8_t)) {
xlog::warn("af_process_client_req_packet: Character payload too short");
return;
}
const uint8_t idx = bytes[offset];
if (idx < rf::num_multi_characters) {
// Store the client's real selection separately from
// settings.multi_character, which force_character overwrites at
// spawn time. The spawn hook picks force vs. this reported value.
player->reported_multi_character = static_cast<int>(idx);
}
else {
xlog::warn("af_process_client_req_packet: character index {} out of range (max {})",
idx, rf::num_multi_characters - 1);
}
break;
}
case af_client_req_type::af_req_spray: {
constexpr size_t expected = sizeof(uint16_t) + sizeof(RF_Vector) + sizeof(RF_Vector);
if (remaining < expected) {
Expand Down
11 changes: 9 additions & 2 deletions game_patch/multi/alpine_packets.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ enum class af_client_req_type : uint8_t
af_req_handicap = 0x0,
af_req_server_cfg = 0x1,
af_req_spray = 0x2,
af_req_character = 0x3,
};

struct HandicapPayload
Expand All @@ -96,7 +97,12 @@ struct SprayReqPayload
};
static_assert(sizeof(SprayReqPayload) == 26);

using af_client_payload = std::variant<HandicapPayload, SprayReqPayload, std::monostate>;
struct CharacterPayload
{
uint8_t character_index = 0;
};

using af_client_payload = std::variant<HandicapPayload, SprayReqPayload, CharacterPayload, std::monostate>;

struct af_client_req_packet
{
Expand Down Expand Up @@ -451,8 +457,9 @@ void af_send_damage_notify_packet(uint8_t player_id, float damage, bool died, rf
static void af_process_damage_notify_packet(const void* data, size_t len, const rf::NetAddr& addr);
void af_send_obj_update_packet(rf::Player* player);
static void af_process_obj_update_packet(const void* data, size_t len, const rf::NetAddr& addr);
void af_send_client_req_packet(const af_client_req_packet& packet);
void af_send_client_req_packet(const af_client_req_packet& packet, bool is_reliable = false);
static void af_process_client_req_packet(const void* data, size_t len, const rf::NetAddr& addr);
void af_send_character_request(int character_index);
void af_send_server_req_packet(const af_server_req_packet& packet, rf::Player* player);
void af_send_should_gib_req(uint32_t obj_handle);
void af_send_teleport_entity_req(uint32_t obj_handle, const rf::Vector3& pos, const rf::Matrix3& orient, const rf::Vector3& vel);
Expand Down
41 changes: 41 additions & 0 deletions game_patch/multi/dedi_cfg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,47 @@ void apply_defaults_for_game_type(rf::NetGameType game_type, AlpineServerConfigR
break;
}

case rf::NetGameType::NG_TYPE_WO: {
// Round-based team wipe mode.
rules.location_pinging = true;

// 200 / 200 spawn health and armor.
rules.spawn_life.enabled = true;
rules.spawn_life.set_value(200.0f);
rules.spawn_armour.enabled = true;
rules.spawn_armour.set_value(200.0f);

// Base respawn delay of 5s; the death hook multiplies it by the
// player's death count this round (escalating 5s/10s/15s...).
rules.spawn_delay.enabled = true;
rules.spawn_delay.set_base_value(5.0f);
rules.force_respawn = false; // wipeout_do_frame auto-respawns once the delay elapses

// Fixed loadout: Pistol / AR / SR / RL / SG, with infinite reloads
// and large reserves (effectively infinite ammo).
constexpr int wo_reserve = 999;
rules.spawn_loadout.loadouts_active = true;
rules.spawn_loadout.add("12mm handgun", wo_reserve, false, true);
rules.spawn_loadout.add("Assault Rifle", wo_reserve, false, true);
rules.spawn_loadout.add("Sniper Rifle", wo_reserve, false, true);
rules.spawn_loadout.add("Rocket Launcher", wo_reserve, false, true);
rules.spawn_loadout.add("Shotgun", wo_reserve, false, true);
rules.weapon_infinite_magazines = true;
rules.default_player_weapon.set_weapon("Assault Rifle");

// No item spawns, no weapon/amp drops (wipeout also hides all level
// items each round; see wipeout.cpp).
rules.drop_weapons = false;
rules.drop_amps = false;

// Best-of-7, untimed rounds (a round ends only on a team wipe).
rules.rounds.set_max_rounds(7);
rules.rounds.round_time = 0; // unlimited
rules.rounds.set_post_round_time(3);
rules.rounds.set_intermission_time(5);
break;
}

default: {
rules.spawn_delay.enabled = false;

Expand Down
Loading
Loading