Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Version 1.3.0 (Bakeapple): Not yet released
[@nickalreadyinuse](https://github.com/nickalreadyinuse)
- Optimize network performance for `af_obj_update` packets and bot decommission logic
- Sync animation state for crouched players in first person spectate view
- Add freelook/first person toggle bind in spectator mode

Version 1.2.2 (Willow): Released Jan-04-2026
--------------------------------
Expand Down
65 changes: 64 additions & 1 deletion game_patch/hud/multi_spectate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ static rf::Player* g_spectate_mode_target;
static rf::Camera* g_old_target_camera = nullptr;
static bool g_spectate_mode_enabled = false;
static bool g_spectate_mode_follow_killer = false;
static rf::Player* g_spectate_freelook_saved_target = nullptr;

void player_fpgun_set_player(rf::Player* pp);

Expand Down Expand Up @@ -232,6 +233,61 @@ bool multi_spectate_is_spectating()
return g_spectate_mode_enabled || multi_spectate_is_freelook();
}

bool multi_spectate_is_first_person()
{
return g_spectate_mode_enabled;
}

void multi_spectate_toggle_freelook()
{
if (!multi_spectate_is_spectating())
return;

if (g_spectate_mode_enabled) {
// Currently in first-person spectate, switch to freelook
// Save the current target so we can resume on the same player when toggling back
g_spectate_freelook_saved_target = g_spectate_mode_target;

// Clean up the first-person spectate state (restore old target's camera, weapon state)
if (g_spectate_mode_target && g_spectate_mode_target != rf::local_player) {
g_spectate_mode_target->cam = g_old_target_camera;
g_old_target_camera = nullptr;

#if SPECTATE_MODE_SHOW_WEAPON
g_spectate_mode_target->flags &= ~(1u << 4);
rf::Entity* entity = rf::entity_from_handle(g_spectate_mode_target->entity_handle);
if (entity)
entity->local_player = nullptr;
#endif
}

g_spectate_mode_enabled = false;
g_spectate_mode_target = rf::local_player;

#if SPECTATE_MODE_SHOW_WEAPON
player_fpgun_set_player(rf::local_player);
#endif

// Now enter freelook cleanly
multi_spectate_enter_freelook();
}
else {
// Currently in freelook, switch to first-person spectate
// Try to resume on the saved target if they're still valid
if (g_spectate_freelook_saved_target
&& g_spectate_freelook_saved_target != rf::local_player
&& !g_spectate_freelook_saved_target->is_browser) {
multi_spectate_set_target_player(g_spectate_freelook_saved_target);
}
else {
// Saved target is gone, find any player
g_spectate_mode_target = rf::local_player;
spectate_next_player(true, true);
}
g_spectate_freelook_saved_target = nullptr;
}
}

rf::Player* multi_spectate_get_target_player()
{
return g_spectate_mode_target;
Expand Down Expand Up @@ -273,7 +329,8 @@ bool multi_spectate_execute_action(rf::ControlConfigAction action, bool was_pres
else if (multi_spectate_is_freelook()) {
// don't allow respawn in freelook spectate
if (action == rf::CC_ACTION_PRIMARY_ATTACK || action == rf::CC_ACTION_SECONDARY_ATTACK) {
if (was_pressed)
// If we entered freelook via toggle, just consume the click without leaving
if (!g_spectate_freelook_saved_target && was_pressed)
multi_spectate_leave();
return true;
}
Expand Down Expand Up @@ -304,6 +361,8 @@ void multi_spectate_on_player_kill(rf::Player* victim, rf::Player* killer)
void multi_spectate_on_destroy_player(rf::Player* player)
{
if (player != rf::local_player) {
if (g_spectate_freelook_saved_target == player)
g_spectate_freelook_saved_target = nullptr;
if (g_spectate_mode_target == player)
spectate_next_player(true);
if (g_spectate_mode_target == player)
Expand Down Expand Up @@ -588,9 +647,13 @@ void multi_spectate_render() {
std::string spec_menu_text = get_action_bind_name(
get_af_control(rf::AlpineControlConfigAction::AF_ACTION_SPECTATE_MENU)
);
std::string toggle_freelook_text = get_action_bind_name(
get_af_control(rf::AlpineControlConfigAction::AF_ACTION_SPECTATE_TOGGLE_FREELOOK)
);
const char* hints[][3] = {
{next_player_text.c_str(), "Next Player"},
{prev_player_text.c_str(), "Previous Player"},
{toggle_freelook_text.c_str(), "Toggle Freelook"},
{spec_menu_text.c_str(), "Open Spectate Options Menu"},
{exit_spec_text.c_str(), "Exit Spectate Mode"},
};
Expand Down
2 changes: 2 additions & 0 deletions game_patch/hud/multi_spectate.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ void multi_spectate_on_player_kill(rf::Player* player, rf::Player* killer);
void multi_spectate_on_destroy_player(rf::Player* player);
void multi_spectate_player_create_entity_post(rf::Player* player, rf::Entity* entity);
bool multi_spectate_is_spectating();
bool multi_spectate_is_first_person();
void multi_spectate_toggle_freelook();
bool multi_spectate_execute_action(rf::ControlConfigAction action, bool was_pressed);
void multi_spectate_sync_crouch_anim();
7 changes: 7 additions & 0 deletions game_patch/input/key.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ CodeInjection control_config_init_patch{
rf::AlpineControlConfigAction::AF_ACTION_REMOTE_SERVER_CFG);
alpine_control_config_add_item(ccp, "Inspect Weapon", false, rf::KEY_I, -1, -1,
rf::AlpineControlConfigAction::AF_ACTION_INSPECT_WEAPON);
alpine_control_config_add_item(ccp, "Toggle Freelook Spectate", false, rf::KEY_PERIOD, -1, -1,
rf::AlpineControlConfigAction::AF_ACTION_SPECTATE_TOGGLE_FREELOOK);
},
};

Expand Down Expand Up @@ -393,6 +395,11 @@ CodeInjection player_execute_action_patch3{
== static_cast<int>(rf::AlpineControlConfigAction::AF_ACTION_REMOTE_SERVER_CFG)
&& is_server_minimum_af_version(1, 2)) {
g_remote_server_cfg_popup.toggle();
} else if (alpine_action_index
== static_cast<int>(rf::AlpineControlConfigAction::AF_ACTION_SPECTATE_TOGGLE_FREELOOK)
&& !rf::is_dedicated_server
&& multi_spectate_is_spectating()) {
multi_spectate_toggle_freelook();
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion game_patch/rf/player/control_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ namespace rf
AF_ACTION_NO_AUTOSWITCH = 0xC,
AF_ACTION_REMOTE_SERVER_CFG = 0xD,
AF_ACTION_INSPECT_WEAPON = 0xE,
_AF_ACTION_LAST_VARIANT = AF_ACTION_INSPECT_WEAPON
AF_ACTION_SPECTATE_TOGGLE_FREELOOK = 0xF,
_AF_ACTION_LAST_VARIANT = AF_ACTION_SPECTATE_TOGGLE_FREELOOK
};

struct ControlConfigItem
Expand Down
Loading