From 95ee6d7f6eb91c1a628952f9b21f45b8eeb78d71 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sun, 8 Mar 2026 13:46:15 -0500 Subject: [PATCH 1/3] Implement Photo Mode --- code/camera/photomode.cpp | 514 ++++++++++++++++++++ code/camera/photomode.h | 17 + code/controlconfig/controlsconfig.h | 8 + code/controlconfig/controlsconfigcommon.cpp | 21 + code/graphics/2d.cpp | 5 + code/graphics/2d.h | 1 + code/io/keycontrol.cpp | 59 ++- code/localization/localize.cpp | 2 +- code/missionui/missionpause.cpp | 13 +- code/parse/sexp.cpp | 33 +- code/parse/sexp.h | 1 + code/scripting/global_hooks.cpp | 8 + code/scripting/global_hooks.h | 2 + code/source_groups.cmake | 2 + freespace2/freespace.cpp | 13 + 15 files changed, 687 insertions(+), 12 deletions(-) create mode 100644 code/camera/photomode.cpp create mode 100644 code/camera/photomode.h diff --git a/code/camera/photomode.cpp b/code/camera/photomode.cpp new file mode 100644 index 00000000000..5ee18ca1657 --- /dev/null +++ b/code/camera/photomode.cpp @@ -0,0 +1,514 @@ +#include "freespace.h" + +#include "actions/Action.h" +#include "camera/camera.h" +#include "camera/photomode.h" +#include "controlconfig/controlsconfig.h" +#include "debugconsole/console.h" +#include "globalincs/alphacolors.h" +#include "graphics/2d.h" +#include "graphics/font.h" +#include "hud/hud.h" +#include "io/keycontrol.h" +#include "localization/localize.h" +#include "mission/missionmessage.h" +#include "object/object.h" +#include "parse/parselo.h" +#include "playerman/player.h" +#include "render/3d.h" +#include "scripting/global_hooks.h" +#include "sound/audiostr.h" + +bool Photo_mode_active = false; +camid Photo_mode_id; +fix Photo_mode_saved_time_compression = F1_0; +bool Photo_mode_saved_lock_state = false; +bool Photo_mode_screenshot_queued_this_frame = false; +bool Photo_mode_allowed = true; +bool Photo_mode_audio_paused = false; + +const float Photo_mode_turn_rate = PI_2; // radians/sec +constexpr float Photo_mode_move_speed = 90.0f; +constexpr float Photo_mode_boost_multiplier = 6.0f; +constexpr float Photo_mode_time_compression = 0.01f; + +struct photo_mode_post_effect_state { + SCP_string name; + float intensity = 0.0f; + vec3d rgb = vmd_zero_vector; +}; + +enum photo_mode_parameter { + PHOTO_MODE_PARAM_SATURATION = 0, + PHOTO_MODE_PARAM_BRIGHTNESS, + PHOTO_MODE_PARAM_CONTRAST, + PHOTO_MODE_PARAM_COUNT +}; + +SCP_vector Photo_mode_saved_post_effects; +std::array Photo_mode_saved_parameter_values = {100, 100, 100}; +std::array Photo_mode_parameter_values = {100, 100, 100}; +int Photo_mode_selected_parameter = PHOTO_MODE_PARAM_SATURATION; +bool Photo_mode_grid_enabled = false; + +const char* photo_mode_get_parameter_effect_name(int index) +{ + switch (index) { + case PHOTO_MODE_PARAM_SATURATION: + return "saturation"; + case PHOTO_MODE_PARAM_BRIGHTNESS: + return "brightness"; + case PHOTO_MODE_PARAM_CONTRAST: + return "contrast"; + default: + return nullptr; + } +} + +void photo_mode_capture_post_effect_state() +{ + Photo_mode_saved_post_effects.clear(); + + if (graphics::Post_processing_manager == nullptr) { + return; + } + + const auto& post_effects = graphics::Post_processing_manager->getPostEffects(); + Photo_mode_saved_post_effects.reserve(post_effects.size()); + + for (const auto& effect : post_effects) { + photo_mode_post_effect_state state; + state.name = effect.name; + state.intensity = effect.intensity; + state.rgb = effect.rgb; + Photo_mode_saved_post_effects.push_back(state); + } +} + +void photo_mode_apply_saved_post_effect_state(bool clear_saved_state) +{ + if (graphics::Post_processing_manager == nullptr) { + if (clear_saved_state) { + Photo_mode_saved_post_effects.clear(); + } + return; + } + + const auto& post_effects = graphics::Post_processing_manager->getPostEffects(); + for (const auto& saved_state : Photo_mode_saved_post_effects) { + for (const auto& effect : post_effects) { + if (!stricmp(effect.name.c_str(), saved_state.name.c_str())) { + const int value = static_cast(std::lround((saved_state.intensity - effect.add) * effect.div)); + gr_post_process_set_effect(saved_state.name.c_str(), value, &saved_state.rgb); + break; + } + } + } + + if (clear_saved_state) { + Photo_mode_saved_post_effects.clear(); + } +} + +int photo_mode_get_saved_post_effect_value(const char* effect_name) +{ + int value = 100; + + if (effect_name == nullptr || graphics::Post_processing_manager == nullptr) { + return value; + } + + for (const auto& saved_state : Photo_mode_saved_post_effects) { + if (stricmp(saved_state.name.c_str(), effect_name) != 0) { + continue; + } + + for (const auto& effect : graphics::Post_processing_manager->getPostEffects()) { + if (stricmp(effect.name.c_str(), effect_name) == 0) { + value = static_cast(std::lround((saved_state.intensity - effect.add) * effect.div)); + break; + } + } + + break; + } + + return value; +} + +void photo_mode_sync_parameter_values_from_saved_state() +{ + for (int i = 0; i < PHOTO_MODE_PARAM_COUNT; ++i) { + const auto effect_name = photo_mode_get_parameter_effect_name(i); + Photo_mode_saved_parameter_values[i] = photo_mode_get_saved_post_effect_value(effect_name); + Photo_mode_parameter_values[i] = Photo_mode_saved_parameter_values[i]; + } + + Photo_mode_selected_parameter = PHOTO_MODE_PARAM_SATURATION; +} + +void photo_mode_apply_parameter_values() +{ + for (int i = 0; i < PHOTO_MODE_PARAM_COUNT; ++i) { + const auto effect_name = photo_mode_get_parameter_effect_name(i); + if (effect_name == nullptr) { + continue; + } + + gr_post_process_set_effect(effect_name, Photo_mode_parameter_values[i], nullptr); + } +} + +SCP_string format_photo_mode_keybind(int action) +{ + auto primary = Control_config[action].first.textify(); + auto secondary = Control_config[action].second.textify(); + + if (primary.empty() && secondary.empty()) { + return SCP_string(XSTR("Unbound", 1909)); + } + if (primary.empty()) { + return secondary; + } + if (secondary.empty()) { + return primary; + } + + return primary + " / " + secondary; +} + +void photo_mode_set_active(bool active) +{ + if (active == Photo_mode_active) { + return; + } + + if (active) { + if (!Photo_mode_allowed) { + return; + } + + if ((Game_mode & GM_MULTIPLAYER) != 0 || Player_obj == nullptr || !(Game_mode & GM_IN_MISSION)) { + return; + } + + if (Time_compression_locked) { + return; + } + + if (Player_obj->type != OBJ_SHIP) { + return; + } + + Photo_mode_saved_time_compression = Game_time_compression; + Photo_mode_saved_lock_state = Time_compression_locked; + + Photo_mode_id = cam_create("Photo Mode", &Eye_position, &Eye_matrix); + if (!Photo_mode_id.isValid() || !cam_set_camera(Photo_mode_id)) { + cam_delete(Photo_mode_id); + Photo_mode_id = camid(); + return; + } + + set_time_compression(Photo_mode_time_compression, 0.0f); + lock_time_compression(true); + + photo_mode_capture_post_effect_state(); + photo_mode_sync_parameter_values_from_saved_state(); + photo_mode_apply_parameter_values(); + + audiostream_pause_all(); + message_pause_all(); + Photo_mode_audio_paused = true; + + Photo_mode_active = true; + mprintf(("Photo Mode enabled.\n")); + + if (scripting::hooks::OnPhotoModeStarted->isActive()) { + scripting::hooks::OnPhotoModeStarted->run(); + } + return; + } + + cam_reset_camera(); + cam_delete(Photo_mode_id); + Photo_mode_id = camid(); + + set_time_compression(f2fl(Photo_mode_saved_time_compression), 0.0f); + lock_time_compression(Photo_mode_saved_lock_state); + + if (Photo_mode_audio_paused) { + audiostream_unpause_all(); + message_resume_all(); + Photo_mode_audio_paused = false; + } + + photo_mode_apply_saved_post_effect_state(true); + Photo_mode_saved_parameter_values = {100, 100, 100}; + Photo_mode_parameter_values = {100, 100, 100}; + Photo_mode_selected_parameter = PHOTO_MODE_PARAM_SATURATION; + Photo_mode_grid_enabled = false; + + Photo_mode_active = false; + mprintf(("Photo Mode disabled.\n")); + + if (scripting::hooks::OnPhotoModeEnded->isActive()) { + scripting::hooks::OnPhotoModeEnded->run(); + } +} + +void photo_mode_do_frame(float frame_time) +{ + if (!Photo_mode_active || !Photo_mode_id.isValid()) { + return; + } + + if (!Photo_mode_allowed) { + photo_mode_set_active(false); + return; + } + + auto photo_mode = Photo_mode_id.getCamera(); + if (photo_mode == nullptr) { + photo_mode_set_active(false); + return; + } + + vec3d cam_pos = vmd_zero_vector; + matrix cam_orient = vmd_identity_matrix; + photo_mode->get_info(&cam_pos, &cam_orient); + + angles delta_angles{}; + float pitch = check_control_timef(PITCH_FORWARD) - check_control_timef(PITCH_BACK); + float heading = check_control_timef(YAW_RIGHT) - check_control_timef(YAW_LEFT); + float bank = check_control_timef(BANK_LEFT) - check_control_timef(BANK_RIGHT); + + int axis[Action::NUM_VALUES] = {0}; + control_get_axes_readings(axis, flRealframetime); + pitch += -f2fl(axis[Action::PITCH]); + heading += f2fl(axis[Action::HEADING]); + bank -= f2fl(axis[Action::BANK]); + + CLAMP(pitch, -1.0f, 1.0f); + CLAMP(heading, -1.0f, 1.0f); + CLAMP(bank, -1.0f, 1.0f); + + delta_angles.p = pitch * Photo_mode_turn_rate * frame_time; + delta_angles.h = heading * Photo_mode_turn_rate * frame_time; + delta_angles.b = bank * Photo_mode_turn_rate * frame_time; + + matrix delta_orient = vmd_identity_matrix; + vm_angles_2_matrix(&delta_orient, &delta_angles); + vm_matrix_x_matrix(&cam_orient, &cam_orient, &delta_orient); + vm_fix_matrix(&cam_orient); + + float speed = Photo_mode_move_speed; + if (check_control(AFTERBURNER)) { + speed *= Photo_mode_boost_multiplier; + } + + const float forward = check_control_timef(FORWARD_THRUST) - check_control_timef(REVERSE_THRUST); + const float right = check_control_timef(RIGHT_SLIDE_THRUST) - check_control_timef(LEFT_SLIDE_THRUST); + const float up = check_control_timef(UP_SLIDE_THRUST) - check_control_timef(DOWN_SLIDE_THRUST); + + vm_vec_scale_add2(&cam_pos, &cam_orient.vec.fvec, forward * speed * frame_time); + vm_vec_scale_add2(&cam_pos, &cam_orient.vec.rvec, right * speed * frame_time); + vm_vec_scale_add2(&cam_pos, &cam_orient.vec.uvec, up * speed * frame_time); + + photo_mode->set_rotation(&cam_orient); + photo_mode->set_position(&cam_pos); +} + +void photo_mode_maybe_render_hud() +{ + if (!Photo_mode_active || Photo_mode_screenshot_queued_this_frame || gr_is_screenshot_requested()) { + return; + } + + if (!Photo_mode_allowed) { + photo_mode_set_active(false); + return; + } + + auto photo_mode = Photo_mode_id.getCamera(); + if (photo_mode == nullptr) { + return; + } + + vec3d cam_pos = vmd_zero_vector; + matrix cam_orient = vmd_identity_matrix; + photo_mode->get_info(&cam_pos, &cam_orient); + + const auto toggle_keybind = format_photo_mode_keybind(TOGGLE_PHOTO_MODE); + const auto prev_filter_keybind = format_photo_mode_keybind(PHOTO_MODE_FILTER_PREV); + const auto next_filter_keybind = format_photo_mode_keybind(PHOTO_MODE_FILTER_NEXT); + const auto reset_filter_keybind = format_photo_mode_keybind(PHOTO_MODE_FILTER_RESET); + const auto decrease_param_keybind = format_photo_mode_keybind(PHOTO_MODE_PARAM_DECREASE); + const auto increase_param_keybind = format_photo_mode_keybind(PHOTO_MODE_PARAM_INCREASE); + const auto grid_keybind = format_photo_mode_keybind(PHOTO_MODE_TOGGLE_GRID); + + gr_set_color_fast(&Color_silver); + const auto old_font = font::get_current_fontnum(); + font::set_font(font::FONT1); + const int line_height = gr_get_font_height(); + const int panel_padding = 8; + const int panel_width = 530; + const int panel_height = panel_padding * 2 + line_height * 18; + const int panel_x = gr_screen.center_offset_x + (gr_screen.center_w / 4); + const int panel_y = gr_screen.center_offset_y + gr_screen.center_h - panel_height - (gr_screen.center_h / 6); + + gr_line(panel_x, panel_y, panel_x + panel_width, panel_y, GR_RESIZE_NONE); + gr_line(panel_x, panel_y + panel_height, panel_x + panel_width, panel_y + panel_height, GR_RESIZE_NONE); + gr_line(panel_x, panel_y, panel_x, panel_y + panel_height, GR_RESIZE_NONE); + gr_line(panel_x + panel_width, panel_y, panel_x + panel_width, panel_y + panel_height, GR_RESIZE_NONE); + gr_line(panel_x, + panel_y + line_height + panel_padding + 1, + panel_x + panel_width, + panel_y + line_height + panel_padding + 1, + GR_RESIZE_NONE); + + if (Photo_mode_grid_enabled) { + const int x1 = gr_screen.center_offset_x + gr_screen.center_w / 3; + const int x2 = gr_screen.center_offset_x + (gr_screen.center_w * 2) / 3; + const int y1 = gr_screen.center_offset_y + gr_screen.center_h / 3; + const int y2 = gr_screen.center_offset_y + (gr_screen.center_h * 2) / 3; + gr_line(x1, gr_screen.center_offset_y, x1, gr_screen.center_offset_y + gr_screen.center_h, GR_RESIZE_NONE); + gr_line(x2, gr_screen.center_offset_y, x2, gr_screen.center_offset_y + gr_screen.center_h, GR_RESIZE_NONE); + gr_line(gr_screen.center_offset_x, y1, gr_screen.center_offset_x + gr_screen.center_w, y1, GR_RESIZE_NONE); + gr_line(gr_screen.center_offset_x, y2, gr_screen.center_offset_x + gr_screen.center_w, y2, GR_RESIZE_NONE); + } + + int line = panel_y + panel_padding; + const int text_x = panel_x + panel_padding; + gr_printf_no_resize(text_x, line, XSTR("Photo Mode", 1892)); + line += line_height; + line += line_height; + gr_printf_no_resize(text_x, line, XSTR("Controls", 1893)); + line += line_height; + gr_printf_no_resize(text_x, line, XSTR("Toggle: %s", 1894), toggle_keybind.c_str()); + line += line_height; + gr_printf_no_resize(text_x, line, XSTR("Previous Filter: %s", 1895), prev_filter_keybind.c_str()); + line += line_height; + gr_printf_no_resize(text_x, line, XSTR("Next Filter: %s", 1896), next_filter_keybind.c_str()); + line += line_height; + gr_printf_no_resize(text_x, line, XSTR("Reset Filters: %s", 1897), reset_filter_keybind.c_str()); + line += line_height; + gr_printf_no_resize(text_x, line, XSTR("Decrease Parameter: %s", 1899), decrease_param_keybind.c_str()); + line += line_height; + gr_printf_no_resize(text_x, line, XSTR("Increase Parameter: %s", 1900), increase_param_keybind.c_str()); + line += line_height; + gr_printf_no_resize(text_x, line, XSTR("Toggle Thirds Grid: %s", 1901), grid_keybind.c_str()); + line += line_height; + line += line_height; + gr_printf_no_resize(text_x, line, XSTR("Status", 1902)); + line += line_height; + gr_printf_no_resize(text_x, line, XSTR("Time Compression: %.2fx", 1903), f2fl(Game_time_compression)); + line += line_height; + gr_printf_no_resize(text_x, + line, + XSTR("Cam Pos: X %.1f Y %.1f Z %.1f", 1904), + cam_pos.xyz.x, + cam_pos.xyz.y, + cam_pos.xyz.z); + line += line_height; + gr_set_color_fast( + Photo_mode_selected_parameter == PHOTO_MODE_PARAM_SATURATION ? &Color_bright_white : &Color_silver); + gr_printf_no_resize(text_x, + line, + XSTR("Saturation: %d", 1905), + Photo_mode_parameter_values[PHOTO_MODE_PARAM_SATURATION]); + line += line_height; + gr_set_color_fast( + Photo_mode_selected_parameter == PHOTO_MODE_PARAM_BRIGHTNESS ? &Color_bright_white : &Color_silver); + gr_printf_no_resize(text_x, + line, + XSTR("Brightness: %d", 1906), + Photo_mode_parameter_values[PHOTO_MODE_PARAM_BRIGHTNESS]); + line += line_height; + gr_set_color_fast(Photo_mode_selected_parameter == PHOTO_MODE_PARAM_CONTRAST ? &Color_bright_white : &Color_silver); + gr_printf_no_resize(text_x, line, XSTR("Contrast: %d", 1907), Photo_mode_parameter_values[PHOTO_MODE_PARAM_CONTRAST]); + line += line_height; + gr_set_color_fast(&Color_silver); + gr_printf_no_resize(text_x, line, XSTR("Grid: %s", 1908), Photo_mode_grid_enabled ? XSTR("On", 1285) : XSTR("Off", 1286)); + + font::set_font(old_font); +} + +void photo_mode_clear_screenshot_queued_flag() +{ + Photo_mode_screenshot_queued_this_frame = false; +} + +void photo_mode_set_screenshot_queued_flag() +{ + Photo_mode_screenshot_queued_this_frame = true; +} + +void game_toggle_photo_mode() +{ + photo_mode_set_active(!Photo_mode_active); +} + +void game_set_photo_mode_allowed(bool allowed) +{ + Photo_mode_allowed = allowed && ((Game_mode & GM_MULTIPLAYER) == 0); + + if (!Photo_mode_allowed && Photo_mode_active) { + photo_mode_set_active(false); + } +} + +bool game_get_photo_mode_allowed() +{ + return Photo_mode_allowed && ((Game_mode & GM_MULTIPLAYER) == 0); +} + +bool game_is_photo_mode_active() +{ + return Photo_mode_active; +} + +void game_cycle_photo_mode_filter(int direction) +{ + if (!Photo_mode_active) { + return; + } + + Photo_mode_selected_parameter += direction; + while (Photo_mode_selected_parameter < 0) { + Photo_mode_selected_parameter += PHOTO_MODE_PARAM_COUNT; + } + while (Photo_mode_selected_parameter >= PHOTO_MODE_PARAM_COUNT) { + Photo_mode_selected_parameter -= PHOTO_MODE_PARAM_COUNT; + } +} + +void game_reset_photo_mode_filters() +{ + if (!Photo_mode_active) { + return; + } + + Photo_mode_parameter_values = Photo_mode_saved_parameter_values; + photo_mode_apply_parameter_values(); +} + +void game_adjust_photo_mode_filter_parameter(int delta) +{ + if (!Photo_mode_active) { + return; + } + + Photo_mode_parameter_values[Photo_mode_selected_parameter] = + std::clamp(Photo_mode_parameter_values[Photo_mode_selected_parameter] + delta, 0, 200); + photo_mode_apply_parameter_values(); +} + +void game_toggle_photo_mode_grid() +{ + if (!Photo_mode_active) { + return; + } + + Photo_mode_grid_enabled = !Photo_mode_grid_enabled; +} diff --git a/code/camera/photomode.h b/code/camera/photomode.h new file mode 100644 index 00000000000..194f54f3980 --- /dev/null +++ b/code/camera/photomode.h @@ -0,0 +1,17 @@ +#pragma once + +void photo_mode_set_active(bool active); +void photo_mode_do_frame(float frame_time); +void photo_mode_maybe_render_hud(); +void photo_mode_clear_screenshot_queued_flag(); +void photo_mode_set_screenshot_queued_flag(); + +// mission-level permission to allow/disallow Photo Mode +void game_toggle_photo_mode(); +void game_set_photo_mode_allowed(bool allowed); +bool game_get_photo_mode_allowed(); +bool game_is_photo_mode_active(); +void game_cycle_photo_mode_filter(int direction); +void game_reset_photo_mode_filters(); +void game_adjust_photo_mode_filter_parameter(int delta); +void game_toggle_photo_mode_grid(); \ No newline at end of file diff --git a/code/controlconfig/controlsconfig.h b/code/controlconfig/controlsconfig.h index 51022c85f74..a4403199ae4 100644 --- a/code/controlconfig/controlsconfig.h +++ b/code/controlconfig/controlsconfig.h @@ -308,6 +308,14 @@ enum IoActionId : int { CYCLE_PRIMARY_WEAPON_PATTERN, + TOGGLE_PHOTO_MODE, + PHOTO_MODE_FILTER_PREV, + PHOTO_MODE_FILTER_NEXT, + PHOTO_MODE_FILTER_RESET, + PHOTO_MODE_PARAM_DECREASE, + PHOTO_MODE_PARAM_INCREASE, + PHOTO_MODE_TOGGLE_GRID, + /*! * This must always be below the last defined item */ diff --git a/code/controlconfig/controlsconfigcommon.cpp b/code/controlconfig/controlsconfigcommon.cpp index e44a2c9e3f5..b30a7d26d1f 100644 --- a/code/controlconfig/controlsconfigcommon.cpp +++ b/code/controlconfig/controlsconfigcommon.cpp @@ -265,6 +265,13 @@ void control_config_common_init_bindings() { (TOGGLE_HUD_CONTRAST, KEY_L, -1, COMPUTER_TAB, 1, "Toggle High HUD Contrast", CC_TYPE_TRIGGER) (TOGGLE_HUD_SHADOWS, KEY_ALTED | KEY_L, -1, COMPUTER_TAB, 1781, "Toggle HUD Drop Shadows", CC_TYPE_TRIGGER) (HUD_TARGETBOX_TOGGLE_WIREFRAME, KEY_ALTED | KEY_SHIFTED | KEY_Q, -1, COMPUTER_TAB, 1, "Toggle HUD Wireframe Target View", CC_TYPE_TRIGGER) + (TOGGLE_PHOTO_MODE, KEY_ALTED | KEY_1, -1, COMPUTER_TAB, 1910, "Toggle Photo Mode", CC_TYPE_TRIGGER) + (PHOTO_MODE_FILTER_PREV, KEY_ALTED | KEY_2, -1, COMPUTER_TAB, 1911, "Photo Mode Previous Parameter", CC_TYPE_TRIGGER) + (PHOTO_MODE_FILTER_NEXT, KEY_ALTED | KEY_3, -1, COMPUTER_TAB, 1912, "Photo Mode Next Parameter", CC_TYPE_TRIGGER) + (PHOTO_MODE_FILTER_RESET, KEY_ALTED | KEY_4, -1, COMPUTER_TAB, 1913, "Photo Mode Reset Parameters", CC_TYPE_TRIGGER) + (PHOTO_MODE_PARAM_DECREASE, KEY_ALTED | KEY_SHIFTED | KEY_MINUS, -1, COMPUTER_TAB, 1914, "Photo Mode Decrease Selected Parameter", CC_TYPE_TRIGGER) + (PHOTO_MODE_PARAM_INCREASE, KEY_ALTED | KEY_SHIFTED | KEY_EQUAL, -1, COMPUTER_TAB, 1915, "Photo Mode Increase Selected Parameter", CC_TYPE_TRIGGER) + (PHOTO_MODE_TOGGLE_GRID, KEY_ALTED | KEY_5, -1, COMPUTER_TAB, 1916, "Photo Mode Toggle Grid", CC_TYPE_TRIGGER) // Custom Controls (CUSTOM_CONTROL_1, KEY_ALTED | KEY_SHIFTED | KEY_1, -1, COMPUTER_TAB, 1784, "Custom Control 1", CC_TYPE_TRIGGER, true) @@ -432,6 +439,13 @@ SCP_unordered_map old_text = { {"Up Thrust", UP_SLIDE_THRUST}, {"Down Thrust", DOWN_SLIDE_THRUST}, {"Toggle HUD Wireframe Target View", HUD_TARGETBOX_TOGGLE_WIREFRAME}, + {"Toggle Photo Mode", TOGGLE_PHOTO_MODE}, + {"Photo Mode Previous Parameter", PHOTO_MODE_FILTER_PREV}, + {"Photo Mode Next Parameter", PHOTO_MODE_FILTER_NEXT}, + {"Photo Mode Reset Parameters", PHOTO_MODE_FILTER_RESET}, + {"Photo Mode Decrease Selected Parameter", PHOTO_MODE_PARAM_DECREASE}, + {"Photo Mode Increase Selected Parameter", PHOTO_MODE_PARAM_INCREASE}, + {"Photo Mode Toggle Grid", PHOTO_MODE_TOGGLE_GRID}, {"Top-Down View", VIEW_TOPDOWN}, {"Target Padlock View", VIEW_TRACK_TARGET}, @@ -1151,6 +1165,13 @@ void LoadEnumsIntoActionMap() { ADD_ENUM_TO_ACTION_MAP(UP_SLIDE_THRUST) ADD_ENUM_TO_ACTION_MAP(DOWN_SLIDE_THRUST) ADD_ENUM_TO_ACTION_MAP(HUD_TARGETBOX_TOGGLE_WIREFRAME) + ADD_ENUM_TO_ACTION_MAP(TOGGLE_PHOTO_MODE) + ADD_ENUM_TO_ACTION_MAP(PHOTO_MODE_FILTER_PREV) + ADD_ENUM_TO_ACTION_MAP(PHOTO_MODE_FILTER_NEXT) + ADD_ENUM_TO_ACTION_MAP(PHOTO_MODE_FILTER_RESET) + ADD_ENUM_TO_ACTION_MAP(PHOTO_MODE_PARAM_DECREASE) + ADD_ENUM_TO_ACTION_MAP(PHOTO_MODE_PARAM_INCREASE) + ADD_ENUM_TO_ACTION_MAP(PHOTO_MODE_TOGGLE_GRID) ADD_ENUM_TO_ACTION_MAP(VIEW_TOPDOWN) ADD_ENUM_TO_ACTION_MAP(VIEW_TRACK_TARGET) diff --git a/code/graphics/2d.cpp b/code/graphics/2d.cpp index 68c364e0c71..e713e20cf1a 100644 --- a/code/graphics/2d.cpp +++ b/code/graphics/2d.cpp @@ -2963,6 +2963,11 @@ void gr_request_screenshot(const char* filename) } } +bool gr_is_screenshot_requested() +{ + return !Pending_screenshot_filename.empty(); +} + void gr_print_timestamp(int x, int y, fix timestamp, int resize_mode) { int seconds = f2i(timestamp); diff --git a/code/graphics/2d.h b/code/graphics/2d.h index f0b825aa908..7f982f703fa 100644 --- a/code/graphics/2d.h +++ b/code/graphics/2d.h @@ -1075,6 +1075,7 @@ extern void gr_activate(int active); #define gr_dump_envmap GR_CALL(gr_screen.gf_dump_envmap) void gr_request_screenshot(const char* filename); +bool gr_is_screenshot_requested(); //#define gr_flip GR_CALL(gr_screen.gf_flip) void gr_flip(bool execute_scripting = true); diff --git a/code/io/keycontrol.cpp b/code/io/keycontrol.cpp index d191caba4e4..679b4b985af 100644 --- a/code/io/keycontrol.cpp +++ b/code/io/keycontrol.cpp @@ -13,6 +13,7 @@ #include "globalincs/pstypes.h" #include "globalincs/globals.h" #include "globalincs/linklist.h" +#include "camera/photomode.h" #include "io/key.h" #include "io/joy.h" #include "io/timer.h" @@ -328,6 +329,13 @@ int Normal_key_set[] = { MULTI_SELF_DESTRUCT, TOGGLE_HUD, + TOGGLE_PHOTO_MODE, + PHOTO_MODE_FILTER_PREV, + PHOTO_MODE_FILTER_NEXT, + PHOTO_MODE_FILTER_RESET, + PHOTO_MODE_PARAM_DECREASE, + PHOTO_MODE_PARAM_INCREASE, + PHOTO_MODE_TOGGLE_GRID, HUD_TARGETBOX_TOGGLE_WIREFRAME, AUTO_PILOT_TOGGLE, @@ -475,6 +483,13 @@ int Non_critical_key_set[] = { MULTI_SELF_DESTRUCT, TOGGLE_HUD, + TOGGLE_PHOTO_MODE, + PHOTO_MODE_FILTER_PREV, + PHOTO_MODE_FILTER_NEXT, + PHOTO_MODE_FILTER_RESET, + PHOTO_MODE_PARAM_DECREASE, + PHOTO_MODE_PARAM_INCREASE, + PHOTO_MODE_TOGGLE_GRID, HUD_TARGETBOX_TOGGLE_WIREFRAME, AUTO_PILOT_TOGGLE, @@ -1536,9 +1551,13 @@ void game_do_end_mission_popup() if (Game_subspace_effect) { game_start_subspace_ambient_sound(); } - audiostream_unpause_all(); + + if (!game_is_photo_mode_active()) { + audiostream_unpause_all(); + message_resume_all(); + } + weapon_unpause_sounds(); - message_resume_all(); break; } @@ -2230,10 +2249,12 @@ int button_function_demo_valid(int n) case TIME_SLOW_DOWN: ret = 1; - if ( Game_mode & GM_NORMAL && !Time_compression_locked ) { + if (Game_mode & GM_NORMAL && (!Time_compression_locked || game_is_photo_mode_active())) { + const auto min_compression = game_is_photo_mode_active() ? fl2f(0.01f) : (F1_0 / (Cmdline_retail_time_compression_range ? MAX_TIME_DIVIDER_RETAIL : MAX_TIME_DIVIDER)); + // Goober5000 - time dilation only available in cheat mode (see above); // now you can do it with or without pressing the tilde, per Kazan's request - if ((Game_time_compression > F1_0) || (Cheats_enabled && (Game_time_compression > (F1_0 / (Cmdline_retail_time_compression_range ? MAX_TIME_DIVIDER_RETAIL : MAX_TIME_DIVIDER))))) { + if ((Game_time_compression > F1_0) || (Game_time_compression > min_compression && (Cheats_enabled || game_is_photo_mode_active()))) { change_time_compression(0.5f); break; } @@ -2243,7 +2264,7 @@ int button_function_demo_valid(int n) case TIME_SPEED_UP: ret = 1; - if ( Game_mode & GM_NORMAL && !Time_compression_locked ) { + if (Game_mode & GM_NORMAL && (!Time_compression_locked || game_is_photo_mode_active())) { if (Game_time_compression < (F1_0 * (Cmdline_retail_time_compression_range ? MAX_TIME_MULTIPLIER_RETAIL : MAX_TIME_MULTIPLIER))) { change_time_compression(2.0f); break; @@ -2582,6 +2603,34 @@ int button_function(int n) hud_toggle_draw(); break; + case TOGGLE_PHOTO_MODE: + game_toggle_photo_mode(); + break; + + case PHOTO_MODE_FILTER_PREV: + game_cycle_photo_mode_filter(-1); + break; + + case PHOTO_MODE_FILTER_NEXT: + game_cycle_photo_mode_filter(1); + break; + + case PHOTO_MODE_FILTER_RESET: + game_reset_photo_mode_filters(); + break; + + case PHOTO_MODE_PARAM_DECREASE: + game_adjust_photo_mode_filter_parameter(-1); + break; + + case PHOTO_MODE_PARAM_INCREASE: + game_adjust_photo_mode_filter_parameter(1); + break; + + case PHOTO_MODE_TOGGLE_GRID: + game_toggle_photo_mode_grid(); + break; + case HUD_TARGETBOX_TOGGLE_WIREFRAME: if (!Lock_targetbox_mode) { gamesnd_play_iface(InterfaceSounds::USER_SELECT); diff --git a/code/localization/localize.cpp b/code/localization/localize.cpp index 5ad87fe4bfc..8bb0f79f17f 100644 --- a/code/localization/localize.cpp +++ b/code/localization/localize.cpp @@ -64,7 +64,7 @@ bool *Lcl_unexpected_tstring_check = nullptr; // NOTE: with map storage of XSTR strings, the indexes no longer need to be contiguous, // but internal strings should still increment XSTR_SIZE to avoid collisions. // retail XSTR_SIZE = 1570 -// #define XSTR_SIZE 1892 // This is the next available ID +// #define XSTR_SIZE 1917 // This is the next available ID // struct to allow for strings.tbl-determined x offset // offset is 0 for english, by default diff --git a/code/missionui/missionpause.cpp b/code/missionui/missionpause.cpp index d944fd4df38..3bb4e7dbec2 100644 --- a/code/missionui/missionpause.cpp +++ b/code/missionui/missionpause.cpp @@ -12,6 +12,7 @@ #include "controlconfig/controlsconfig.h" #include "freespace.h" +#include "camera/photomode.h" #include "gamesequence/gamesequence.h" #include "globalincs/alphacolors.h" #include "graphics/font.h" @@ -254,8 +255,10 @@ void pause_close() // unpause all weapon sounds weapon_unpause_sounds(); - // unpause voices - message_resume_all(); + // unpause voices unless Photo Mode is keeping playback paused + if (!game_is_photo_mode_active()) { + message_resume_all(); + } // deinit stuff if(Pause_saved_screen != -1) { @@ -273,8 +276,10 @@ void pause_close() io::mouse::CursorManager::get()->popStatus(); - // unpause all the music - audiostream_unpause_all(); + // unpause all the music unless Photo Mode is keeping playback paused + if (!game_is_photo_mode_active()) { + audiostream_unpause_all(); + } Paused = false; } diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index f137e860af9..51e14bba78e 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -26,6 +26,7 @@ #include "asteroid/asteroid.h" #include "autopilot/autopilot.h" #include "camera/camera.h" +#include "camera/photomode.h" #include "cmdline/cmdline.h" #include "debris/debris.h" #include "debugconsole/console.h" @@ -747,6 +748,7 @@ SCP_vector Operators = { { "show-subtitle-image", OP_CUTSCENES_SHOW_SUBTITLE_IMAGE, 8, 11, SEXP_ACTION_OPERATOR, }, { "clear-subtitles", OP_CLEAR_SUBTITLES, 0, 0, SEXP_ACTION_OPERATOR, }, { "lock-perspective", OP_CUTSCENES_FORCE_PERSPECTIVE, 1, 3, SEXP_ACTION_OPERATOR, }, + { "allow-photo-mode", OP_ALLOW_PHOTO_MODE, 1, 1, SEXP_ACTION_OPERATOR, }, { "set-camera-shudder", OP_SET_CAMERA_SHUDDER, 2, 4, SEXP_ACTION_OPERATOR, }, { "supernova-start", OP_SUPERNOVA_START, 1, 1, SEXP_ACTION_OPERATOR, }, { "supernova-stop", OP_SUPERNOVA_STOP, 0, 0, SEXP_ACTION_OPERATOR, }, //CommanderDJ @@ -25700,6 +25702,8 @@ camera* sexp_get_set_camera(bool reset = false) void sexp_set_camera(int node) { + game_set_photo_mode_allowed(false); + if (node < 0) { sexp_get_set_camera(true); @@ -26191,6 +26195,8 @@ void multi_sexp_reset_fov() void sexp_reset_camera(int node) { + game_set_photo_mode_allowed(true); + bool cam_reset = false; camera *cam = cam_get_current().getCamera(); if (cam != nullptr) @@ -26209,6 +26215,8 @@ void sexp_reset_camera(int node) void multi_sexp_reset_camera() { + game_set_photo_mode_allowed(true); + camera *cam = cam_get_current().getCamera(); bool cam_reset = false; @@ -26787,6 +26795,11 @@ void sexp_force_perspective(int n) } } +void sexp_allow_photo_mode(int n) +{ + game_set_photo_mode_allowed(is_sexp_true(n)); +} + void sexp_set_camera_shudder(int n) { int time; @@ -30378,6 +30391,10 @@ int eval_sexp(int cur_node, int referenced_node) sexp_val = SEXP_TRUE; sexp_force_perspective(node); break; + case OP_ALLOW_PHOTO_MODE: + sexp_val = SEXP_TRUE; + sexp_allow_photo_mode(node); + break; case OP_SET_CAMERA_SHUDDER: sexp_val = SEXP_TRUE; @@ -31778,6 +31795,7 @@ int query_operator_return_type(int op) case OP_CUTSCENES_SET_TIME_COMPRESSION: case OP_CUTSCENES_RESET_TIME_COMPRESSION: case OP_CUTSCENES_FORCE_PERSPECTIVE: + case OP_ALLOW_PHOTO_MODE: case OP_SET_CAMERA_SHUDDER: case OP_JUMP_NODE_SET_JUMPNODE_NAME: case OP_JUMP_NODE_SET_JUMPNODE_DISPLAY_NAME: @@ -34426,6 +34444,9 @@ int query_operator_argument_type(int op, int argnum) else return OPF_BOOL; + case OP_ALLOW_PHOTO_MODE: + return OPF_BOOL; + case OP_SET_CAMERA_SHUDDER: if (argnum == 0 || argnum == 1) return OPF_POSITIVE; @@ -36871,6 +36892,7 @@ int get_category(int op_id) case OP_CUTSCENES_SET_TIME_COMPRESSION: case OP_CUTSCENES_RESET_TIME_COMPRESSION: case OP_CUTSCENES_FORCE_PERSPECTIVE: + case OP_ALLOW_PHOTO_MODE: case OP_JUMP_NODE_SET_JUMPNODE_NAME: case OP_JUMP_NODE_SET_JUMPNODE_DISPLAY_NAME: case OP_JUMP_NODE_SET_JUMPNODE_COLOR: @@ -37519,6 +37541,7 @@ int get_subcategory(int op_id) case OP_CUTSCENES_SHOW_SUBTITLE_IMAGE: case OP_CLEAR_SUBTITLES: case OP_CUTSCENES_FORCE_PERSPECTIVE: + case OP_ALLOW_PHOTO_MODE: case OP_SET_CAMERA_SHUDDER: case OP_SUPERNOVA_START: case OP_SUPERNOVA_STOP: @@ -41997,7 +42020,7 @@ SCP_vector Sexp_help = { }, { OP_CUTSCENES_SET_CAMERA, "set-camera\r\n" - "\tSets SEXP camera, or another specified cutscene camera. " + "\tSets SEXP camera, or another specified cutscene camera. Automatically disables photo mode while cutscene camera control is active." "Takes 0 to 1 arguments...\r\n" "\t(optional)\r\n" "\t1:\tCamera name (created if nonexistent)\r\n" @@ -42095,7 +42118,7 @@ SCP_vector Sexp_help = { }, { OP_CUTSCENES_RESET_CAMERA, "reset-camera\r\n" - "\tReleases cutscene camera control. " + "\tReleases cutscene camera control. Automatically re-enables photo mode." "Takes 1 optional argument...\r\n" "\t(optional)\r\n" "\t1:\tReset camera data (Position, facing, FOV...) (default: false)" @@ -42182,6 +42205,12 @@ SCP_vector Sexp_help = { "\t3:\tIf in first-person, true to lock the hat/slew/free-look/target-track mode, false to unlock it (optional)\r\n" }, + { OP_ALLOW_PHOTO_MODE, "allow-photo-mode\r\n" + "\tAllows or disallows Photo Mode for this mission. " + "Takes 1 argument...\r\n" + "\t1:\tTrue to allow Photo Mode, false to disallow it\r\n" + }, + { OP_SET_CAMERA_SHUDDER, "set-camera-shudder\r\n" "\tCauses the camera to shudder. Normally this will only work if the camera is showing the player's viewpoint (i.e. the HUD), unless the Everywhere flag is set.\r\n\r\n" "Takes 2 to 4 arguments...\r\n" diff --git a/code/parse/sexp.h b/code/parse/sexp.h index 45461e9e234..08009abb56f 100644 --- a/code/parse/sexp.h +++ b/code/parse/sexp.h @@ -686,6 +686,7 @@ enum : int { OP_CUTSCENES_SET_TIME_COMPRESSION, // WMC OP_CUTSCENES_RESET_TIME_COMPRESSION, // WMC OP_CUTSCENES_FORCE_PERSPECTIVE, // WMC + OP_ALLOW_PHOTO_MODE, OP_JUMP_NODE_SET_JUMPNODE_NAME, // CommanderDJ OP_JUMP_NODE_SET_JUMPNODE_DISPLAY_NAME, OP_JUMP_NODE_SET_JUMPNODE_COLOR, // WMC diff --git a/code/scripting/global_hooks.cpp b/code/scripting/global_hooks.cpp index b5f8e673352..0d82cb0d4c7 100644 --- a/code/scripting/global_hooks.cpp +++ b/code/scripting/global_hooks.cpp @@ -86,6 +86,14 @@ const std::shared_ptr> OnGameplayStart = Hook<>::Factory("On Gameplay Sta "Invoked when the gameplay portion of a mission starts.", { {"Player", "object", "The player object."} }); +const std::shared_ptr> OnPhotoModeStarted = Hook<>::Factory("On Photo Mode Started", + "Invoked when Photo Mode is enabled.", + {}); + +const std::shared_ptr> OnPhotoModeEnded = Hook<>::Factory("On Photo Mode Ended", + "Invoked when Photo Mode is disabled.", + {}); + const std::shared_ptr> OnAction = Hook::Factory("On Action", "Invoked whenever a user action was invoked through control input.", { {"Action", "string", "The name of the action that was executed."} }); diff --git a/code/scripting/global_hooks.h b/code/scripting/global_hooks.h index d797f54b2dc..8112c6bf537 100644 --- a/code/scripting/global_hooks.h +++ b/code/scripting/global_hooks.h @@ -22,6 +22,8 @@ extern const std::shared_ptr> OnCampaignMissionAccept; extern const std::shared_ptr> OnBriefStage; extern const std::shared_ptr> OnMissionStart; extern const std::shared_ptr> OnGameplayStart; +extern const std::shared_ptr> OnPhotoModeStarted; +extern const std::shared_ptr> OnPhotoModeEnded; extern const std::shared_ptr> OnAction; extern const std::shared_ptr> OnActionStopped; diff --git a/code/source_groups.cmake b/code/source_groups.cmake index 0b92ef05420..46321772d54 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -115,6 +115,8 @@ add_file_folder("Bmpman" add_file_folder("Camera" camera/camera.cpp camera/camera.h + camera/photomode.cpp + camera/photomode.h ) add_file_folder("Cheats Table" diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index a175f82c7bf..c6e611be5ae 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -41,6 +41,7 @@ #include "asteroid/asteroid.h" #include "autopilot/autopilot.h" #include "bmpman/bmpman.h" +#include "camera/photomode.h" #include "cfile/cfile.h" #include "cheats_table/cheats_table.h" #include "cmdline/cmdline.h" @@ -945,6 +946,7 @@ void game_level_close() ct_level_close(); beam_level_close(); mission_brief_common_reset(); // close out parsed briefing/mission stuff + photo_mode_set_active(false); cam_close(); subtitles_close(); animation::ModelAnimationSet::stopAnimations(); @@ -1044,6 +1046,7 @@ void game_level_init() Perspective_locked = false; Slew_locked = false; + game_set_photo_mode_allowed(true); // reset the geometry map and distortion map batcher, this should to be done pretty soon in this mission load process (though it's not required) batch_reset(); @@ -3639,6 +3642,8 @@ void game_simulation_frame() cam_do_frame(flRealframetime); } + photo_mode_do_frame(flRealframetime); + // blow ships up in multiplayer dogfight if( MULTIPLAYER_MASTER && (Net_player != nullptr) && (Netgame.type_flags & NG_TYPE_DOGFIGHT) && (f2fl(Missiontime) >= 2.0f) && !dogfight_blown){ // blow up all non-player ships @@ -4152,6 +4157,7 @@ void game_do_full_frame(DEBUG_TIMER_SIG const vec3d* offset = nullptr, const mat gr_reset_clip(); game_render_post_frame(); + photo_mode_maybe_render_hud(); game_tst_frame(); @@ -4549,6 +4555,7 @@ void game_do_frame(bool set_frametime) } game_update_missiontime(); + photo_mode_clear_screenshot_queued_flag(); if (Game_mode & GM_STANDALONE_SERVER) { std_multi_set_standalone_missiontime(f2fl(Missiontime)); @@ -4759,6 +4766,8 @@ int game_poll() case KEY_PRINT_SCRN: { + photo_mode_set_screenshot_queued_flag(); + static int counter = os_config_read_uint(nullptr, "ScreenshotNum", 0); char tmp_name[MAX_FILENAME_LEN]; @@ -5272,6 +5281,10 @@ void game_leave_state( int old_state, int new_state ) break; } + if (old_state == GS_STATE_GAME_PLAY && new_state != GS_STATE_GAME_PLAY) { + photo_mode_set_active(false); + } + // This is kind of a hack but it ensures options are logged even if scripting calls for a state change with an override active if (old_state == GS_STATE_OPTIONS_MENU) { if (new_state != GS_STATE_CONTROL_CONFIG && new_state != GS_STATE_HUD_CONFIG) { From 656737d9978833347d50eeaede7a5da1d33f09fb Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 14 Mar 2026 16:54:44 -0500 Subject: [PATCH 2/3] fix gr string errors --- code/camera/photomode.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/camera/photomode.cpp b/code/camera/photomode.cpp index 5ee18ca1657..1c9157f56b1 100644 --- a/code/camera/photomode.cpp +++ b/code/camera/photomode.cpp @@ -380,10 +380,10 @@ void photo_mode_maybe_render_hud() int line = panel_y + panel_padding; const int text_x = panel_x + panel_padding; - gr_printf_no_resize(text_x, line, XSTR("Photo Mode", 1892)); + gr_printf_no_resize(text_x, line, "%s", XSTR("Photo Mode", 1892)); line += line_height; line += line_height; - gr_printf_no_resize(text_x, line, XSTR("Controls", 1893)); + gr_printf_no_resize(text_x, line, "%s", XSTR("Controls", 1893)); line += line_height; gr_printf_no_resize(text_x, line, XSTR("Toggle: %s", 1894), toggle_keybind.c_str()); line += line_height; @@ -400,7 +400,7 @@ void photo_mode_maybe_render_hud() gr_printf_no_resize(text_x, line, XSTR("Toggle Thirds Grid: %s", 1901), grid_keybind.c_str()); line += line_height; line += line_height; - gr_printf_no_resize(text_x, line, XSTR("Status", 1902)); + gr_printf_no_resize(text_x, line, "%s", XSTR("Status", 1902)); line += line_height; gr_printf_no_resize(text_x, line, XSTR("Time Compression: %.2fx", 1903), f2fl(Game_time_compression)); line += line_height; From 98663147db72c8a14ceb709a82fc2a49effc4fbe Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 14 Mar 2026 17:21:53 -0500 Subject: [PATCH 3/3] return raw char array --- code/camera/photomode.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/camera/photomode.cpp b/code/camera/photomode.cpp index 1c9157f56b1..85d35933c67 100644 --- a/code/camera/photomode.cpp +++ b/code/camera/photomode.cpp @@ -165,7 +165,7 @@ SCP_string format_photo_mode_keybind(int action) auto secondary = Control_config[action].second.textify(); if (primary.empty() && secondary.empty()) { - return SCP_string(XSTR("Unbound", 1909)); + return XSTR("Unbound", 1909); } if (primary.empty()) { return secondary;