From 3aff98e5e37a4aca912c640cd13673605fc5970d Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Thu, 16 Oct 2025 00:29:49 -0400 Subject: [PATCH 01/65] preliminary cleanup for split_str_once refactor String processing enhancement prerequisites for the upcoming `split_str_once` PR. 1. Add overloads of some string processing functions with a length parameter, to allow processing of substrings of C-strings 2. Add overloads of gr_printf that work with string lengths 3. Add constructors to `HUD_message_data` and `line_node` 4. Rename "size" to "len" in `vm_strndup` for clarity 5. Fill in some string attribute annotations 6. Make the rest of the HUD render functions `const` 7. Removed unneeded circular #includes in code/globalincs/memory/ --- code/globalincs/memory/memory.h | 2 - code/globalincs/memory/utils.h | 12 ++--- code/globalincs/toolchain/clang.h | 1 + code/globalincs/toolchain/gcc.h | 1 + code/globalincs/toolchain/mingw.h | 1 + code/graphics/2d.h | 12 +++-- code/graphics/software/font.cpp | 77 ++++++++++++++++++++----------- code/hud/hud.cpp | 41 ++++++++++------ code/hud/hud.h | 13 ++++-- code/hud/hudmessage.cpp | 38 ++++----------- code/hud/hudmessage.h | 19 ++++++-- code/weapon/emp.cpp | 17 ++++--- code/weapon/emp.h | 2 +- 13 files changed, 135 insertions(+), 101 deletions(-) diff --git a/code/globalincs/memory/memory.h b/code/globalincs/memory/memory.h index a25c2974a58..f3b7214a336 100644 --- a/code/globalincs/memory/memory.h +++ b/code/globalincs/memory/memory.h @@ -4,8 +4,6 @@ #include #include -#include "globalincs/pstypes.h" - namespace memory { struct quiet_alloc_t { quiet_alloc_t(){} }; diff --git a/code/globalincs/memory/utils.h b/code/globalincs/memory/utils.h index adc3386b48b..72a66bbe044 100644 --- a/code/globalincs/memory/utils.h +++ b/code/globalincs/memory/utils.h @@ -2,18 +2,16 @@ #include -#include "globalincs/pstypes.h" - -inline char *vm_strndup(const char *ptr, size_t size) +inline char *vm_strndup(const char *ptr, size_t len) { - char *dst = (char *) vm_malloc(size + 1); + char *dst = static_cast(vm_malloc(len + 1)); if (!dst) - return NULL; + return nullptr; - std::strncpy(dst, ptr, size); + std::strncpy(dst, ptr, len); // make sure it has a NULL terminiator - dst[size] = '\0'; + dst[len] = '\0'; return dst; } diff --git a/code/globalincs/toolchain/clang.h b/code/globalincs/toolchain/clang.h index 2f4ae40242c..1056c893bd2 100644 --- a/code/globalincs/toolchain/clang.h +++ b/code/globalincs/toolchain/clang.h @@ -21,6 +21,7 @@ #if defined(__clang__) #define SCP_FORMAT_STRING +// from gcc: Since non-static C++ methods have an implicit this argument, the arguments of such methods should be counted from two, not one, when giving values for string-index and first-to-check. #define SCP_FORMAT_STRING_ARGS(x,y) __attribute__((format(printf, x, y))) #define __UNUSED __attribute__((__unused__)) diff --git a/code/globalincs/toolchain/gcc.h b/code/globalincs/toolchain/gcc.h index 8e884653719..80c603ccd39 100644 --- a/code/globalincs/toolchain/gcc.h +++ b/code/globalincs/toolchain/gcc.h @@ -22,6 +22,7 @@ #if defined(__GNUC__) && !defined(__clang__) #define SCP_FORMAT_STRING +// from gcc: Since non-static C++ methods have an implicit this argument, the arguments of such methods should be counted from two, not one, when giving values for string-index and first-to-check. #define SCP_FORMAT_STRING_ARGS(x,y) __attribute__((format(printf, x, y))) #define __UNUSED __attribute__((__unused__)) diff --git a/code/globalincs/toolchain/mingw.h b/code/globalincs/toolchain/mingw.h index 5b7924b3dea..6f454689f47 100644 --- a/code/globalincs/toolchain/mingw.h +++ b/code/globalincs/toolchain/mingw.h @@ -23,6 +23,7 @@ #include #define SCP_FORMAT_STRING +// from gcc: Since non-static C++ methods have an implicit this argument, the arguments of such methods should be counted from two, not one, when giving values for string-index and first-to-check. #define SCP_FORMAT_STRING_ARGS(x,y) __attribute__((format(__MINGW_PRINTF_FORMAT, x, y))) #define __UNUSED __attribute__((__unused__)) diff --git a/code/graphics/2d.h b/code/graphics/2d.h index b701bdeefc6..481a245d3cb 100644 --- a/code/graphics/2d.h +++ b/code/graphics/2d.h @@ -1043,13 +1043,17 @@ bool gr_resize_screen_posf(float *x, float *y, float *w = NULL, float *h = NULL, // Does formatted printing. This calls gr_string after formatting, // so if you don't need to format the string, then call gr_string // directly. -extern void gr_printf( int x, int y, const char * format, SCP_FORMAT_STRING ... ) SCP_FORMAT_STRING_ARGS(3, 4); +extern void gr_printf(int x, int y, SCP_FORMAT_STRING const char *format, ...) SCP_FORMAT_STRING_ARGS(3, 4); +extern void gr_printf(int x, int y, size_t len, SCP_FORMAT_STRING const char *format, ...) SCP_FORMAT_STRING_ARGS(4, 5); // same as gr_printf but positions text correctly in menus -extern void gr_printf_menu( int x, int y, const char * format, SCP_FORMAT_STRING ... ) SCP_FORMAT_STRING_ARGS(3, 4); +extern void gr_printf_menu(int x, int y, SCP_FORMAT_STRING const char *format, ...) SCP_FORMAT_STRING_ARGS(3, 4); +extern void gr_printf_menu(int x, int y, size_t len, SCP_FORMAT_STRING const char *format, ...) SCP_FORMAT_STRING_ARGS(4, 5); // same as gr_printf_menu but accounts for menu zooming -extern void gr_printf_menu_zoomed( int x, int y, const char * format, SCP_FORMAT_STRING ... ) SCP_FORMAT_STRING_ARGS(3, 4); +extern void gr_printf_menu_zoomed(int x, int y, SCP_FORMAT_STRING const char *format, ...) SCP_FORMAT_STRING_ARGS(3, 4); +extern void gr_printf_menu_zoomed(int x, int y, size_t len, SCP_FORMAT_STRING const char *format, ...) SCP_FORMAT_STRING_ARGS(4, 5); // same as gr_printf but doesn't resize for non-standard resolutions -extern void gr_printf_no_resize( int x, int y, const char * format, SCP_FORMAT_STRING ... ) SCP_FORMAT_STRING_ARGS(3, 4); +extern void gr_printf_no_resize(int x, int y, SCP_FORMAT_STRING const char *format, ...) SCP_FORMAT_STRING_ARGS(3, 4); +extern void gr_printf_no_resize(int x, int y, size_t len, SCP_FORMAT_STRING const char *format, ...) SCP_FORMAT_STRING_ARGS(4, 5); // Returns the size of the string in pixels in w and h extern void gr_get_string_size( int *w, int *h, const char * text, float scaleMultiplier = 1.0f, size_t len = std::string::npos); diff --git a/code/graphics/software/font.cpp b/code/graphics/software/font.cpp index 1d8b542a38d..52089379de9 100644 --- a/code/graphics/software/font.cpp +++ b/code/graphics/software/font.cpp @@ -801,60 +801,81 @@ void gr_string_win(int x, int y, char *s) #endif // ifdef _WIN32 -char grx_printf_text[2048]; +static char grx_printf_text[2048]; -void gr_printf(int x, int y, const char * format, ...) +void gr_printf_args(int resize_mode, int x, int y, size_t len, SCP_FORMAT_STRING const char *format, va_list args) { - va_list args; + if (!FontManager::isReady()) + return; - if (!FontManager::isReady()) return; + len = std::min(len, sizeof(grx_printf_text) - 1); - va_start(args, format); - vsnprintf(grx_printf_text, sizeof(grx_printf_text), format, args); - va_end(args); - grx_printf_text[sizeof(grx_printf_text) - 1] = '\0'; + vsnprintf(grx_printf_text, len+1, format, args); + grx_printf_text[len] = '\0'; - gr_string(x, y, grx_printf_text); + gr_string(x, y, grx_printf_text, resize_mode, 1.0f, len); } -void gr_printf_menu(int x, int y, const char * format, ...) +void gr_printf(int x, int y, SCP_FORMAT_STRING const char *format, ...) { va_list args; - - if (!FontManager::isReady()) return; - va_start(args, format); - vsnprintf(grx_printf_text, sizeof(grx_printf_text), format, args); + gr_printf_args(GR_RESIZE_FULL, x, y, std::string::npos, format, args); va_end(args); - grx_printf_text[sizeof(grx_printf_text) - 1] = '\0'; - - gr_string(x, y, grx_printf_text, GR_RESIZE_MENU); } -void gr_printf_menu_zoomed(int x, int y, const char * format, ...) +void gr_printf(int x, int y, size_t len, SCP_FORMAT_STRING const char *format, ...) { va_list args; + va_start(args, format); + gr_printf_args(GR_RESIZE_FULL, x, y, len, format, args); + va_end(args); +} - if (!FontManager::isReady()) return; - +void gr_printf_menu(int x, int y, SCP_FORMAT_STRING const char *format, ...) +{ + va_list args; va_start(args, format); - vsnprintf(grx_printf_text, sizeof(grx_printf_text), format, args); + gr_printf_args(GR_RESIZE_MENU, x, y, std::string::npos, format, args); va_end(args); - grx_printf_text[sizeof(grx_printf_text) - 1] = '\0'; +} - gr_string(x, y, grx_printf_text, GR_RESIZE_MENU_ZOOMED); +void gr_printf_menu(int x, int y, size_t len, SCP_FORMAT_STRING const char *format, ...) +{ + va_list args; + va_start(args, format); + gr_printf_args(GR_RESIZE_MENU, x, y, len, format, args); + va_end(args); } -void gr_printf_no_resize(int x, int y, const char * format, ...) +void gr_printf_menu_zoomed(int x, int y, SCP_FORMAT_STRING const char *format, ...) { va_list args; + va_start(args, format); + gr_printf_args(GR_RESIZE_MENU_ZOOMED, x, y, std::string::npos, format, args); + va_end(args); +} - if (!FontManager::isReady()) return; +void gr_printf_menu_zoomed(int x, int y, size_t len, SCP_FORMAT_STRING const char *format, ...) +{ + va_list args; + va_start(args, format); + gr_printf_args(GR_RESIZE_MENU_ZOOMED, x, y, len, format, args); + va_end(args); +} +void gr_printf_no_resize(int x, int y, SCP_FORMAT_STRING const char *format, ...) +{ + va_list args; va_start(args, format); - vsnprintf(grx_printf_text, sizeof(grx_printf_text), format, args); + gr_printf_args(GR_RESIZE_NONE, x, y, std::string::npos, format, args); va_end(args); - grx_printf_text[sizeof(grx_printf_text) - 1] = '\0'; +} - gr_string(x, y, grx_printf_text, GR_RESIZE_NONE); +void gr_printf_no_resize(int x, int y, size_t len, SCP_FORMAT_STRING const char *format, ...) +{ + va_list args; + va_start(args, format); + gr_printf_args(GR_RESIZE_NONE, x, y, len, format, args); + va_end(args); } diff --git a/code/hud/hud.cpp b/code/hud/hud.cpp index ee3da79cfbb..2761a3e1545 100644 --- a/code/hud/hud.cpp +++ b/code/hud/hud.cpp @@ -896,7 +896,12 @@ void HudGauge::render(float /*frametime*/, bool config) } } -void HudGauge::renderString(int x, int y, const char *str, float scale, bool config) +void HudGauge::renderString(int x, int y, const char *str, float scale, bool config) const +{ + renderString(x, y, str, std::string::npos, scale, config); +} + +void HudGauge::renderString(int x, int y, const char *str, size_t len, float scale, bool config) const { int nx = 0, ny = 0; int resize = GR_RESIZE_FULL; @@ -923,15 +928,20 @@ void HudGauge::renderString(int x, int y, const char *str, float scale, bool con if (HUD_shadows) { color cur = gr_screen.current_color; gr_set_color_fast(&Color_black); - gr_string(x + nx + 1, y + ny + 1, str, resize, scale); + gr_string(x + nx + 1, y + ny + 1, str, resize, scale, len); gr_set_color_fast(&cur); } - gr_string(x + nx, y + ny, str, resize, scale); + gr_string(x + nx, y + ny, str, resize, scale, len); gr_reset_screen_scale(); } -void HudGauge::renderString(int x, int y, int gauge_id, const char *str, float scale, bool config) +void HudGauge::renderString(int x, int y, int gauge_id, const char *str, float scale, bool config) const +{ + renderString(x, y, gauge_id, str, std::string::npos, scale, config); +} + +void HudGauge::renderString(int x, int y, int gauge_id, const char *str, size_t len, float scale, bool config) const { int nx = 0, ny = 0; int resize = GR_RESIZE_FULL; @@ -960,32 +970,37 @@ void HudGauge::renderString(int x, int y, int gauge_id, const char *str, float s if (HUD_shadows) { color cur = gr_screen.current_color; gr_set_color_fast(&Color_black); - emp_hud_string(x + nx + 1, y + ny + 1, gauge_id, str, resize, scale); + emp_hud_string(x + nx + 1, y + ny + 1, gauge_id, str, len, resize, scale); gr_set_color_fast(&cur); } - emp_hud_string(x + nx, y + ny, gauge_id, str, resize, scale); + emp_hud_string(x + nx, y + ny, gauge_id, str, len, resize, scale); } else { if (HUD_shadows) { color cur = gr_screen.current_color; gr_set_color_fast(&Color_black); - gr_string(x + nx + 1, y + ny + 1, str, resize, scale); + gr_string(x + nx + 1, y + ny + 1, str, resize, scale, len); gr_set_color_fast(&cur); } - gr_string(x + nx, y + ny, str, resize, scale); + gr_string(x + nx, y + ny, str, resize, scale, len); } gr_reset_screen_scale(); } -void HudGauge::renderStringAlignCenter(int x, int y, int area_width, const char *s, float scale, bool config) +void HudGauge::renderStringAlignCenter(int x, int y, int area_width, const char *s, float scale, bool config) const +{ + renderStringAlignCenter(x, y, area_width, s, std::string::npos, scale, config); +} + +void HudGauge::renderStringAlignCenter(int x, int y, int area_width, const char *s, size_t len, float scale, bool config) const { int w, h; - gr_get_string_size(&w, &h, s, scale); - renderString(x + ((area_width - w) / 2), y, s, scale, config); + gr_get_string_size(&w, &h, s, scale, len); + renderString(x + ((area_width - w) / 2), y, s, len, scale, config); } -void HudGauge::renderPrintf(int x, int y, float scale, bool config, const char* format, ...) +void HudGauge::renderPrintf(int x, int y, float scale, bool config, SCP_FORMAT_STRING const char* format, ...) const { SCP_string tmp; va_list args; @@ -998,7 +1013,7 @@ void HudGauge::renderPrintf(int x, int y, float scale, bool config, const char* renderString(x, y, tmp.c_str(), scale, config); } -void HudGauge::renderPrintfWithGauge(int x, int y, int gauge_id, float scale, bool config, const char* format, ...) +void HudGauge::renderPrintfWithGauge(int x, int y, int gauge_id, float scale, bool config, SCP_FORMAT_STRING const char* format, ...) const { SCP_string tmp; va_list args; diff --git a/code/hud/hud.h b/code/hud/hud.h index ccefec7d3fa..9c8979e89d9 100644 --- a/code/hud/hud.h +++ b/code/hud/hud.h @@ -366,11 +366,14 @@ class HudGauge void renderBitmap(int frame, int x, int y, float scale = 1.0f, bool config = false) const; void renderBitmapColor(int frame, int x, int y, float scale = 1.0f, bool config = false) const; void renderBitmapEx(int frame, int x, int y, int w, int h, int sx, int sy, float scale = 1.0f, bool config = false) const; - void renderString(int x, int y, const char *str, float scale = 1.0f, bool config = false); - void renderString(int x, int y, int gauge_id, const char *str, float scale = 1.0f, bool config = false); - void renderStringAlignCenter(int x, int y, int area_width, const char *s, float scale = 1.0f, bool config = false); - void renderPrintf(int x, int y, float scale, bool config, SCP_FORMAT_STRING const char* format, ...) SCP_FORMAT_STRING_ARGS(6, 7); - void renderPrintfWithGauge(int x, int y, int gauge_id, float scale, bool config, SCP_FORMAT_STRING const char* format, ...) SCP_FORMAT_STRING_ARGS(7, 8); + void renderString(int x, int y, const char *str, float scale = 1.0f, bool config = false) const; + void renderString(int x, int y, const char *str, size_t len, float scale = 1.0f, bool config = false) const; + void renderString(int x, int y, int gauge_id, const char *str, float scale = 1.0f, bool config = false) const; + void renderString(int x, int y, int gauge_id, const char *str, size_t len, float scale = 1.0f, bool config = false) const; + void renderStringAlignCenter(int x, int y, int area_width, const char *s, float scale = 1.0f, bool config = false) const; + void renderStringAlignCenter(int x, int y, int area_width, const char *s, size_t len, float scale = 1.0f, bool config = false) const; + void renderPrintf(int x, int y, float scale, bool config, SCP_FORMAT_STRING const char* format, ...) const SCP_FORMAT_STRING_ARGS(6, 7); + void renderPrintfWithGauge(int x, int y, int gauge_id, float scale, bool config, SCP_FORMAT_STRING const char* format, ...) const SCP_FORMAT_STRING_ARGS(7, 8); void renderLine(int x1, int y1, int x2, int y2, bool config = false) const; void renderGradientLine(int x1, int y1, int x2, int y2, bool config = false) const; void renderRect(int x, int y, int w, int h, bool config = false) const; diff --git a/code/hud/hudmessage.cpp b/code/hud/hudmessage.cpp index 348b0a207d3..4cbaac87c6c 100644 --- a/code/hud/hudmessage.cpp +++ b/code/hud/hudmessage.cpp @@ -331,15 +331,9 @@ void HudGaugeMessages::processMessageBuffer() void HudGaugeMessages::addPending(const char *text, int source, int x) { - Assert(text != NULL); + Assert(text != nullptr); - HUD_message_data new_message; - - new_message.text = text; - new_message.source = source; - new_message.x = x; - - pending_messages.push(new_message); + pending_messages.emplace(text, source, x); } void HudGaugeMessages::scrollMessages() @@ -484,7 +478,7 @@ void HudGaugeMessages::render(float /*frametime*/, bool config) } // Similar to HUD printf, but shows only one message at a time, at a fixed location. -void HUD_fixed_printf(float duration, color col, const char *format, ...) +void HUD_fixed_printf(float duration, color col, SCP_FORMAT_STRING const char *format, ...) { va_list args; @@ -525,8 +519,7 @@ int HUD_source_get_team(int source) return source - HUD_SOURCE_TEAM_OFFSET; } - -void HUD_printf(const char *format, ...) +void HUD_printf(SCP_FORMAT_STRING const char *format, ...) { va_list args; SCP_string tmp; @@ -551,7 +544,7 @@ void HUD_printf(const char *format, ...) // message on the HUD. Text is split into multiple lines if width exceeds msg display area // width. 'source' is used to indicate who send the message, and is used to color code text. // -void HUD_sourced_printf(int source, const char *format, ...) +void HUD_sourced_printf(int source, SCP_FORMAT_STRING const char *format, ...) { va_list args; SCP_string tmp; @@ -579,13 +572,7 @@ void hud_sourced_print(int source, const SCP_string &msg) // add message to the scrollback log first hud_add_msg_to_scrollback(msg.c_str(), source, Missiontime); - HUD_message_data new_msg; - - new_msg.text = msg; - new_msg.source = source; - new_msg.x = 0; - - HUD_msg_buffer.push_back(new_msg); + HUD_msg_buffer.emplace_back(msg, source, 0); // Invoke the scripting hook if (OnHudMessageReceivedHook->isActive()) { @@ -604,13 +591,7 @@ void hud_sourced_print(int source, const char *msg) // add message to the scrollback log first hud_add_msg_to_scrollback(msg, source, Missiontime); - HUD_message_data new_msg; - - new_msg.text = SCP_string(msg); - new_msg.source = source; - new_msg.x = 0; - - HUD_msg_buffer.push_back(new_msg); + HUD_msg_buffer.emplace_back(msg, source, 0); // Invoke the scripting hook if (OnHudMessageReceivedHook->isActive()) { @@ -663,10 +644,7 @@ void hud_add_msg_to_scrollback(const char *text, int source, int t) } // create the new node for the vector - line_node newLine = {t, The_mission.HUD_timer_padding, source, 0, 1, w, ""}; - newLine.text = text; - - Msg_scrollback_vec.push_back(newLine); + Msg_scrollback_vec.emplace_back(t, The_mission.HUD_timer_padding, source, 0, 1, w, text); } // how many lines to skip diff --git a/code/hud/hudmessage.h b/code/hud/hudmessage.h index ebfbef29d46..6ce2cdfb053 100644 --- a/code/hud/hudmessage.h +++ b/code/hud/hudmessage.h @@ -30,13 +30,20 @@ #define HUD_SOURCE_TEAM_OFFSET 8 // must be higher than any previous hud source -typedef struct HUD_message_data { +struct HUD_message_data +{ SCP_string text; int source; // where this message came from so we can color code it int x; -} HUD_message_data; -typedef struct line_node { + HUD_message_data() = default; + HUD_message_data(SCP_string _text, int _source, int _x) + : text(std::move(_text)), source(_source), x(_x) + {} +}; + +struct line_node +{ fix time; // timestamp when message was added int timer_padding; // the mission timer padding, in seconds, at the time the message was added int source; // who/what the source of the message was (for color coding) @@ -44,7 +51,11 @@ typedef struct line_node { int y; int underline_width; SCP_string text; -} line_node; + + line_node(fix _time, int _timer_padding, int _source, int _x, int _y, int _underline_width, SCP_string _text) + : time(_time), timer_padding(_timer_padding), source(_source), x(_x), y(_y), underline_width(_underline_width), text(std::move(_text)) + {} +}; extern SCP_vector Msg_scrollback_vec; diff --git a/code/weapon/emp.cpp b/code/weapon/emp.cpp index 32fbf1fb887..36f406a156f 100644 --- a/code/weapon/emp.cpp +++ b/code/weapon/emp.cpp @@ -78,7 +78,7 @@ char Emp_random_char[NUM_RANDOM_CHARS] = // // maybe reformat a string -void emp_maybe_reformat_text(char *text, int max_len, int gauge_id); +void emp_maybe_reformat_text(char *text, size_t max_len, int gauge_id); // randomize the chars in a string void emp_randomize_chars(char *str); @@ -487,7 +487,7 @@ int emp_should_blit_gauge() } // emp hud string -void emp_hud_string(int x, int y, int gauge_id, const char *str, int resize_mode, float scale) +void emp_hud_string(int x, int y, int gauge_id, const char *str, size_t len, int resize_mode, float scale) { // maybe bail if (!*str) @@ -495,25 +495,28 @@ void emp_hud_string(int x, int y, int gauge_id, const char *str, int resize_mode // if the emp effect is not active, don't even bother messing with the text if(emp_active_local()){ + auto tmp_len = std::min(len, i2sz(255)); + // use a copied string rather than the original char tmp[256] = ""; - strcpy_s(tmp, str); + strncpy(tmp, str, tmp_len); + tmp[tmp_len] = '\0'; - emp_maybe_reformat_text(tmp, 256, gauge_id); + emp_maybe_reformat_text(tmp, tmp_len, gauge_id); // jitter the coords emp_hud_jitter(&x, &y); // print the string out - gr_string(x, y, tmp, resize_mode, scale); + gr_string(x, y, tmp, resize_mode, scale, len); } else { // print the original string out - gr_string(x, y, str, resize_mode, scale); + gr_string(x, y, str, resize_mode, scale, len); } } // maybe reformat a string -void emp_maybe_reformat_text(char *text, int /*max_len*/, int gauge_id) +void emp_maybe_reformat_text(char *text, size_t /*max_len*/, int gauge_id) { wacky_text *wt; diff --git a/code/weapon/emp.h b/code/weapon/emp.h index 71c48d44d0d..6af24faf3fe 100644 --- a/code/weapon/emp.h +++ b/code/weapon/emp.h @@ -103,7 +103,7 @@ void emp_process_local(); int emp_should_blit_gauge(); // emp hud string -void emp_hud_string(int x, int y, int gauge_id, const char *str, int resize_mode, float scale = 1.0f); +void emp_hud_string(int x, int y, int gauge_id, const char *str, size_t len, int resize_mode, float scale = 1.0f); // throw some jitter into HUD x and y coords void emp_hud_jitter(int *x, int *y); From 785bd36986bb57dedd77bc26fe7c3584a4bc167b Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Mon, 2 Mar 2026 21:36:18 -0500 Subject: [PATCH 02/65] make clang-tidy happy --- code/globalincs/memory/utils.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/code/globalincs/memory/utils.h b/code/globalincs/memory/utils.h index 72a66bbe044..967b81f2579 100644 --- a/code/globalincs/memory/utils.h +++ b/code/globalincs/memory/utils.h @@ -1,4 +1,5 @@ -#pragma once +#ifndef MEMORY_UTILS_H +#define MEMORY_UTILS_H #include @@ -22,3 +23,5 @@ inline char *vm_strdup(const char *ptr) return vm_strndup(ptr, len); } + +#endif // MEMORY_UTILS_H From a56bded91946aa5b74e6ac9ee79db33aa0c671d2 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 21 Mar 2026 01:23:01 -0400 Subject: [PATCH 03/65] convert debriefing editor to modeless; add focus_sexp(); fix bugs Convert `debriefing_editor_dlg` from a modal stack-allocated dialog to a modeless heap-allocated dialog, matching the pattern of the briefing editor. Update FRED infrastructure accordingly. Add `focus_sexp(int node)` to `debriefing_editor_dlg`, mirroring the existing method in `briefing_editor_dlg`. Fix both `focus_sexp()` implementations to search all team debriefings/briefings instead of only the currently selected team, so nodes in non-zero teams are found correctly in TVT missions. Fix `debriefing set_modified()` calls to only set the flag if the debriefing data actually changes, rather than every time the editor was opened. --- fred2/bgbitmapdlg.cpp | 2 +- fred2/briefingeditordlg.cpp | 26 +++++++----- fred2/debriefingeditordlg.cpp | 79 +++++++++++++++++++++++------------ fred2/debriefingeditordlg.h | 7 +++- fred2/fred.cpp | 6 ++- fred2/fred.h | 5 ++- fred2/fredview.cpp | 9 +++- fred2/management.cpp | 11 +++-- 8 files changed, 97 insertions(+), 48 deletions(-) diff --git a/fred2/bgbitmapdlg.cpp b/fred2/bgbitmapdlg.cpp index bcf1df20c37..0f64c8627df 100644 --- a/fred2/bgbitmapdlg.cpp +++ b/fred2/bgbitmapdlg.cpp @@ -530,7 +530,7 @@ void bg_bitmap_dlg::OnClose() // close window stuff theApp.record_window_data(&Bg_wnd_data, this); delete Bg_bitmap_dialog; - Bg_bitmap_dialog = NULL; + Bg_bitmap_dialog = nullptr; FREDDoc_ptr->autosave("background editor"); } diff --git a/fred2/briefingeditordlg.cpp b/fred2/briefingeditordlg.cpp index 1f62d1c798f..bbcae42c5ff 100644 --- a/fred2/briefingeditordlg.cpp +++ b/fred2/briefingeditordlg.cpp @@ -253,19 +253,23 @@ void briefing_editor_dlg::create() void briefing_editor_dlg::focus_sexp(int select_sexp_node) { - int i, n; + int i, t, n; n = m_tree.select_sexp_node = select_sexp_node; - if (n != -1) { - for (i=0; inum_stages; i++) - if (query_node_in_sexp(n, Briefing->stages[i].formula)) - break; + if (n == -1) + return; - if (i < Briefing->num_stages) { - m_cur_stage = i; - update_data(); - GetDlgItem(IDC_TREE) -> SetFocus(); - m_tree.hilite_item(m_tree.select_sexp_node); + for (t = 0; t < Num_teams; t++) { + for (i = 0; i < Briefings[t].num_stages; i++) { + if (query_node_in_sexp(n, Briefings[t].stages[i].formula)) { + m_current_briefing = t; + Briefing = &Briefings[t]; + m_cur_stage = i; + update_data(); + GetDlgItem(IDC_TREE)->SetFocus(); + m_tree.hilite_item(m_tree.select_sexp_node); + return; + } } } } @@ -307,7 +311,7 @@ void briefing_editor_dlg::OnClose() theApp.record_window_data(&Briefing_wnd_data, this); ptr = Briefing_dialog; // this juggling prevents a crash in certain situations - Briefing_dialog = NULL; + Briefing_dialog = nullptr; delete ptr; FREDDoc_ptr->autosave("briefing editor"); diff --git a/fred2/debriefingeditordlg.cpp b/fred2/debriefingeditordlg.cpp index 33471699ebd..d9dfea4e6a9 100644 --- a/fred2/debriefingeditordlg.cpp +++ b/fred2/debriefingeditordlg.cpp @@ -53,6 +53,35 @@ debriefing_editor_dlg::debriefing_editor_dlg(CWnd* pParent /*=NULL*/) select_sexp_node = -1; } +void debriefing_editor_dlg::create() +{ + CDialog::Create(IDD); + theApp.init_window(&Debriefing_wnd_data, this); +} + +void debriefing_editor_dlg::focus_sexp(int node) +{ + int i, t, n; + + n = m_tree.select_sexp_node = node; + if (n == -1) + return; + + for (t = 0; t < Num_teams; t++) { + for (i = 0; i < Debriefings[t].num_stages; i++) { + if (query_node_in_sexp(n, Debriefings[t].stages[i].formula)) { + m_current_debriefing = t; + Debriefing = &Debriefings[t]; + m_cur_stage = i; + update_data(); + GetDlgItem(IDC_TREE)->SetFocus(); + m_tree.hilite_item(m_tree.select_sexp_node); + return; + } + } + } +} + void debriefing_editor_dlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); @@ -118,8 +147,6 @@ void debriefing_editor_dlg::OnInitMenu(CMenu* pMenu) BOOL debriefing_editor_dlg::OnInitDialog() { - int i, n; - CDialog::OnInitDialog(); m_play_bm.LoadBitmap(IDB_PLAY); ((CButton *) GetDlgItem(IDC_PLAY)) -> SetBitmap(m_play_bm); @@ -151,28 +178,15 @@ BOOL debriefing_editor_dlg::OnInitDialog() m_debriefFail_music = Mission_music[SCORE_DEBRIEFING_FAILURE] + 1; m_tree.link_modified(&modified); // provide way to indicate trees are modified in dialog - n = m_tree.select_sexp_node = select_sexp_node; - select_sexp_node = -1; - if (n != -1) { - for (i=0; inum_stages; i++) - if (query_node_in_sexp(n, Debriefing->stages[i].formula)) - break; - - if (i < Debriefing->num_stages) { - m_cur_stage = i; - update_data(); - GetDlgItem(IDC_TREE) -> SetFocus(); - m_tree.hilite_item(m_tree.select_sexp_node); - set_modified(); - return FALSE; - } - } CDialog::OnInitDialog(); update_data(); - set_modified(); - // hard coded stuff to deal with the multiple briefings per mission. + if (select_sexp_node != -1) { + focus_sexp(select_sexp_node); + select_sexp_node = -1; + return FALSE; + } return TRUE; } @@ -201,10 +215,20 @@ void debriefing_editor_dlg::update_data(int update) free_sexp2(ptr->formula); ptr->formula = m_tree.save_tree(); - deconvert_multiline_string(ptr->text, m_text); - lcl_fred_replace_stuff(ptr->text); - deconvert_multiline_string(ptr->recommendation_text, m_rec_text); - lcl_fred_replace_stuff(ptr->recommendation_text); + + SCP_string new_text, new_rec_text; + deconvert_multiline_string(new_text, m_text); + lcl_fred_replace_stuff(new_text); + deconvert_multiline_string(new_rec_text, m_rec_text); + lcl_fred_replace_stuff(new_rec_text); + + if (modified || ptr->text != new_text || ptr->recommendation_text != new_rec_text + || stricmp(ptr->voice, m_voice) != 0) + set_modified(); + + modified = 0; + ptr->text = new_text; + ptr->recommendation_text = new_rec_text; string_copy(ptr->voice, m_voice, MAX_FILENAME_LEN - 1); } @@ -425,7 +449,7 @@ void debriefing_editor_dlg::OnEndlabeleditTree(NMHDR* pNMHDR, LRESULT* pResult) *pResult = m_tree.end_label_edit(pTVDispInfo->item); } -void debriefing_editor_dlg::OnClose() +void debriefing_editor_dlg::OnClose() { audiostream_close_file(m_voice_id, 0); m_voice_id = -1; @@ -436,7 +460,10 @@ void debriefing_editor_dlg::OnClose() Mission_music[SCORE_DEBRIEFING_AVERAGE] = m_debriefAvg_music - 1; Mission_music[SCORE_DEBRIEFING_FAILURE] = m_debriefFail_music - 1; - CDialog::OnClose(); + theApp.record_window_data(&Debriefing_wnd_data, this); + debriefing_editor_dlg *ptr = Debriefing_dialog; + Debriefing_dialog = nullptr; + delete ptr; FREDDoc_ptr->autosave("debriefing editor"); } diff --git a/fred2/debriefingeditordlg.h b/fred2/debriefingeditordlg.h index 020ec382915..949d580bacf 100644 --- a/fred2/debriefingeditordlg.h +++ b/fred2/debriefingeditordlg.h @@ -1,12 +1,13 @@ /* * Copyright (C) Volition, Inc. 1999. All rights reserved. * - * All source code herein is the property of Volition, Inc. You may not sell - * or otherwise commercially exploit the source or things you created based on the + * All source code herein is the property of Volition, Inc. You may not sell + * or otherwise commercially exploit the source or things you created based on the * source. * */ +#pragma once #include "mission/missionbriefcommon.h" @@ -18,6 +19,8 @@ class debriefing_editor_dlg : public CDialog // Construction public: void OnOK(); + void create(); + void focus_sexp(int node); void update_data(int update = 1); debriefing_editor_dlg(CWnd* pParent = NULL); // standard constructor int select_sexp_node; diff --git a/fred2/fred.cpp b/fred2/fred.cpp index 9b2d906928b..e176125c30f 100644 --- a/fred2/fred.cpp +++ b/fred2/fred.cpp @@ -121,8 +121,9 @@ wing_editor Wing_editor_dialog; waypoint_path_dlg Waypoint_editor_dialog; jumpnode_dlg Jumpnode_editor_dialog; music_player_dlg Music_player_dialog; -bg_bitmap_dlg* Bg_bitmap_dialog = NULL; -briefing_editor_dlg* Briefing_dialog = NULL; +bg_bitmap_dlg* Bg_bitmap_dialog = nullptr; +briefing_editor_dlg* Briefing_dialog = nullptr; +debriefing_editor_dlg* Debriefing_dialog = nullptr; window_data Main_wnd_data; window_data Ship_wnd_data; @@ -136,6 +137,7 @@ window_data Player_wnd_data; window_data Events_wnd_data; window_data Bg_wnd_data; window_data Briefing_wnd_data; +window_data Debriefing_wnd_data; window_data Reinforcement_wnd_data; window_data Waypoint_wnd_data; window_data Jumpnode_wnd_data; diff --git a/fred2/fred.h b/fred2/fred.h index 99e240c4932..d7e90184f09 100644 --- a/fred2/fred.h +++ b/fred2/fred.h @@ -17,6 +17,7 @@ #include "BgBitmapDlg.h" #include "BriefingEditorDlg.h" +#include "DebriefingEditorDlg.h" #include "resource.h" #include "ShipEditorDlg.h" #include "propdlg.h" @@ -187,7 +188,8 @@ extern waypoint_path_dlg Waypoint_editor_dialog; //!< The waypoint editor ins extern jumpnode_dlg Jumpnode_editor_dialog; //!< The jumpnode editor instance extern music_player_dlg Music_player_dialog; //!< The music player instance extern bg_bitmap_dlg* Bg_bitmap_dialog; //!< The bitmap dialog instance -extern briefing_editor_dlg* Briefing_dialog; //!< The briefing editor instance +extern briefing_editor_dlg* Briefing_dialog; //!< The briefing editor instance +extern debriefing_editor_dlg* Debriefing_dialog; //!< The debriefing editor instance extern CFREDApp theApp; //!< The application instance @@ -203,6 +205,7 @@ extern window_data Player_wnd_data; extern window_data Events_wnd_data; extern window_data Bg_wnd_data; extern window_data Briefing_wnd_data; +extern window_data Debriefing_wnd_data; extern window_data Reinforcement_wnd_data; extern window_data Waypoint_wnd_data; extern window_data Jumpnode_wnd_data; diff --git a/fred2/fredview.cpp b/fred2/fredview.cpp index 352a98e86e5..77495c11db5 100644 --- a/fred2/fredview.cpp +++ b/fred2/fredview.cpp @@ -4213,9 +4213,14 @@ void CFREDView::OnEditorsBriefing() void CFREDView::OnEditorsDebriefing() { - debriefing_editor_dlg dlg; + if (!Debriefing_dialog) { + Debriefing_dialog = new debriefing_editor_dlg; + Debriefing_dialog->create(); + } - dlg.DoModal(); + Debriefing_dialog->SetWindowPos(&wndTop, 0, 0, 0, 0, + SWP_SHOWWINDOW | SWP_NOMOVE | SWP_NOSIZE); + Debriefing_dialog->ShowWindow(SW_RESTORE); } void CFREDView::OnSaveCamera() diff --git a/fred2/management.cpp b/fred2/management.cpp index 1d8e7179cd2..befffd80a44 100644 --- a/fred2/management.cpp +++ b/fred2/management.cpp @@ -2283,10 +2283,15 @@ int sexp_reference_handler(int node, sexp_src source, int source_index, char *ms } case sexp_src::DEBRIEFING: { - debriefing_editor_dlg dlg; + if (!Debriefing_dialog) { + Debriefing_dialog = new debriefing_editor_dlg; + Debriefing_dialog->create(); + } - dlg.select_sexp_node = node; - dlg.DoModal(); + Debriefing_dialog->SetWindowPos(&CWnd::wndTop, 0, 0, 0, 0, + SWP_SHOWWINDOW | SWP_NOMOVE | SWP_NOSIZE); + Debriefing_dialog->ShowWindow(SW_RESTORE); + Debriefing_dialog->focus_sexp(node); break; } From 56341002c8e91a7c42551e30770ba28ab67b2143 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 8 Apr 2026 20:20:20 -0400 Subject: [PATCH 04/65] improve SEXP syntax checking Clean up the syntax checker, using clearer variable names for each check, and most importantly unique variable names. Fixes several bugs where syntax checking failed because the same variable was overwritten with incompatible type information. Also fixes a bug in `check_operator_argument_count` where the count check never ran due to a mixup of `op_index` and `op_const`. --- code/parse/sexp.cpp | 423 ++++++++++++++++++++++---------------------- 1 file changed, 214 insertions(+), 209 deletions(-) diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 8ca2ecddce2..457d1db9a94 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -1986,24 +1986,28 @@ int query_sexp_args_count(int node, bool only_valid_args = false) /** * Needed to fix bug with sexps like send-message list which have arguments that need to be supplied as a block * - * @return 0 if the number of arguments for the supplied operation is wrong, 1 otherwise. + * @return whether the number of arguments for the supplied operation is correct */ -int check_operator_argument_count(int count, int op) +static bool check_operator_argument_count(int count, int op_index) { - if (count < Operators[op].min || count > Operators[op].max) - return 0; + Assertion(op_index >= 0 && op_index < sz2i(Operators.size()), "op_index is out of range!"); + + if (count < Operators[op_index].min || count > Operators[op_index].max) + return false; + + int op_const = Operators[op_index].value; // send-message-list has arguments as blocks of 4 // same with send-message-chain, but there's an extra argument - if (op == OP_SEND_MESSAGE_CHAIN) + if (op_const == OP_SEND_MESSAGE_CHAIN) count--; - if (op == OP_SEND_MESSAGE_LIST || op == OP_SEND_MESSAGE_CHAIN) + if (op_const == OP_SEND_MESSAGE_LIST || op_const == OP_SEND_MESSAGE_CHAIN) if (count % 4 != 0) - return 0; + return false; - return 1; + return true; } // helper functions for check_container_value_data_type() @@ -2141,23 +2145,23 @@ bool is_special_sender(const char* name) { * @return 0 if ok, negative if there's an error in expression.. * See the returns types in sexp.h */ -int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, sexp_mode mode) +int check_sexp_syntax(int node, int desired_return_type, int recursive, int *bad_node, sexp_mode mode) { - int i = 0, z, type, argnum = 0, op, type2 = 0, op2; + int i = 0, z, argnum = 0, desired_argument_type = OPF_NONE, node_subtype = -1, node_return_type = OPR_NONE; size_t count; int op_node; int var_index = -1; size_t st; const sexp_container *p_container = nullptr; // for SEXPs that take container name as arg - Assert(node >= 0 && node < Num_sexp_nodes); - Assert(Sexp_nodes[node].type != SEXP_NOT_USED); + Assertion(node >= 0 && node < Num_sexp_nodes, "Node %d must be a valid SEXP node!", node); + Assertion(Sexp_nodes[node].type != SEXP_NOT_USED, "Node %d must be in use!", node); op_node = node; // save the node of the operator since we need to get to other args. if (bad_node) *bad_node = op_node; - if (Sexp_nodes[node].subtype == SEXP_ATOM_NUMBER && return_type == OPR_BOOL) { + if (Sexp_nodes[node].subtype == SEXP_ATOM_NUMBER && desired_return_type == OPR_BOOL) { // special case Mark seems to want supported Assert(Sexp_nodes[node].first == -1); // only lists should have a first pointer if (Sexp_nodes[node].rest != -1) // anything after the number? @@ -2169,25 +2173,26 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s if (Sexp_nodes[op_node].subtype != SEXP_ATOM_OPERATOR) return SEXP_CHECK_OP_EXPECTED; // not an operator, which it should always be - op = get_operator_index(op_node); - if (op == -1) + int op_index = get_operator_index(op_node); + int op_const = SCP_vector_inbounds(Operators, op_index) ? Operators[op_index].value : OP_NOT_AN_OP; + if (op_const == OP_NOT_AN_OP) return SEXP_CHECK_UNKNOWN_OP; // unrecognized operator // check that types match - except that OPR_AMBIGUOUS matches everything - if (return_type != OPR_AMBIGUOUS) + if (desired_return_type != OPR_AMBIGUOUS) { // get the return type of the next thing - z = query_operator_return_type(op); - if (z == OPR_POSITIVE && return_type == OPR_NUMBER) + z = query_operator_return_type(op_const); + if (z == OPR_POSITIVE && desired_return_type == OPR_NUMBER) { // positive data type can map to number data type just fine } // Goober5000's number hack - else if (z == OPR_NUMBER && return_type == OPR_POSITIVE) + else if (z == OPR_NUMBER && desired_return_type == OPR_POSITIVE) { // this isn't kosher, but we hack it to make it work } - else if (z != return_type) + else if (z != desired_return_type) { // anything else is a mismatch return SEXP_CHECK_TYPE_MISMATCH; @@ -2196,17 +2201,18 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s count = query_sexp_args_count(op_node); - if (!check_operator_argument_count(sz2i(count), op)) + if (!check_operator_argument_count(sz2i(count), op_index)) return SEXP_CHECK_BAD_ARG_COUNT; // incorrect number of arguments node = Sexp_nodes[op_node].rest; while (node != -1) { - type = query_operator_argument_type(op, argnum); - Assert(Sexp_nodes[node].type != SEXP_NOT_USED); + desired_argument_type = query_operator_argument_type(op_index, argnum); + Assertion(Sexp_nodes[node].type != SEXP_NOT_USED, "Node %d must be in use!", node); if (bad_node) *bad_node = node; + node_subtype = Sexp_nodes[node].subtype; - if (Sexp_nodes[node].subtype == SEXP_ATOM_LIST) { + if (node_subtype == SEXP_ATOM_LIST) { i = Sexp_nodes[node].first; if (bad_node) *bad_node = i; @@ -2217,14 +2223,15 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s if (Sexp_nodes[i].subtype == SEXP_ATOM_LIST) return 0; - op2 = get_operator_index(i); - if (op2 == -1) + int op2_index = get_operator_index(i); + int op2_const = SCP_vector_inbounds(Operators, op2_index) ? Operators[op2_index].value : OP_NOT_AN_OP; + if (op2_const == OP_NOT_AN_OP) return SEXP_CHECK_UNKNOWN_OP; - type2 = query_operator_return_type(op2); + node_return_type = query_operator_return_type(op2_const); if (recursive) { sexp_opr_t opr; - if (!map_opf_to_opr((sexp_opf_t)type, opr)) { + if (!map_opf_to_opr((sexp_opf_t)desired_argument_type, opr)) { return SEXP_CHECK_UNKNOWN_TYPE; } @@ -2233,18 +2240,18 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } } - } else if (Sexp_nodes[node].subtype == SEXP_ATOM_NUMBER) { - type2 = OPR_POSITIVE; + } else if (node_subtype == SEXP_ATOM_NUMBER) { + node_return_type = OPR_POSITIVE; auto ptr = CTEXT(node); if (*ptr == '-') { - type2 = OPR_NUMBER; + node_return_type = OPR_NUMBER; ptr++; } else if (*ptr == '+') { ptr++; } - if (type == OPF_BOOL) // allow numbers to be used where boolean is required. - type2 = OPR_BOOL; + if (desired_argument_type == OPF_BOOL) // allow numbers to be used where boolean is required. + node_return_type = OPR_BOOL; // Only check that this is a number if it's not . if (!(Sexp_nodes[node].flags & SNF_SPECIAL_ARG_IN_NODE)) { @@ -2258,13 +2265,12 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } i = atoi(CTEXT(node)); - z = get_operator_const(op_node); - if ( (z == OP_HAS_DOCKED_DELAY) || (z == OP_HAS_UNDOCKED_DELAY) ) + if ( (op_const == OP_HAS_DOCKED_DELAY) || (op_const == OP_HAS_UNDOCKED_DELAY) ) if ( (argnum == 2) && (i < 1) ) return SEXP_CHECK_NUM_RANGE_INVALID; // valid color range 0 to 255 - FUBAR - if ((z == OP_CHANGE_IFF_COLOR) && ((argnum >= 2) && (argnum <= 4))) + if ((op_const == OP_CHANGE_IFF_COLOR) && ((argnum >= 2) && (argnum <= 4))) { if ( i < 0 || i > 255) { @@ -2272,23 +2278,22 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } } - z = get_operator_index(op_node); - if ( (query_operator_return_type(z) == OPR_AI_GOAL) && (argnum == Operators[op].min - 1) ) + if ( (query_operator_return_type(op_const) == OPR_AI_GOAL) && (argnum == Operators[op_index].min - 1) ) if ( (i < 0) || (i > 200) ) return SEXP_CHECK_NUM_RANGE_INVALID; } - } else if (Sexp_nodes[node].subtype == SEXP_ATOM_STRING) { - type2 = SEXP_ATOM_STRING; + } else if (node_subtype == SEXP_ATOM_STRING) { + ; // no special handling - } else if (Sexp_nodes[node].subtype == SEXP_ATOM_CONTAINER_NAME) { - type2 = SEXP_ATOM_CONTAINER_NAME; + } else if (node_subtype == SEXP_ATOM_CONTAINER_NAME) { + ; // no special handling - } else if (Sexp_nodes[node].subtype == SEXP_ATOM_CONTAINER_DATA) { + } else if (node_subtype == SEXP_ATOM_CONTAINER_DATA) { // this is an instance of "Replace Container Data" // can't be used in special argument list - if (is_argument_provider_op(get_operator_const(op_node))) { + if (is_argument_provider_op(op_const)) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -2300,16 +2305,16 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s const auto *p_data_container = get_sexp_container(Sexp_nodes[node].text); // name should have already been checked in get_sexp() Assertion(p_data_container, - "Attempt to check type of container data for SEXP operator %d at arg %d for non-existent container %s. " + "Attempt to check type of container data for SEXP operator %s at arg %d for non-existent container %s. " "Please report!", - op, + Operators[op_index].text.c_str(), argnum, Sexp_nodes[node].text); const auto &data_container = *p_data_container; - if (!check_container_data_type(type, + if (!check_container_data_type(desired_argument_type, data_container.type, - get_operator_const(op_node), + op_const, argnum, p_container)) { return SEXP_CHECK_WRONG_CONTAINER_DATA_TYPE; @@ -2368,18 +2373,18 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s UNREACHABLE("SEXP subtype is %d when it should be SEXP_ATOM_LIST, SEXP_ATOM_NUMBER, SEXP_ATOM_STRING, " "SEXP_ATOM_CONTAINER_NAME, or " "SEXP_ATOM_CONTAINER_DATA!", - Sexp_nodes[node].subtype); + node_subtype); } // variables should only be typechecked. - if ((Sexp_nodes[node].type & SEXP_FLAG_VARIABLE) && (type != OPF_VARIABLE_NAME)) { + if ((Sexp_nodes[node].type & SEXP_FLAG_VARIABLE) && (desired_argument_type != OPF_VARIABLE_NAME)) { var_index = sexp_get_variable_index(node); if (var_index < 0) return SEXP_CHECK_INVALID_VARIABLE; - if (!check_variable_data_type(type, + if (!check_variable_data_type(desired_argument_type, Sexp_variables[var_index].type, - get_operator_const(op_node), + op_const, argnum, p_container)) { return SEXP_CHECK_INVALID_VARIABLE_TYPE; @@ -2405,24 +2410,24 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } } - switch (type) { + switch (desired_argument_type) { case OPF_NAV_POINT: - if (type2 != SEXP_ATOM_STRING){ + if (node_subtype != SEXP_ATOM_STRING){ return SEXP_CHECK_TYPE_MISMATCH; } break; case OPF_NUMBER: - if ((type2 != OPR_NUMBER) && (type2 != OPR_POSITIVE)){ + if ((node_return_type != OPR_NUMBER) && (node_return_type != OPR_POSITIVE)){ return SEXP_CHECK_TYPE_MISMATCH; } break; case OPF_POSITIVE: - if (type2 == OPR_NUMBER){ + if (node_return_type == OPR_NUMBER){ // for numeric literals, check whether the number is negative - if (Sexp_nodes[node].subtype == SEXP_ATOM_NUMBER){ + if (node_subtype == SEXP_ATOM_NUMBER){ if (*Sexp_nodes[node].text == '-') return SEXP_CHECK_NEGATIVE_NUM; } @@ -2432,14 +2437,14 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s // return SEXP_CHECK_NEGATIVE_NUM; } - if (type2 != OPR_POSITIVE){ + if (node_return_type != OPR_POSITIVE){ return SEXP_CHECK_TYPE_MISMATCH; } break; case OPF_SHIP_NOT_PLAYER: - if (type2 != SEXP_ATOM_STRING){ + if (node_subtype != SEXP_ATOM_STRING){ return SEXP_CHECK_TYPE_MISMATCH; } @@ -2454,7 +2459,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_SHIP_OR_NONE: - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -2474,14 +2479,14 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s case OPF_SHIP: case OPF_SHIP_POINT: - if (type2 != SEXP_ATOM_STRING){ + if (node_subtype != SEXP_ATOM_STRING){ return SEXP_CHECK_TYPE_MISMATCH; } if (ship_name_lookup(CTEXT(node), 1) < 0) { if (Fred_running || !mission_check_ship_yet_to_arrive(CTEXT(node))) { - if (type == OPF_SHIP) + if (desired_argument_type == OPF_SHIP) { // return invalid ship if not also looking for point return SEXP_CHECK_INVALID_SHIP; } @@ -2499,7 +2504,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_PROP: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } if (prop_name_lookup(CTEXT(node)) < 0) { @@ -2508,7 +2513,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_WING: - if (type2 != SEXP_ATOM_STRING){ + if (node_subtype != SEXP_ATOM_STRING){ return SEXP_CHECK_TYPE_MISMATCH; } @@ -2524,11 +2529,11 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s case OPF_SHIP_WING_POINT: case OPF_SHIP_WING_POINT_OR_NONE: case OPF_ORDER_RECIPIENT: - if ( type2 != SEXP_ATOM_STRING ){ + if ( node_subtype != SEXP_ATOM_STRING ){ return SEXP_CHECK_TYPE_MISMATCH; } - if (type == OPF_ORDER_RECIPIENT) { + if (desired_argument_type == OPF_ORDER_RECIPIENT) { if (!strcmp ("", CTEXT(node))) { break; } @@ -2544,38 +2549,38 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } // none is okay for _OR_NONE - if (type == OPF_SHIP_WING_POINT_OR_NONE && !stricmp(CTEXT(node), SEXP_NONE_STRING)) { + if (desired_argument_type == OPF_SHIP_WING_POINT_OR_NONE && !stricmp(CTEXT(node), SEXP_NONE_STRING)) { break; } // two different ways of checking teams - if ((type == OPF_SHIP_WING_WHOLETEAM) && iff_lookup(CTEXT(node)) >= 0) { + if ((desired_argument_type == OPF_SHIP_WING_WHOLETEAM) && iff_lookup(CTEXT(node)) >= 0) { break; } - if ((type == OPF_SHIP_WING_SHIPONTEAM_POINT) && sexp_determine_team(CTEXT(node)) >= 0) { + if ((desired_argument_type == OPF_SHIP_WING_SHIPONTEAM_POINT) && sexp_determine_team(CTEXT(node)) >= 0) { break; } // only other possibility is waypoints - if (type == OPF_SHIP_WING_SHIPONTEAM_POINT || type == OPF_SHIP_WING_POINT || type == OPF_SHIP_WING_POINT_OR_NONE) { + if (desired_argument_type == OPF_SHIP_WING_SHIPONTEAM_POINT || desired_argument_type == OPF_SHIP_WING_POINT || desired_argument_type == OPF_SHIP_WING_POINT_OR_NONE) { if (find_matching_waypoint(CTEXT(node)) == nullptr) { if (verify_vector(CTEXT(node))) { // non-zero on verify vector mean invalid! - return (type == OPF_SHIP_WING_SHIPONTEAM_POINT) ? SEXP_CHECK_INVALID_SHIP_WING_SHIPONTEAM_POINT : SEXP_CHECK_INVALID_SHIP_WING_POINT; + return (desired_argument_type == OPF_SHIP_WING_SHIPONTEAM_POINT) ? SEXP_CHECK_INVALID_SHIP_WING_SHIPONTEAM_POINT : SEXP_CHECK_INVALID_SHIP_WING_POINT; } } break; } // nothing left - if (type == OPF_ORDER_RECIPIENT) + if (desired_argument_type == OPF_ORDER_RECIPIENT) return SEXP_CHECK_INVALID_ORDER_RECIPIENT; - else if (type == OPF_SHIP_WING_WHOLETEAM) + else if (desired_argument_type == OPF_SHIP_WING_WHOLETEAM) return SEXP_CHECK_INVALID_SHIP_WING_WHOLETEAM; else return SEXP_CHECK_INVALID_SHIP_WING; case OPF_SHIP_PROP: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } if (ship_name_lookup(CTEXT(node), 1) >= 0) { @@ -2602,18 +2607,18 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s int shipnum,ship_class; int ship_node; - if (type2 != SEXP_ATOM_STRING){ + if (node_subtype != SEXP_ATOM_STRING){ return SEXP_CHECK_TYPE_MISMATCH; } // none is okay for subsys_or_none - if (type == OPF_SUBSYSTEM_OR_NONE && !stricmp(CTEXT(node), SEXP_NONE_STRING)) + if (desired_argument_type == OPF_SUBSYSTEM_OR_NONE && !stricmp(CTEXT(node), SEXP_NONE_STRING)) { break; } // subsys_or_generic can also accept generic types - if (type == OPF_SUBSYS_OR_GENERIC && get_generic_subsys(CTEXT(node)) != SUBSYSTEM_NONE) { + if (desired_argument_type == OPF_SUBSYS_OR_GENERIC && get_generic_subsys(CTEXT(node)) != SUBSYSTEM_NONE) { break; } @@ -2621,7 +2626,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s // with that name. This code assumes by default that the ship is *always* the first name // in the sexpression. If this is ever not the case, the code here must be changed to // get the correct ship name. - switch(get_operator_const(op_node)) + switch(op_const) { case OP_CAP_SUBSYS_CARGO_KNOWN_DELAY: case OP_DISTANCE_CENTER_SUBSYSTEM: @@ -2659,7 +2664,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; default: - if (get_operator_const(op_node) < First_available_operator_id) { + if (op_const < First_available_operator_id) { ship_node = CDR(op_node); } else { int r_count = get_dynamic_parameter_index(Sexp_nodes[op_node].text, argnum); @@ -2705,7 +2710,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s if (!p_objp) { - if (type == OPF_SUBSYSTEM_OR_NONE) + if (desired_argument_type == OPF_SUBSYSTEM_OR_NONE) break; else { @@ -2720,13 +2725,13 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } // check for the special "hull" value - if ( (Operators[op].value == OP_SABOTAGE_SUBSYSTEM) || (Operators[op].value == OP_REPAIR_SUBSYSTEM) || (Operators[op].value == OP_SET_SUBSYSTEM_STRNGTH) || (Operators[op].value == OP_SET_ARMOR_TYPE) || (Operators[op].value == OP_BEAM_FIRE)) { + if ( (op_const == OP_SABOTAGE_SUBSYSTEM) || (op_const == OP_REPAIR_SUBSYSTEM) || (op_const == OP_SET_SUBSYSTEM_STRNGTH) || (op_const == OP_SET_ARMOR_TYPE) || (op_const == OP_BEAM_FIRE)) { if ( !stricmp( CTEXT(node), SEXP_HULL_STRING) || !stricmp( CTEXT(node), SEXP_SIM_HULL_STRING) ){ break; } } // check for special "shields" value for armor types - if (Operators[op].value == OP_SET_ARMOR_TYPE) { + if (op_const == OP_SET_ARMOR_TYPE) { if ( !stricmp( CTEXT(node), SEXP_SHIELD_STRING) || !stricmp( CTEXT(node), SEXP_SIM_HULL_STRING) ){ break; } @@ -2748,19 +2753,19 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s if(Fred_running) { // if we're checking for an AWACS subsystem and this is not an awacs subsystem - if((type == OPF_AWACS_SUBSYSTEM) && !(Ship_info[ship_class].subsystems[i].flags[Model::Subsystem_Flags::Awacs])) + if((desired_argument_type == OPF_AWACS_SUBSYSTEM) && !(Ship_info[ship_class].subsystems[i].flags[Model::Subsystem_Flags::Awacs])) { return SEXP_CHECK_INVALID_AWACS_SUBSYS; } // rotating subsystem, like above - Goober5000 - if ((type == OPF_ROTATING_SUBSYSTEM) && !(Ship_info[ship_class].subsystems[i].flags[Model::Subsystem_Flags::Rotates])) + if ((desired_argument_type == OPF_ROTATING_SUBSYSTEM) && !(Ship_info[ship_class].subsystems[i].flags[Model::Subsystem_Flags::Rotates])) { return SEXP_CHECK_INVALID_ROTATING_SUBSYS; } // translating subsystem, like above - Goober5000 - if ((type == OPF_TRANSLATING_SUBSYSTEM) && !(Ship_info[ship_class].subsystems[i].flags[Model::Subsystem_Flags::Translates])) + if ((desired_argument_type == OPF_TRANSLATING_SUBSYSTEM) && !(Ship_info[ship_class].subsystems[i].flags[Model::Subsystem_Flags::Translates])) { return SEXP_CHECK_INVALID_TRANSLATING_SUBSYS; } @@ -2775,7 +2780,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s int shipnum,ship_class; int ship_node; - if (type2 != SEXP_ATOM_STRING){ + if (node_subtype != SEXP_ATOM_STRING){ return SEXP_CHECK_TYPE_MISMATCH; } @@ -2805,7 +2810,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s if (!p_objp) { - if (type == OPF_SUBSYSTEM_OR_NONE) + if (desired_argument_type == OPF_SUBSYSTEM_OR_NONE) break; else { @@ -2820,7 +2825,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } const auto& animSet = Ship_info[ship_class].animations; - switch(get_operator_const(op_node)) { + switch(op_const) { case OP_TRIGGER_ANIMATION_NEW: case OP_STOP_LOOPING_ANIMATION: { //Second OP trigger type @@ -2881,7 +2886,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_POINT: - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -2897,7 +2902,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_IFF: - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -2910,7 +2915,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_AI_CLASS: - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -2931,7 +2936,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_ARRIVAL_LOCATION: - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -2952,7 +2957,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_DEPARTURE_LOCATION: - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -2973,7 +2978,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_ARRIVAL_ANCHOR_ALL: - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -3006,7 +3011,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_SOUNDTRACK_NAME: - if (type2 != SEXP_ATOM_STRING){ + if (node_subtype != SEXP_ATOM_STRING){ return SEXP_CHECK_TYPE_MISMATCH; } @@ -3026,7 +3031,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s auto name = CTEXT(node); int shipnum = -1; - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) return SEXP_CHECK_TYPE_MISMATCH; if (!stricmp(name, "")) @@ -3056,7 +3061,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } case OPF_SUPPORT_SHIP_CLASS: - if (type2 != SEXP_ATOM_STRING){ + if (node_subtype != SEXP_ATOM_STRING){ return SEXP_CHECK_TYPE_MISMATCH; } @@ -3084,28 +3089,28 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_BOOL: - if (type2 != OPR_BOOL){ + if (node_return_type != OPR_BOOL){ return SEXP_CHECK_TYPE_MISMATCH; } break; case OPF_AI_ORDER: - if ( type2 != SEXP_ATOM_STRING ){ + if ( node_subtype != SEXP_ATOM_STRING ){ return SEXP_CHECK_TYPE_MISMATCH; } break; case OPF_NULL: - if (type2 != OPR_NULL){ + if (node_return_type != OPR_NULL){ return SEXP_CHECK_TYPE_MISMATCH; } break; case OPF_SSM_CLASS: - if ( type2 != SEXP_ATOM_STRING ) { + if ( node_subtype != SEXP_ATOM_STRING ) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -3117,21 +3122,21 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s // Goober5000 case OPF_FLEXIBLE_ARGUMENT: - if (type2 != OPR_FLEXIBLE_ARGUMENT) { + if (node_return_type != OPR_FLEXIBLE_ARGUMENT) { return SEXP_CHECK_TYPE_MISMATCH; } break; // Goober5000 case OPF_ANYTHING: - if (type2 == SEXP_ATOM_CONTAINER_NAME) { + if (node_subtype == SEXP_ATOM_CONTAINER_NAME) { return SEXP_CHECK_TYPE_MISMATCH; } break; case OPF_AI_GOAL: { - if (type2 != OPR_AI_GOAL){ + if (node_return_type != OPR_AI_GOAL){ return SEXP_CHECK_TYPE_MISMATCH; } @@ -3145,7 +3150,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s int ship_num, ship2, wing_num = 0; // if it's the "goals" operator, this is part of initial orders, so we can't grab the ship from it - if (get_operator_const(op_node) == OP_GOALS_ID) { + if (op_const == OP_GOALS_ID) { break; } @@ -3175,7 +3180,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } } - Assert(Sexp_nodes[node].subtype == SEXP_ATOM_LIST); + Assert(node_subtype == SEXP_ATOM_LIST); z = Sexp_nodes[node].first; Assert(Sexp_nodes[z].subtype != SEXP_ATOM_LIST); z = get_operator_const(z); @@ -3210,7 +3215,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } case OPF_SHIP_TYPE: { - if (type2 != SEXP_ATOM_STRING){ + if (node_subtype != SEXP_ATOM_STRING){ return SEXP_CHECK_TYPE_MISMATCH; } @@ -3236,7 +3241,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_MESSAGE: - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) return SEXP_CHECK_TYPE_MISMATCH; if (Fred_running) { @@ -3251,7 +3256,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_PRIORITY: { - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) return SEXP_CHECK_TYPE_MISMATCH; if (Fred_running) { // should still check in Fred though.. @@ -3266,7 +3271,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } case OPF_MISSION_NAME: - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) return SEXP_CHECK_TYPE_MISMATCH; if (Fred_running) { @@ -3285,8 +3290,8 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } else { // mwa -- put the following if statement to prevent Fred errors for possibly valid // conditions. We should do something else here!!! - if ( (Operators[op].value == OP_PREVIOUS_EVENT_TRUE) || (Operators[op].value == OP_PREVIOUS_EVENT_FALSE) || (Operators[op].value == OP_PREVIOUS_EVENT_INCOMPLETE) - || (Operators[op].value == OP_PREVIOUS_GOAL_TRUE) || (Operators[op].value == OP_PREVIOUS_GOAL_FALSE) || (Operators[op].value == OP_PREVIOUS_GOAL_INCOMPLETE) ) + if ( (op_const == OP_PREVIOUS_EVENT_TRUE) || (op_const == OP_PREVIOUS_EVENT_FALSE) || (op_const == OP_PREVIOUS_EVENT_INCOMPLETE) + || (op_const == OP_PREVIOUS_GOAL_TRUE) || (op_const == OP_PREVIOUS_GOAL_FALSE) || (op_const == OP_PREVIOUS_GOAL_INCOMPLETE) ) break; if (!(*Mission_filename) || stricmp(Mission_filename, CTEXT(node)) != 0) @@ -3299,7 +3304,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s case OPF_GOAL_NAME: case OPF_EVENT_NAME: { - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) return SEXP_CHECK_TYPE_MISMATCH; count = 0; @@ -3341,40 +3346,40 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s Campaign.missions[i].flags &= ~CMISSION_FLAG_FRED_LOAD_PENDING; } - if (type == OPF_GOAL_NAME) { + if (desired_argument_type == OPF_GOAL_NAME) { count = count_items_with_string(Campaign.missions[i].goals, &mgoal::name, CTEXT(node)); - } else if (type == OPF_EVENT_NAME) { + } else if (desired_argument_type == OPF_EVENT_NAME) { count = count_items_with_string(Campaign.missions[i].events, &mevent::name, CTEXT(node)); } else { - UNREACHABLE("type == %d; expected OPF_GOAL_NAME or OPF_EVENT_NAME", type); + UNREACHABLE("desired_argument_type == %d; expected OPF_GOAL_NAME or OPF_EVENT_NAME", desired_argument_type); } - } else if (type == OPF_GOAL_NAME) { + } else if (desired_argument_type == OPF_GOAL_NAME) { // neither the previous mission nor the previous goal is guaranteed to exist (missions can be developed out of sequence), so we don't need to check them - if ((Operators[op].value == OP_PREVIOUS_GOAL_TRUE) || (Operators[op].value == OP_PREVIOUS_GOAL_FALSE) || (Operators[op].value == OP_PREVIOUS_GOAL_INCOMPLETE)) + if ((op_const == OP_PREVIOUS_GOAL_TRUE) || (op_const == OP_PREVIOUS_GOAL_FALSE) || (op_const == OP_PREVIOUS_GOAL_INCOMPLETE)) break; count = count_items_with_string(Mission_goals, &mission_goal::name, CTEXT(node)); - } else if (type == OPF_EVENT_NAME) { + } else if (desired_argument_type == OPF_EVENT_NAME) { // neither the previous mission nor the previous event is guaranteed to exist (missions can be developed out of sequence), so we don't need to check them - if ((Operators[op].value == OP_PREVIOUS_EVENT_TRUE) || (Operators[op].value == OP_PREVIOUS_EVENT_FALSE) || (Operators[op].value == OP_PREVIOUS_EVENT_INCOMPLETE)) + if ((op_const == OP_PREVIOUS_EVENT_TRUE) || (op_const == OP_PREVIOUS_EVENT_FALSE) || (op_const == OP_PREVIOUS_EVENT_INCOMPLETE)) break; count = count_items_with_string(Mission_events, &mission_event::name, CTEXT(node)); } else { - UNREACHABLE("type == %d; expected OPF_GOAL_NAME or OPF_EVENT_NAME", type); + UNREACHABLE("desired_argument_type == %d; expected OPF_GOAL_NAME or OPF_EVENT_NAME", desired_argument_type); } if (count == 0) - return (type == OPF_GOAL_NAME) ? SEXP_CHECK_INVALID_GOAL_NAME : SEXP_CHECK_INVALID_EVENT_NAME; + return (desired_argument_type == OPF_GOAL_NAME) ? SEXP_CHECK_INVALID_GOAL_NAME : SEXP_CHECK_INVALID_EVENT_NAME; else if (count > 1) - return (type == OPF_GOAL_NAME) ? SEXP_CHECK_AMBIGUOUS_GOAL_NAME : SEXP_CHECK_AMBIGUOUS_EVENT_NAME; + return (desired_argument_type == OPF_GOAL_NAME) ? SEXP_CHECK_AMBIGUOUS_GOAL_NAME : SEXP_CHECK_AMBIGUOUS_EVENT_NAME; break; } case OPF_DOCKER_POINT: case OPF_DOCKEE_POINT: - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) return SEXP_CHECK_TYPE_MISMATCH; // This makes massive assumptions about the structure of the SEXP using it. If you add any @@ -3384,7 +3389,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s // Look for the node containing the docker/dockee ship. In most cases, we want // the current SEXP operator, but for ai-dock and the docker, we want its parent. - if (get_operator_const(op_node) == OP_AI_DOCK && type == OPF_DOCKER_POINT) { + if (op_const == OP_AI_DOCK && desired_argument_type == OPF_DOCKER_POINT) { z = find_parent_operator(op_node); // if it's the "goals" operator, this is part of initial orders, so we can't grab the ship from it @@ -3396,10 +3401,10 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } else { z = op_node; - if (get_operator_const(op_node) == OP_AI_DOCK) { // ai-dock with dockee + if (op_const == OP_AI_DOCK) { // ai-dock with dockee ship_node = CDR(z); - } else if (type == OPF_DOCKER_POINT) { - if (get_operator_const(op_node) >= First_available_operator_id) { + } else if (desired_argument_type == OPF_DOCKER_POINT) { + if (op_const >= First_available_operator_id) { int r_count = get_dynamic_parameter_index(Sexp_nodes[op_node].text, argnum); if (r_count < 0) @@ -3418,9 +3423,9 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } else { ship_node = CDR(z); } - } else if (type == OPF_DOCKEE_POINT) { + } else if (desired_argument_type == OPF_DOCKEE_POINT) { ship_node = CDDDR(z); - } else if (get_operator_const(op_node) >= First_available_operator_id) { + } else if (op_const >= First_available_operator_id) { int r_count = get_dynamic_parameter_index(Sexp_nodes[op_node].text, argnum); if (r_count < 0) @@ -3469,13 +3474,13 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; if (i == z) - return (type == OPF_DOCKER_POINT) ? SEXP_CHECK_INVALID_DOCKER_POINT : SEXP_CHECK_INVALID_DOCKEE_POINT; + return (desired_argument_type == OPF_DOCKER_POINT) ? SEXP_CHECK_INVALID_DOCKER_POINT : SEXP_CHECK_INVALID_DOCKEE_POINT; } break; case OPF_WHO_FROM: - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) return SEXP_CHECK_TYPE_MISMATCH; if (!is_special_sender(CTEXT(node))) { // not a manual source? @@ -3490,7 +3495,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s //Karajorma case OPF_PERSONA: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -3506,7 +3511,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_MISSION_MOOD: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -3551,7 +3556,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } case OPF_TEAM_COLOR: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -3564,7 +3569,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_FONT: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -3574,7 +3579,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_SOUND_ENVIRONMENT: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -3584,7 +3589,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_AUDIO_VOLUME_OPTION: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -3594,13 +3599,13 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s case OPF_BUILTIN_HUD_GAUGE: { - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } auto gauge_name = CTEXT(node); // for compatibility, since this operator now uses a different set of parameters - if (get_operator_const(op_node) == OP_FLASH_HUD_GAUGE) { + if (op_const == OP_FLASH_HUD_GAUGE) { bool found = false; for (int legacy_idx = 0; legacy_idx < NUM_HUD_GAUGES; legacy_idx++) { if (stricmp(gauge_name, Legacy_HUD_gauges[legacy_idx].hud_gauge_text) == 0) { @@ -3620,7 +3625,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } case OPF_CUSTOM_HUD_GAUGE: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -3631,7 +3636,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_ANY_HUD_GAUGE: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -3642,7 +3647,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_SOUND_ENVIRONMENT_OPTION: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -3652,7 +3657,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_EXPLOSION_OPTION: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -3662,7 +3667,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_KEYPRESS: - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) return SEXP_CHECK_TYPE_MISMATCH; break; @@ -3670,12 +3675,12 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s case OPF_CARGO: case OPF_STRING: case OPF_MESSAGE_OR_STRING: - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) return SEXP_CHECK_TYPE_MISMATCH; break; case OPF_SKILL_LEVEL: - if ( type2 != SEXP_ATOM_STRING ) + if ( node_subtype != SEXP_ATOM_STRING ) return SEXP_CHECK_TYPE_MISMATCH; for (i = 0; i < NUM_SKILL_LEVELS; i++) { @@ -3687,7 +3692,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_MEDAL_NAME: - if ( type2 != SEXP_ATOM_STRING) + if ( node_subtype != SEXP_ATOM_STRING) return SEXP_CHECK_TYPE_MISMATCH; for (i = 0; i < (int)Medals.size(); i++) { @@ -3701,7 +3706,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s case OPF_HUGE_WEAPON: case OPF_WEAPON_NAME: - if ( type2 != SEXP_ATOM_STRING ) + if ( node_subtype != SEXP_ATOM_STRING ) return SEXP_CHECK_TYPE_MISMATCH; i = weapon_info_lookup(CTEXT(node)); @@ -3710,7 +3715,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s return SEXP_CHECK_INVALID_WEAPON_NAME; // we need to be sure that for huge weapons, the WIF_HUGE flag is set - if ( type == OPF_HUGE_WEAPON ) { + if (desired_argument_type == OPF_HUGE_WEAPON ) { if ( !(Weapon_info[i].wi_flags[Weapon::Info_Flags::Huge]) ) return SEXP_CHECK_INVALID_WEAPON_NAME; } @@ -3719,7 +3724,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s // Goober5000 case OPF_INTEL_NAME: - if ( type2 != SEXP_ATOM_STRING ) + if ( node_subtype != SEXP_ATOM_STRING ) return SEXP_CHECK_TYPE_MISMATCH; for ( i = 0; i < intel_info_size(); i++ ) { @@ -3733,7 +3738,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_TURRET_TARGET_ORDER: - if ( type2 != SEXP_ATOM_STRING ) + if ( node_subtype != SEXP_ATOM_STRING ) return SEXP_CHECK_TYPE_MISMATCH; for (i = 0; i < NUM_TURRET_ORDER_TYPES; i++ ) { @@ -3747,7 +3752,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_TURRET_TYPE: - if (type2 != SEXP_ATOM_STRING) + if (node_subtype != SEXP_ATOM_STRING) return SEXP_CHECK_TYPE_MISMATCH; for (i = 0; i < NUM_TURRET_TYPES; i++) { @@ -3761,7 +3766,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_ARMOR_TYPE: - if ( type2 != SEXP_ATOM_STRING ) + if ( node_subtype != SEXP_ATOM_STRING ) return SEXP_CHECK_TYPE_MISMATCH; if (!stricmp(CTEXT(node), SEXP_NONE_STRING)) @@ -3778,7 +3783,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_DAMAGE_TYPE: - if ( type2 != SEXP_ATOM_STRING ) + if ( node_subtype != SEXP_ATOM_STRING ) return SEXP_CHECK_TYPE_MISMATCH; if (!stricmp(CTEXT(node), SEXP_NONE_STRING)) @@ -3795,7 +3800,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_ANIMATION_TYPE: - if ( type2 != SEXP_ATOM_STRING ) + if ( node_subtype != SEXP_ATOM_STRING ) return SEXP_CHECK_TYPE_MISMATCH; if (animation::anim_match_type(CTEXT(node)) == animation::ModelAnimationTriggerType::None ) @@ -3804,7 +3809,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_TARGET_PRIORITIES: - if ( type2 != SEXP_ATOM_STRING ) + if ( node_subtype != SEXP_ATOM_STRING ) return SEXP_CHECK_TYPE_MISMATCH; for(st = 0; st < Ai_tp_list.size(); st++) { @@ -3818,7 +3823,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_SHIP_CLASS_NAME: - if ( type2 != SEXP_ATOM_STRING ) + if ( node_subtype != SEXP_ATOM_STRING ) return SEXP_CHECK_TYPE_MISMATCH; if (ship_info_lookup(CTEXT(node)) < 0) @@ -3827,7 +3832,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_SKYBOX_MODEL_NAME: - if ( type2 != SEXP_ATOM_STRING ) + if ( node_subtype != SEXP_ATOM_STRING ) return SEXP_CHECK_TYPE_MISMATCH; if ( stricmp(CTEXT(node), NOX("default")) != 0 && stricmp(CTEXT(node), NOX("none")) != 0 && !strstr(CTEXT(node), NOX(".pof")) ) @@ -3836,7 +3841,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_SKYBOX_FLAGS: - if ( type2 != SEXP_ATOM_STRING ) + if ( node_subtype != SEXP_ATOM_STRING ) return SEXP_CHECK_TYPE_MISMATCH; for ( i = 0; i < Num_skybox_flags; ++i ) { @@ -3851,7 +3856,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_JUMP_NODE_NAME: - if ( type2 != SEXP_ATOM_STRING ) + if ( node_subtype != SEXP_ATOM_STRING ) return SEXP_CHECK_TYPE_MISMATCH; if (jumpnode_get_by_name(CTEXT(node)) == nullptr) @@ -3865,7 +3870,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s if (var_index < 0) return SEXP_CHECK_INVALID_VARIABLE; - switch (Operators[op].value) + switch (op_const) { // some SEXPs demand a number variable case OP_ADD_BACKGROUND_BITMAP: @@ -3908,13 +3913,13 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s case OPF_NEBULA_POOF: case OPF_NEBULA_PATTERN: case OPF_POST_EFFECT: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } break; case OPF_HUD_ELEMENT: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } else { auto gauge = CTEXT(node); @@ -3925,7 +3930,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s return SEXP_CHECK_INVALID_HUD_ELEMENT; case OPF_WEAPON_BANK_NUMBER: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -3943,7 +3948,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_SHIP_EFFECT: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -3953,14 +3958,14 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_GAME_SND: - if (type2 == SEXP_ATOM_NUMBER) + if (node_subtype == SEXP_ATOM_NUMBER) { if (!gamesnd_get_by_tbl_index(atoi(CTEXT(node))).isValid()) { return SEXP_CHECK_NUM_RANGE_INVALID; } } - else if (type2 == SEXP_ATOM_STRING) + else if (node_subtype == SEXP_ATOM_STRING) { if (stricmp(CTEXT(node), SEXP_NONE_STRING) != 0 && !gamesnd_get_by_name(CTEXT(node)).isValid()) { @@ -3970,7 +3975,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_FIREBALL: - if (type2 == SEXP_ATOM_NUMBER || can_construe_as_integer(CTEXT(node))) + if (node_subtype == SEXP_ATOM_NUMBER || can_construe_as_integer(CTEXT(node))) { int num = atoi(CTEXT(node)); if (!SCP_vector_inbounds(Fireball_info, num)) @@ -3978,7 +3983,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s return SEXP_CHECK_NUM_RANGE_INVALID; } } - else if (type2 == SEXP_ATOM_STRING) + else if (node_subtype == SEXP_ATOM_STRING) { if (fireball_info_lookup(CTEXT(node)) < 0) { @@ -3988,7 +3993,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_SPECIES: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -3998,7 +4003,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_LANGUAGE: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -4006,7 +4011,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_FUNCTIONAL_WHEN_EVAL_TYPE: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -4019,15 +4024,15 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s case OPF_LIST_CONTAINER_NAME: case OPF_MAP_CONTAINER_NAME: { - if (type2 != SEXP_ATOM_CONTAINER_NAME) { + if (node_subtype != SEXP_ATOM_CONTAINER_NAME) { return SEXP_CHECK_TYPE_MISMATCH; } p_container = get_sexp_container(Sexp_nodes[node].text); Assertion(p_container, "Attempt to use unknown container %s. Please report!", Sexp_nodes[node].text); - if ((type == OPF_LIST_CONTAINER_NAME && !p_container->is_list()) || - (type == OPF_MAP_CONTAINER_NAME && !p_container->is_map())) { + if ((desired_argument_type == OPF_LIST_CONTAINER_NAME && !p_container->is_list()) || + (desired_argument_type == OPF_MAP_CONTAINER_NAME && !p_container->is_map())) { return SEXP_CHECK_WRONG_CONTAINER_TYPE; } break; @@ -4036,13 +4041,13 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s case OPF_CONTAINER_VALUE: Assertion(p_container, "Attempt to check value arg for null container for SEXP operator %d at arg %d. Please report!", - op, + op_const, argnum); - z = check_container_value_data_type(get_operator_const(op_node), + z = check_container_value_data_type(op_const, argnum, p_container->type, - (type2 == SEXP_ATOM_STRING), - (type2 == OPR_NUMBER) || (type2 == OPR_POSITIVE)); + (node_subtype == SEXP_ATOM_STRING), + (node_return_type == OPR_NUMBER) || (node_return_type == OPR_POSITIVE)); if (z) { return z; } @@ -4050,7 +4055,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s case OPF_DATA_OR_STR_CONTAINER: { - if (type2 == SEXP_ATOM_CONTAINER_NAME) { + if (node_subtype == SEXP_ATOM_CONTAINER_NAME) { // only list containers of strings or map containers with string keys are allowed const auto *p_str_container = get_sexp_container(Sexp_nodes[node].text); Assertion(p_str_container, @@ -4068,7 +4073,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s } case OPF_ASTEROID_TYPES: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } else { auto list = get_list_valid_asteroid_subtypes(); @@ -4087,7 +4092,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_DEBRIS_TYPES: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -4097,7 +4102,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_WING_FORMATION: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -4111,7 +4116,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_MOTION_DEBRIS: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -4121,7 +4126,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_BOLT_TYPE: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -4131,7 +4136,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_TRAITOR_OVERRIDE: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -4141,7 +4146,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_LUA_GENERAL_ORDER: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -4151,7 +4156,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_MISSION_CUSTOM_STRING: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -4161,7 +4166,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; case OPF_MESSAGE_TYPE: - if (type2 != SEXP_ATOM_STRING) { + if (node_subtype != SEXP_ATOM_STRING) { return SEXP_CHECK_TYPE_MISMATCH; } @@ -4171,9 +4176,9 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, s break; default: //This handles OPF_CHILD_LUA_ENUM as well - if (Dynamic_enums.size() > 0) { - if ((type - First_available_opf_id) < (int)Dynamic_enums.size()) { - if (type2 != SEXP_ATOM_STRING) + if (!Dynamic_enums.empty()) { + if ((desired_argument_type - First_available_opf_id) < sz2i(Dynamic_enums.size())) { + if (node_subtype != SEXP_ATOM_STRING) return SEXP_CHECK_TYPE_MISMATCH; } else { Error(LOCATION, "Unhandled argument format"); @@ -31984,35 +31989,35 @@ int query_operator_return_type(int op) * @param op operator index * @param argnum is 0 indexed. */ -int query_operator_argument_type(int op, int argnum) +int query_operator_argument_type(int op_index, int argnum) { - if (op < 0) + if (op_index < 0) return OPF_NONE; - int index = op; - - if (op < FIRST_OP) + int op_const; + if (op_index < FIRST_OP) { - Assertion(SCP_vector_inbounds(Operators, index), "Operator index is out of bounds!"); - op = Operators[index].value; + Assertion(SCP_vector_inbounds(Operators, op_index), "Operator index is out of bounds!"); + op_const = Operators[op_index].value; } else { Warning(LOCATION, "Possible unnecessary search for operator index. Trace out and see if this is necessary.\n"); + op_const = op_index; - int count = static_cast(Operators.size()); - for (index=0; index= count) + if (op_index >= count) return OPF_NONE; } - if (argnum >= Operators[index].max) + if (argnum >= Operators[op_index].max) return OPF_NONE; - switch (op) { + switch (op_const) { case OP_TRUE: case OP_FALSE: case OP_MISSION_TIME: @@ -33240,7 +33245,7 @@ int query_operator_argument_type(int op, int argnum) case OP_SEND_MESSAGE_CHAIN: { // chain has one extra argument but is otherwise the same - if (op == OP_SEND_MESSAGE_CHAIN) + if (op_const == OP_SEND_MESSAGE_CHAIN) { if (argnum == 0) return OPF_EVENT_NAME; @@ -34860,12 +34865,12 @@ int query_operator_argument_type(int op, int argnum) return OPF_POSITIVE; default: { - auto dynamicSEXP = sexp::get_dynamic_sexp(op); + auto dynamicSEXP = sexp::get_dynamic_sexp(op_const); if (dynamicSEXP != nullptr) { return dynamicSEXP->getArgumentType(argnum); } - Assertion(false, "query_operator_argument_type(%d, %d) called for unsupported operator type!", op, argnum); + Assertion(false, "query_operator_argument_type(%d, %d) called for unsupported operator type!", op_const, argnum); } } From 66eb4702e123493545a338205ba5c4029196cf02 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 28 Apr 2026 01:26:11 -0400 Subject: [PATCH 05/65] fix coerce_to_utf8 Fix a bug that Claude caught: conversion from Latin-1 would fall through, truncate, and emit a spurious warning. --- code/parse/parselo.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/code/parse/parselo.cpp b/code/parse/parselo.cpp index 9f1e4659181..a89fef40417 100644 --- a/code/parse/parselo.cpp +++ b/code/parse/parselo.cpp @@ -2568,6 +2568,7 @@ void coerce_to_utf8(SCP_string &buffer, const char *str) if (isLatin1) { unicode::convert_encoding(buffer, str, unicode::Encoding::Encoding_iso8859_1, unicode::Encoding::Encoding_utf8); + return; } // unknown encoding, so just truncate From e2707549f4b504b9bdce865ad17774707dce5070 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Thu, 30 Apr 2026 00:35:19 -0400 Subject: [PATCH 06/65] add documentation for issue 4148 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Claude: The key things the new comments add that the old ones were missing: 1. Palette RGB colors are completely ignored — only the index value is used as alpha. This is the non-obvious invariant that @notimaginative described. 2. Exact mapping table — makes the * 18 / * 17 math immediately readable, and clarifies the ANI vs PCX difference (> 15 clamps to opaque in ANI but transparent in PCX). 3. The broken-palette consequence — explicitly states that a reversed palette causes a white square, linking to #4148 so future readers understand the context. --- code/anim/packunpack.cpp | 28 ++++++++++++++++++---------- code/pcxutils/pcxutils.cpp | 15 ++++++++++----- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/code/anim/packunpack.cpp b/code/anim/packunpack.cpp index 0af7526ffee..9b4f44a67c5 100644 --- a/code/anim/packunpack.cpp +++ b/code/anim/packunpack.cpp @@ -142,11 +142,15 @@ int unpack_pixel(anim_instance *ai, ubyte *data, ubyte pix, int aabitmap, int bp bit_16 = (ushort)pix; break; case 8: - // 8 bit-per-pixel aa bitmaps are a bit special since they only use a palette index value in the range [0, 15]. These - // palette indexes must be remapped to alpha values between [0, 255] which is what graphics code expects. Palette - // range [0, 14] is a gradient from black to white, and palette index 15 is a special color which indicates the background - // area of a HUD gauge. Retail code uses the final alpha value for index 1 for this special index to give gauges a dark - // transparent background. + // 8-bit-per-pixel HUD gauge ANI aabitmaps use the palette index directly as an alpha value — + // the actual RGB colors stored in the palette are completely ignored. The valid range is [0, 15]: + // [0]: fully transparent (alpha 0) + // [1-14]: black-to-white gradient (alpha = index * 18) + // [15]: special HUD background (alpha 18, matching index 1, for a dark transparent tint) + // [>15]: clamped to fully opaque (alpha 255) + // Because only the index value matters, the palette in the source file must be ordered so that + // index 0 is the transparent end and index 14 is the opaque end. A reversed palette causes the + // gauge to render as a solid white square (see GitHub issue #4148). if (pix > 15) { bit_8 = 255; } @@ -240,11 +244,15 @@ int unpack_pixel_count(anim_instance *ai, ubyte *data, ubyte pix, int count = 0, bit_16 = (ushort)pix; break; case 8 : - // 8 bit-per-pixel aa bitmaps are a bit special since they only use a palette index value in the range [0, 15]. These - // palette indexes must be remapped to alpha values between [0, 255] which is what graphics code expects. Palette - // range [0, 14] is a gradient from black to white, and palette index 15 is a special color which indicates the background - // area of a HUD gauge. Retail code uses the final alpha value for index 1 for this special index to give gauges a dark - // transparent background. + // 8-bit-per-pixel HUD gauge ANI aabitmaps use the palette index directly as an alpha value — + // the actual RGB colors stored in the palette are completely ignored. The valid range is [0, 15]: + // [0]: fully transparent (alpha 0) + // [1-14]: black-to-white gradient (alpha = index * 18) + // [15]: special HUD background (alpha 18, matching index 1, for a dark transparent tint) + // [>15]: clamped to fully opaque (alpha 255) + // Because only the index value matters, the palette in the source file must be ordered so that + // index 0 is the transparent end and index 14 is the opaque end. A reversed palette causes the + // gauge to render as a solid white square (see GitHub issue #4148). if (pix > 15) { bit_8 = 255; } diff --git a/code/pcxutils/pcxutils.cpp b/code/pcxutils/pcxutils.cpp index 27e90109c5a..2dd6888375b 100644 --- a/code/pcxutils/pcxutils.cpp +++ b/code/pcxutils/pcxutils.cpp @@ -312,11 +312,16 @@ int pcx_read_bitmap( const char * real_filename, ubyte *org_data, ubyte * /*pal* if ( byte_size == 1 ) { auto pixel_val = data; if (!mask_bitmap) { - // 8 bit-per-pixel aa bitmaps are a bit special since they only use values in the range [0, 15] where 15 wraps - // around back to 0. Since the rest of the code expects the value to be in the range [0, 255] the pixel value - // needs to be adjusted here. By multiplying the value with 17 the original range [0, 15] is mapped to [0, 255] - // This only applies to bitmaps that are not used as masks since mask bitmaps use higher values - // to indicate their mask area + // 8-bit-per-pixel HUD gauge PCX aabitmaps use the pixel value directly as an alpha level — + // the actual RGB colors in the palette are completely ignored. The valid range is [0, 15]: + // [0]: fully transparent (alpha 0) + // [1-14]: black-to-white gradient (alpha = value * 17) + // [15]: special HUD background (alpha 17, matching index 1, for a dark transparent tint) + // [>15]: clamped to fully transparent (alpha 0) + // Because only the pixel value matters, the palette in the source file must be ordered so that + // index 0 is the transparent end and index 14 is the opaque end. A reversed palette causes the + // gauge to render as a solid white square (see GitHub issue #4148). + // Mask bitmaps skip this block because they use the full value range for the mask area. if (data > 15) { pixel_val = 0; } else if (data == 15) { From 4235e4f2520a66d1913f10b635c5addc0cde0dd0 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Thu, 30 Apr 2026 01:35:07 -0400 Subject: [PATCH 07/65] small followup to 7327 Submodel `collision_tree_index` should be reset to -1 when removed, not 0. --- code/model/modelread.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index 33fb6102467..81ad988d2fc 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -196,7 +196,7 @@ void model_free(polymodel* pm) if (pm->submodel[i].collision_tree_index >= 0) { model_remove_bsp_collision_tree(pm->submodel[i].collision_tree_index); - pm->submodel[i].collision_tree_index = 0; + pm->submodel[i].collision_tree_index = -1; } } } From 6e5e3425755e18b1d06094f35e4ba8989dcf859b Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Thu, 30 Apr 2026 02:08:32 -0400 Subject: [PATCH 08/65] fix asteroid field undefined behavior Three out-of-bounds/UB fixes for issue #6793: - asteroid_create_all: skip field_debris_type[idx] == -1 entries when building the DG_DEBRIS odds table and loading models; iterate only over valid table slots during spawn. Matches the guard already present in asteroid_frame. - missionparse: bounds-check subtype before indexing colors[] in the obsolete +Field Debris Type: parsing path. - verify_asteroid_list: guard the asteroid_list[0] fallback access against an empty list. - asteroid_init: emit an mprintf when no asteroids are defined instead of returning silently. Co-Authored-By: Claude Sonnet 4.6 --- code/asteroid/asteroid.cpp | 25 +++++++++++++++++++------ code/mission/missionparse.cpp | 6 +++++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/code/asteroid/asteroid.cpp b/code/asteroid/asteroid.cpp index c31d30ab7ec..7dedcde34ba 100644 --- a/code/asteroid/asteroid.cpp +++ b/code/asteroid/asteroid.cpp @@ -648,24 +648,30 @@ void asteroid_create_all() int max_asteroids = Asteroid_field.num_initial_asteroids; // * (1.0f - 0.1f*(MAX_DETAIL_VALUE-Detail.asteroid_density))); int num_debris_types = 0; + int num_valid = 0; // count of valid (non-(-1)) entries written into shipDebrisOddsTable // get number of debris types if (Asteroid_field.debris_genre == DG_DEBRIS) { num_debris_types = static_cast(Asteroid_field.field_debris_type.size()); - // Calculate the odds table + // Calculate the odds table; skip invalid entries (field_debris_type[idx] == -1 means + // missionparse invalidated the entry because the type no longer exists in Asteroid_info) for (idx=0; idx(Asteroid_field.field_debris_type.size()); idx++) { + for (idx = 0; idx < num_valid; idx++) { // for ship debris, choose type according to odds table if (rand_choice < shipDebrisOddsTable[idx].random_threshold) { asteroid_create(&Asteroid_field, shipDebrisOddsTable[idx].debris_type, 0); @@ -2641,6 +2647,11 @@ static void verify_asteroid_list() if (found) continue; + if (asteroid_list.empty()) { + mprintf(("%s asteroid not found and no fallback available in asteroid_list.\n", asteroid_size[i].c_str())); + continue; + } + //Left this as a log print instead of a Warning because of retail-Mjn mprintf(("%s asteroid not found. Using asteroid %s\n", asteroid_size[i].c_str(), asteroid_list[0].name)); asteroid_list[0].type = i; @@ -2695,8 +2706,10 @@ void asteroid_init() parse_modular_table("*-ast.tbm", asteroid_parse_tbl); //No asteroids defined. Bail! - if (asteroid_list.empty()) + if (asteroid_list.empty()) { + mprintf(("No asteroids defined in asteroid.tbl or any modular tables. Asteroid fields will not function.\n")); return; + } // now verify the asteroids were found and put them in the correct order verify_asteroid_list(); diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index a266c04b7f8..057a62afe4f 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -6392,7 +6392,11 @@ void parse_asteroid_fields(mission *pm) if (optional_string("+Field Debris Type:")) { int subtype; stuff_int(&subtype); - Asteroid_field.field_asteroid_type.push_back(colors[subtype]); + if (subtype >= 0 && subtype < NUM_ASTEROID_SIZES) { + Asteroid_field.field_asteroid_type.push_back(colors[subtype]); + } else { + WarningEx(LOCATION, "Invalid +Field Debris Type value %d in asteroid field (must be 0-%d); ignoring.", subtype, NUM_ASTEROID_SIZES - 1); + } } } From 37ecbe439a7194ceb0877e74e5534f4e0bc22459 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Fri, 1 May 2026 09:16:40 +0200 Subject: [PATCH 09/65] Gate correct insignias behind flag / version (#7406) * Gate correct insignias behind flag / version * Move flag to graphics settings --- code/mod_table/mod_table.cpp | 7 +++++++ code/mod_table/mod_table.h | 1 + code/model/model.h | 5 +++++ code/model/modelread.cpp | 32 ++++++++++++++++++++++++++++---- code/model/modelrender.cpp | 28 +++++++++++++++++++++++++++- 5 files changed, 68 insertions(+), 5 deletions(-) diff --git a/code/mod_table/mod_table.cpp b/code/mod_table/mod_table.cpp index 5c7a319456b..29de69de533 100644 --- a/code/mod_table/mod_table.cpp +++ b/code/mod_table/mod_table.cpp @@ -183,6 +183,7 @@ bool Disable_expensive_turret_target_check; float Shield_percent_skips_damage; float Min_radius_for_persistent_debris; bool Zero_radius_explosions_skip_fireballs; +bool Render_insignias_as_decals; #ifdef WITH_DISCORD @@ -1004,6 +1005,10 @@ void parse_mod_table(const char *filename) stuff_boolean(&Disable_all_noncustom_generic_debris); } + if (optional_string("$Render insignias as decals:")) { + stuff_boolean(&Render_insignias_as_decals); + } + optional_string("#NETWORK SETTINGS"); if (optional_string("$FS2NetD port:")) { @@ -1891,6 +1896,7 @@ void mod_table_reset() Shield_percent_skips_damage = 0.1f; Min_radius_for_persistent_debris = 50.0f; Zero_radius_explosions_skip_fireballs = false; + Render_insignias_as_decals = false; } void mod_table_set_version_flags() @@ -1921,5 +1927,6 @@ void mod_table_set_version_flags() } if (mod_supports_version(26, 0, 0)) { Zero_radius_explosions_skip_fireballs = true; + Render_insignias_as_decals = true; } } diff --git a/code/mod_table/mod_table.h b/code/mod_table/mod_table.h index a715083c95a..af0470548fd 100644 --- a/code/mod_table/mod_table.h +++ b/code/mod_table/mod_table.h @@ -205,6 +205,7 @@ extern bool Disable_expensive_turret_target_check; extern float Shield_percent_skips_damage; extern float Min_radius_for_persistent_debris; extern bool Zero_radius_explosions_skip_fireballs; +extern bool Render_insignias_as_decals; void mod_table_init(); void mod_table_post_process(); diff --git a/code/model/model.h b/code/model/model.h index 0c8197fdd86..83ed520ca15 100644 --- a/code/model/model.h +++ b/code/model/model.h @@ -735,6 +735,11 @@ typedef struct insignia { vec3d vecs[MAX_INS_VECS]; // vertex list vec3d offset; // global position offset for this insignia vec3d norm[MAX_INS_VECS] ; //normal of the insignia-Bobboau + + // Computed fields for decal rendering + vec3d position; + matrix orientation; + float diameter; } insignia; #define PM_FLAG_ALLOW_TILING (1<<0) // Allow texture tiling diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index 81ad988d2fc..c522bb52943 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -2742,6 +2742,11 @@ modelread_status read_model_file_no_subsys(polymodel * pm, const char* filename, // read in world offset cfread_vector(&pm->ins[idx].offset, fp); + vec3d min = {{{FLT_MAX, FLT_MAX, FLT_MAX}}}; + vec3d max = {{{-FLT_MAX, -FLT_MAX, -FLT_MAX}}}; + vec3d avg_total = ZERO_VECTOR; + vec3d avg_normal = ZERO_VECTOR; + // read in all the faces for(idx2=0; idx2ins[idx].num_faces; idx2++){ // read in 3 vertices @@ -2753,18 +2758,37 @@ modelread_status read_model_file_no_subsys(polymodel * pm, const char* filename, vec3d tempv; //get three points (rotated) and compute normal + const vec3d& v1 = pm->ins[idx].vecs[pm->ins[idx].faces[idx2][0]]; + const vec3d& v2 = pm->ins[idx].vecs[pm->ins[idx].faces[idx2][1]]; + const vec3d& v3 = pm->ins[idx].vecs[pm->ins[idx].faces[idx2][2]]; vm_vec_perp(&tempv, - &pm->ins[idx].vecs[pm->ins[idx].faces[idx2][0]], - &pm->ins[idx].vecs[pm->ins[idx].faces[idx2][1]], - &pm->ins[idx].vecs[pm->ins[idx].faces[idx2][2]]); + &v1, + &v2, + &v3); vm_vec_normalize_safe(&tempv); pm->ins[idx].norm[idx2] = tempv; -// mprintf(("insignorm %.2f %.2f %.2f\n",pm->ins[idx].norm[idx2].xyz.x, pm->ins[idx].norm[idx2].xyz.y, pm->ins[idx].norm[idx2].xyz.z)); + // mprintf(("insignorm %.2f %.2f %.2f\n",pm->ins[idx].norm[idx2].xyz.x, pm->ins[idx].norm[idx2].xyz.y, pm->ins[idx].norm[idx2].xyz.z)); + + vm_vec_min(&min, &min, &v1); + vm_vec_min(&min, &min, &v2); + vm_vec_min(&min, &min, &v3); + vm_vec_max(&max, &max, &v1); + vm_vec_max(&max, &max, &v2); + vm_vec_max(&max, &max, &v3); + + vec3d avg = (v1 + v2 + v3) * (1.0f / 3.0f); + avg_total += avg; + avg_normal += tempv; } + + pm->ins[idx].position = avg_total / static_cast(num_faces) + pm->ins[idx].offset; + vec3d bb = max - min; + pm->ins[idx].diameter = std::max({bb.xyz.x, bb.xyz.y, bb.xyz.z}); + vm_vector_2_matrix(&pm->ins[idx].orientation, &avg_normal, &vmd_z_vector); } break; diff --git a/code/model/modelrender.cpp b/code/model/modelrender.cpp index 8e6944b3df4..651f46a2242 100644 --- a/code/model/modelrender.cpp +++ b/code/model/modelrender.cpp @@ -11,7 +11,10 @@ #include "asteroid/asteroid.h" #include "cmdline/cmdline.h" +#include "decals/decals.h" #include "gamesequence/gamesequence.h" +#include "globalincs/systemvars.h" +#include "math/floating.h" #include "graphics/light.h" #include "graphics/matrix.h" #include "graphics/shadows.h" @@ -2984,7 +2987,30 @@ void model_render_queue(const model_render_params* interp, model_draw_list* scen // MARKED! if ( !( model_flags & MR_NO_TEXTURING ) && !( model_flags & MR_NO_INSIGNIA) ) { - scene->add_insignia(interp, pm, detail_level, interp->get_insignia_bitmap()); + int bitmap_num = interp->get_insignia_bitmap(); + if ( Render_insignias_as_decals && objnum >= 0 && (pm->num_ins > 0) && (bitmap_num >= 0) ) { + for (int ins_idx = 0; ins_idx < pm->num_ins; ins_idx++) { + const insignia& ins = pm->ins[ins_idx]; + // skip insignias not on our detail level + if (ins.detail_level != detail_level) { + continue; + } + + decals::Decal decal; + decal.object = &Objects[objnum]; + decal.position = ins.position; + decal.submodel = -1; + decal.scale = vec3d{{{ins.diameter, ins.diameter, ins.diameter}}}; + decal.orig_obj_type = OBJ_SHIP; + decal.creation_time = f2fl(Missiontime); + decal.lifetime = 1.0f; + decal.orientation = ins.orientation; + decal.definition_handle = std::make_tuple(bitmap_num, -1, -1); + decals::addSingleFrameDecal(std::move(decal)); + } + } else { + scene->add_insignia(interp, pm, detail_level, bitmap_num); + } } if ( (model_flags & MR_AUTOCENTER) && (set_autocen) ) { From 683b533467c3c78892054a3e2e8974016eaafb0b Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Fri, 1 May 2026 07:36:45 -0500 Subject: [PATCH 10/65] Qtfred Upgrade checkbox dialog (#7405) --- qtfred/source_groups.cmake | 4 +- .../ShipEditor/ShipEditorDialogModel.cpp | 70 ++++++++------- .../ShipEditor/ShipEditorDialogModel.h | 5 +- .../ui/dialogs/General/CheckBoxListDialog.cpp | 70 ++++++--------- .../ui/dialogs/General/CheckBoxListDialog.h | 16 +++- .../dialogs/ShipEditor/ShipEditorDialog.cpp | 31 +++---- qtfred/src/ui/dialogs/WingEditorDialog.cpp | 20 ++++- qtfred/src/ui/dialogs/WingFlagsDialog.cpp | 42 --------- qtfred/src/ui/dialogs/WingFlagsDialog.h | 29 ------ qtfred/src/ui/widgets/FlagList.cpp | 12 +++ qtfred/src/ui/widgets/FlagList.h | 5 ++ qtfred/ui/CheckBoxListDialog.ui | 38 ++++---- qtfred/ui/WingFlagsDialog.ui | 90 ------------------- 13 files changed, 145 insertions(+), 287 deletions(-) delete mode 100644 qtfred/src/ui/dialogs/WingFlagsDialog.cpp delete mode 100644 qtfred/src/ui/dialogs/WingFlagsDialog.h delete mode 100644 qtfred/ui/WingFlagsDialog.ui diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 8901c0f12b9..6c2d6019681 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -216,9 +216,8 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/WaypointPathGeneratorDialog.h src/ui/dialogs/WingEditorDialog.cpp src/ui/dialogs/WingEditorDialog.h - src/ui/dialogs/WingFlagsDialog.cpp - src/ui/dialogs/WingFlagsDialog.h ) + add_file_folder("Source/UI/Dialogs/BriefingEditor" src/ui/dialogs/BriefingEditor/CameraCoordinatesDialog.cpp src/ui/dialogs/BriefingEditor/CameraCoordinatesDialog.h @@ -373,7 +372,6 @@ add_file_folder("UI" ui/ShipWeaponsDialog.ui ui/VariableDialog.ui ui/WingEditorDialog.ui - ui/WingFlagsDialog.ui ui/SaveAsTemplateDialog.ui ui/SceneBrowserPanel.ui ui/TemplateBrowserDialog.ui diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp index 62263a0b0b3..ef59720bc83 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp @@ -78,58 +78,62 @@ int ShipEditorDialogModel::getIfPlayerShip() const { return player_ship; } -std::vector> ShipEditorDialogModel::getAcceptedOrders() const + +SCP_vector> ShipEditorDialogModel::getPlayerOrders() { - std::vector> acceptedOrders; - object* objp; - SCP_set default_orders; - if (!multi_edit) { - default_orders = ship_get_default_orders_accepted(&Ship_info[Ships[_editor->cur_ship].ship_info_index]); - } else { - for (objp = GET_FIRST(&obj_used_list); objp != END_OF_LIST(&obj_used_list); objp = GET_NEXT(objp)) { - if (((objp->type == OBJ_SHIP) || (objp->type == OBJ_START)) && - (objp->flags[Object::Object_Flags::Marked])) { - const SCP_set& these_orders = - ship_get_default_orders_accepted(&Ship_info[Ships[objp->instance].ship_info_index]); + SCP_vector> orders; - if (default_orders.empty()) { - default_orders = these_orders; - } else { - Assert(default_orders == these_orders); - } + // Build the canonical default order set from marked ships (caller guarantees all marked ships share it) + SCP_set default_orders; + object* objp; + for (objp = GET_FIRST(&obj_used_list); objp != END_OF_LIST(&obj_used_list); objp = GET_NEXT(objp)) { + if (((objp->type == OBJ_SHIP) || (objp->type == OBJ_START)) && (objp->flags[Object::Object_Flags::Marked])) { + const SCP_set& these_orders = + ship_get_default_orders_accepted(&Ship_info[Ships[objp->instance].ship_info_index]); + if (default_orders.empty()) { + default_orders = these_orders; + } else { + Assert(default_orders == these_orders); } } } for (size_t order_id : default_orders) { SCP_string name = Player_orders[order_id].localized_name; - bool state = false; - const SCP_set& orders_accepted = Ships[_editor->cur_ship].orders_accepted; - if (orders_accepted.contains(order_id)) - state = true; - acceptedOrders.emplace_back(name, state); + int state = -1; + for (objp = GET_FIRST(&obj_used_list); objp != END_OF_LIST(&obj_used_list); objp = GET_NEXT(objp)) { + if (((objp->type == OBJ_SHIP) || (objp->type == OBJ_START)) && + (objp->flags[Object::Object_Flags::Marked])) { + int ship_state = Ships[objp->instance].orders_accepted.contains(order_id) ? Qt::Checked : Qt::Unchecked; + state = (state == -1) ? ship_state : tristate_set(ship_state, state); + } + } + orders.emplace_back(name, (state == -1) ? Qt::Unchecked : state); } - return acceptedOrders; + + return orders; } -void ShipEditorDialogModel::setAcceptedOrders(const std::vector>& newOrders) + +void ShipEditorDialogModel::applyPlayerOrders(const SCP_vector>& orders) { - orders = newOrders; - // Write directly to all marked ships - for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + object* ptr; + for (ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { auto i = ptr->instance; SCP_set default_orders = ship_get_default_orders_accepted(&Ship_info[Ships[i].ship_info_index]); - SCP_set new_orders_set; for (size_t order_id : default_orders) { - for (const auto& order : orders) { - if (order.first == Player_orders[order_id].localized_name) { - if (order.second) { - new_orders_set.insert(order_id); + for (const auto& [name, state] : orders) { + if (name == Player_orders[order_id].localized_name) { + if (state == Qt::Checked) { + Ships[i].orders_accepted.insert(order_id); + } else if (state == Qt::Unchecked) { + Ships[i].orders_accepted.erase(order_id); } + // Qt::PartiallyChecked: leave each ship's existing state unchanged + break; } } } - Ships[i].orders_accepted = new_orders_set; } } set_modified(); diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h index b6908e46806..97777e29b1b 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h @@ -70,7 +70,6 @@ class ShipEditorDialogModel : public AbstractDialogModel { int respawn_priority; - std::vector> orders; std::vector> arrivalPaths; std::vector> departurePaths; @@ -244,8 +243,8 @@ class ShipEditorDialogModel : public AbstractDialogModel { */ int getIfPlayerShip() const; - std::vector> getAcceptedOrders() const; - void setAcceptedOrders(const std::vector>&); + static SCP_vector> getPlayerOrders(); + void applyPlayerOrders(const SCP_vector>& orders); std::vector> getArrivalPaths() const; void setArrivalPaths(const std::vector>&); diff --git a/qtfred/src/ui/dialogs/General/CheckBoxListDialog.cpp b/qtfred/src/ui/dialogs/General/CheckBoxListDialog.cpp index 2a3b973b28d..a1214ff5422 100644 --- a/qtfred/src/ui/dialogs/General/CheckBoxListDialog.cpp +++ b/qtfred/src/ui/dialogs/General/CheckBoxListDialog.cpp @@ -2,70 +2,54 @@ #include "ui_CheckBoxListDialog.h" -#include -#include -#include - namespace fso::fred::dialogs { CheckBoxListDialog::CheckBoxListDialog(QWidget* parent) : QDialog(parent), ui(new Ui::CheckBoxListDialog) { ui->setupUi(this); - - // Allow resizing - this->setSizeGripEnabled(true); - - // clear placeholder layout contents if any - if (ui->checkboxContainer->layout()) { - QLayoutItem* item; - while ((item = ui->checkboxContainer->layout()->takeAt(0)) != nullptr) { - delete item->widget(); - delete item; - } - delete ui->checkboxContainer->layout(); - } - - // Set a fresh layout - auto* layout = new QVBoxLayout(ui->checkboxContainer); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(4); + setSizeGripEnabled(true); } void CheckBoxListDialog::setCaption(const QString& text) { - this->setWindowTitle(text); + setWindowTitle(text); } void CheckBoxListDialog::setOptions(const QVector>& options) { - // Clear previous checkboxes - for (auto* cb : _checkboxes) { - cb->deleteLater(); - } - _checkboxes.clear(); + QVector> intOptions; + intOptions.reserve(options.size()); + for (const auto& [name, checked] : options) + intOptions.append({name, checked ? Qt::Checked : Qt::Unchecked}); + ui->flagList->setFlags(intOptions); +} + +void CheckBoxListDialog::setOptions(const QVector>& options) +{ + ui->flagList->setFlags(options); +} - auto* layout = qobject_cast(ui->checkboxContainer->layout()); - if (!layout) { - return; - } +void CheckBoxListDialog::setOptionDescriptions(const QVector>& descriptions) +{ + ui->flagList->setFlagDescriptions(descriptions); +} - for (const auto& [label, checked] : options) { - auto* cb = new QCheckBox(label, this); - cb->setChecked(checked); - layout->addWidget(cb); - _checkboxes.append(cb); - } - // Add spacer to push items to top - layout->addStretch(); +void CheckBoxListDialog::setTristate(bool tristate) +{ + ui->flagList->setTristate(tristate); } QVector CheckBoxListDialog::getCheckedStates() const { QVector states; - for (auto* cb : _checkboxes) { - states.append(cb->isChecked()); - } + for (const auto& [name, state] : ui->flagList->getFlags()) + states.append(state == Qt::Checked); return states; } +QVector> CheckBoxListDialog::getFlags() const +{ + return ui->flagList->getFlags(); +} + } // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/General/CheckBoxListDialog.h b/qtfred/src/ui/dialogs/General/CheckBoxListDialog.h index 57273e4da38..ac6abbaeb3d 100644 --- a/qtfred/src/ui/dialogs/General/CheckBoxListDialog.h +++ b/qtfred/src/ui/dialogs/General/CheckBoxListDialog.h @@ -1,8 +1,10 @@ #pragma once +#include #include -#include +#include +#include namespace fso::fred::dialogs { @@ -16,12 +18,20 @@ class CheckBoxListDialog : public QDialog { explicit CheckBoxListDialog(QWidget* parent = nullptr); void setCaption(const QString& text); + + // Binary mode (backwards-compatible) void setOptions(const QVector>& options); QVector getCheckedStates() const; + // Tristate / int-state mode + void setOptions(const QVector>& options); + QVector> getFlags() const; + + void setOptionDescriptions(const QVector>& descriptions); + void setTristate(bool tristate); + private: Ui::CheckBoxListDialog* ui; - QVector _checkboxes; }; -} // namespace fso::fred::dialogs \ No newline at end of file +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp index efaf834739e..63181230927 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp @@ -655,29 +655,20 @@ void ShipEditorDialog::on_weaponsButton_clicked() } void ShipEditorDialog::on_playerOrdersButton_clicked() { - CheckBoxListDialog dlg(this); - dlg.setCaption("Player Orders Accepted"); - // Get our flag list and convert it to Qt's internal types - auto playerOrders = _model->getAcceptedOrders(); + QVector> toWidget; + for (const auto& p : _model->getPlayerOrders()) + toWidget.append({QString::fromUtf8(p.first.c_str()), p.second}); - QVector> checkbox_list; + dialogs::CheckBoxListDialog dlg(this); + dlg.setCaption(tr("Player Orders Accepted")); + dlg.setTristate(true); + dlg.setOptions(toWidget); - for (const auto& porder : playerOrders) { - checkbox_list.append({porder.first.c_str(), porder.second}); - } - dlg.setOptions(checkbox_list); // TODO upgrade checkbox to accept and display item descriptions if (dlg.exec() == QDialog::Accepted) { - auto returned_values = dlg.getCheckedStates(); - - std::vector> updatedOrders; - - for (int i = 0; i < checkbox_list.size(); ++i) { - // Convert back to std::string - std::string name = checkbox_list[i].first.toUtf8().constData(); - updatedOrders.emplace_back(name, returned_values[i]); - } - - _model->setAcceptedOrders(updatedOrders); + SCP_vector> orders; + for (const auto& [name, state] : dlg.getFlags()) + orders.emplace_back(name.toUtf8().constData(), state); + _model->applyPlayerOrders(orders); } } void ShipEditorDialog::on_specialStatsButton_clicked() diff --git a/qtfred/src/ui/dialogs/WingEditorDialog.cpp b/qtfred/src/ui/dialogs/WingEditorDialog.cpp index 0eb9192fece..060bd86def1 100644 --- a/qtfred/src/ui/dialogs/WingEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/WingEditorDialog.cpp @@ -4,7 +4,6 @@ #include "General/ImagePickerDialog.h" #include "ShipEditor/ShipGoalsDialog.h" #include "ShipEditor/ShipCustomWarpDialog.h" -#include "WingFlagsDialog.h" #include "ui_WingEditorDialog.h" @@ -477,9 +476,24 @@ void WingEditorDialog::on_initialOrdersButton_clicked() void WingEditorDialog::on_wingFlagsButton_clicked() { - WingFlagsDialog dlg(this, _model->getWingFlags(), _model->getWingFlagDescriptions()); + QVector> qtFlags; + for (const auto& f : _model->getWingFlags()) + qtFlags.append({QString::fromUtf8(f.first.c_str()), f.second ? Qt::Checked : Qt::Unchecked}); + + QVector> qtDescs; + for (const auto& d : _model->getWingFlagDescriptions()) + qtDescs.append({QString::fromUtf8(d.first.c_str()), QString::fromUtf8(d.second.c_str())}); + + dialogs::CheckBoxListDialog dlg(this); + dlg.setCaption(tr("Wing Flags")); + dlg.setOptions(qtFlags); + dlg.setOptionDescriptions(qtDescs); + if (dlg.exec() == QDialog::Accepted) { - _model->setWingFlags(dlg.getFlags()); + std::vector> result; + for (const auto& f : dlg.getFlags()) + result.emplace_back(f.first.toUtf8().constData(), f.second == Qt::Checked); + _model->setWingFlags(result); } } diff --git a/qtfred/src/ui/dialogs/WingFlagsDialog.cpp b/qtfred/src/ui/dialogs/WingFlagsDialog.cpp deleted file mode 100644 index 389194e486e..00000000000 --- a/qtfred/src/ui/dialogs/WingFlagsDialog.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "WingFlagsDialog.h" - -#include "ui_WingFlagsDialog.h" - -#include - -namespace fso::fred::dialogs { - -WingFlagsDialog::WingFlagsDialog(QWidget* parent, const std::vector>& flags, - const std::vector>& descriptions) - : QDialog(parent), ui(new Ui::WingFlagsDialog()) -{ - ui->setupUi(this); - - QVector> qtFlags; - qtFlags.reserve(static_cast(flags.size())); - for (const auto& f : flags) - qtFlags.append({QString::fromUtf8(f.first.c_str()), f.second ? Qt::Checked : Qt::Unchecked}); - ui->flagList->setFlags(qtFlags); - - if (!descriptions.empty()) { - QVector> qtDescs; - qtDescs.reserve(static_cast(descriptions.size())); - for (const auto& d : descriptions) - qtDescs.append({QString::fromUtf8(d.first.c_str()), QString::fromUtf8(d.second.c_str())}); - ui->flagList->setFlagDescriptions(qtDescs); - } - - resize(QDialog::sizeHint()); -} - -WingFlagsDialog::~WingFlagsDialog() = default; - -std::vector> WingFlagsDialog::getFlags() const -{ - std::vector> result; - for (const auto& f : ui->flagList->getFlags()) - result.emplace_back(f.first.toUtf8().constData(), f.second == Qt::Checked); - return result; -} - -} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/WingFlagsDialog.h b/qtfred/src/ui/dialogs/WingFlagsDialog.h deleted file mode 100644 index 8a5924c6a19..00000000000 --- a/qtfred/src/ui/dialogs/WingFlagsDialog.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include - -namespace fso::fred::dialogs { - -namespace Ui { -class WingFlagsDialog; -} - -class WingFlagsDialog : public QDialog { - Q_OBJECT - public: - explicit WingFlagsDialog(QWidget* parent, const std::vector>& flags, - const std::vector>& descriptions = {}); - ~WingFlagsDialog() override; - - std::vector> getFlags() const; - - private: - std::unique_ptr ui; -}; - -} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/widgets/FlagList.cpp b/qtfred/src/ui/widgets/FlagList.cpp index d954dc34df8..45875385191 100644 --- a/qtfred/src/ui/widgets/FlagList.cpp +++ b/qtfred/src/ui/widgets/FlagList.cpp @@ -117,6 +117,8 @@ void FlagListWidget::rebuildModel(const QVector>& flags) auto* item = new QStandardItem(name); item->setCheckable(true); + if (_tristate) + item->setUserTristate(true); item->setCheckState(Qt::CheckState(checked)); item->setData(name, KeyRole); @@ -187,6 +189,16 @@ bool FlagListWidget::toolbarVisible() const return _toolbarVisible; } +void FlagListWidget::setTristate(bool tristate) +{ + _tristate = tristate; +} + +bool FlagListWidget::tristate() const +{ + return _tristate; +} + void FlagListWidget::onItemChanged(QStandardItem* item) { if (_updating || !item) diff --git a/qtfred/src/ui/widgets/FlagList.h b/qtfred/src/ui/widgets/FlagList.h index e681a8e5f4f..9372ebfde85 100644 --- a/qtfred/src/ui/widgets/FlagList.h +++ b/qtfred/src/ui/widgets/FlagList.h @@ -20,6 +20,7 @@ class FlagListWidget final : public QWidget { Q_OBJECT Q_PROPERTY(bool filterVisible READ filterVisible WRITE setFilterVisible) Q_PROPERTY(bool toolbarVisible READ toolbarVisible WRITE setToolbarVisible) + Q_PROPERTY(bool tristate READ tristate WRITE setTristate) public: explicit FlagListWidget(QWidget* parent = nullptr); @@ -40,6 +41,9 @@ class FlagListWidget final : public QWidget { void setToolbarVisible(bool visible); bool toolbarVisible() const; + void setTristate(bool tristate); + bool tristate() const; + // Clear all items void clear(); @@ -78,6 +82,7 @@ class FlagListWidget final : public QWidget { bool _updating = false; // guards against emitting signals during programmatic changes bool _filterVisible = true; bool _toolbarVisible = true; + bool _tristate = false; }; } // namespace fso::fred \ No newline at end of file diff --git a/qtfred/ui/CheckBoxListDialog.ui b/qtfred/ui/CheckBoxListDialog.ui index deff328f2d9..9756b2a2a4e 100644 --- a/qtfred/ui/CheckBoxListDialog.ui +++ b/qtfred/ui/CheckBoxListDialog.ui @@ -6,30 +6,22 @@ 0 0 - 217 - 294 + 300 + 400 Select Options - + - - - true + + + + 0 + 0 + - - - - 0 - 0 - 197 - 245 - - - - @@ -41,7 +33,17 @@ - + + + fso::fred::FlagListWidget + QWidget +
ui/widgets/FlagList.h
+ 1 +
+
+ + + buttonBox diff --git a/qtfred/ui/WingFlagsDialog.ui b/qtfred/ui/WingFlagsDialog.ui deleted file mode 100644 index eb8a4e33943..00000000000 --- a/qtfred/ui/WingFlagsDialog.ui +++ /dev/null @@ -1,90 +0,0 @@ - - - fso::fred::dialogs::WingFlagsDialog - - - - 0 - 0 - 350 - 500 - - - - Wing Flags - - - true - - - - - - - 0 - 0 - - - - true - - - true - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - fso::fred::FlagListWidget - QWidget -
ui/widgets/FlagList.h
- 1 -
-
- - - - - - buttonBox - accepted() - fso::fred::dialogs::WingFlagsDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - fso::fred::dialogs::WingFlagsDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - -
From 636390b727261e60c30c6b99b27b24e6ade19885 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Fri, 1 May 2026 07:37:12 -0500 Subject: [PATCH 11/65] Fix assert for flag list and ship starts (#7415) --- .../dialogs/ShipEditor/ShipFlagsDialogModel.cpp | 10 +++++++++- .../ui/dialogs/ShipEditor/ShipFlagsDialog.cpp | 17 ++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.cpp index f2055ec55c3..bab3eef9f0b 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.cpp @@ -31,7 +31,15 @@ std::pair* ShipFlagsDialogModel::getFlag(const SCP_string& flag return &flag; } } - Assertion(false, "Illegal flag name \"[%s]\"", flag_name.c_str()); + // Only assert if the name isn't a known flag at all; it may have been legitimately filtered out for ship starts + bool known = false; + for (size_t i = 0; i < Num_Parse_ship_flags && !known; ++i) + known = !stricmp(flag_name.c_str(), Parse_ship_flags[i].name); + for (size_t i = 0; i < Num_Parse_ship_ai_flags && !known; ++i) + known = !stricmp(flag_name.c_str(), Parse_ship_ai_flags[i].name); + for (size_t i = 0; i < Num_Parse_ship_object_flags && !known; ++i) + known = !stricmp(flag_name.c_str(), Parse_ship_object_flags[i].name); + Assertion(known, "Illegal flag name \"%s\"", flag_name.c_str()); return nullptr; } diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.cpp index 1c98bb1ff15..cf46520f126 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.cpp @@ -96,16 +96,19 @@ void ShipFlagsDialog::updateUI() { util::SignalBlockers blockers(this); ui->destroySecondsSpinBox->setValue(_model->getDestroyTime()); - ui->destroyedlabel->setVisible(_model->getFlag("Destroy before Mission")->second); - ui->destroySecondsSpinBox->setVisible(_model->getFlag("Destroy before Mission")->second); - ui->destroySecondsLabel->setVisible(_model->getFlag("Destroy before Mission")->second); + auto* destroyFlag = _model->getFlag("Destroy before Mission"); + ui->destroyedlabel->setVisible(destroyFlag && destroyFlag->second); + ui->destroySecondsSpinBox->setVisible(destroyFlag && destroyFlag->second); + ui->destroySecondsLabel->setVisible(destroyFlag && destroyFlag->second); + auto* escortFlag = _model->getFlag("escort"); ui->escortPrioritySpinBox->setValue(_model->getEscortPriority()); - ui->escortLabel->setVisible(_model->getFlag("escort")->second); - ui->escortPrioritySpinBox->setVisible(_model->getFlag("escort")->second); + ui->escortLabel->setVisible(escortFlag && escortFlag->second); + ui->escortPrioritySpinBox->setVisible(escortFlag && escortFlag->second); + auto* kamikazeFlag = _model->getFlag("kamikaze"); ui->kamikazeDamageSpinBox->setValue(_model->getKamikazeDamage()); - ui->kamikazeLabel->setVisible(_model->getFlag("kamikaze")->second); - ui->kamikazeDamageSpinBox->setVisible(_model->getFlag("kamikaze")->second); + ui->kamikazeLabel->setVisible(kamikazeFlag && kamikazeFlag->second); + ui->kamikazeDamageSpinBox->setVisible(kamikazeFlag && kamikazeFlag->second); } } // namespace fso::fred::dialogs \ No newline at end of file From 0e43b1970c82abb29873a272bffb4cac2b213f79 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Fri, 1 May 2026 19:56:09 +0100 Subject: [PATCH 12/65] Fix Heap Alloction issues in QTFred (#7334) * Build using /MD flag if building QTFred and on windows Fixes Multiple QTFred heap crashes due to mismatched runtime types. * Actually Change Right value --- cmake/toolchain-msvc.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/toolchain-msvc.cmake b/cmake/toolchain-msvc.cmake index 12b1c43dc32..ae4fd67ad44 100644 --- a/cmake/toolchain-msvc.cmake +++ b/cmake/toolchain-msvc.cmake @@ -71,7 +71,7 @@ if (MSVC_RELEASE_DEBUGGING) endif() endif() -IF(MSVC_USE_RUNTIME_DLL) +IF(MSVC_USE_RUNTIME_DLL OR FSO_BUILD_QTFRED) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$>:Debug>DLL") add_compile_definitions(_AFXDLL) ELSE(MSVC_USE_RUNTIME_DLL) From 4416dfc3b0e696df1f4066d977522c406ef6df25 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 2 May 2026 11:03:20 -0500 Subject: [PATCH 13/65] make sure we don't set modified during init (#7417) --- qtfred/src/mission/Editor.cpp | 6 ++---- .../dialogs/AsteroidEditorDialogModel.cpp | 2 +- .../dialogs/BackgroundEditorDialogModel.cpp | 6 ++++++ .../dialogs/BackgroundEditorDialogModel.h | 1 + .../dialogs/BriefingEditorDialogModel.cpp | 1 + .../dialogs/CommandBriefingDialogModel.cpp | 1 + .../mission/dialogs/DebriefingDialogModel.cpp | 1 + .../dialogs/FictionViewerDialogModel.cpp | 3 ++- .../dialogs/JumpNodeEditorDialogModel.cpp | 1 + .../dialogs/MissionCutscenesDialogModel.cpp | 1 + .../dialogs/MissionEventsDialogModel.cpp | 1 + .../dialogs/MissionGoalsDialogModel.cpp | 1 + .../dialogs/MissionSpecDialogModel.cpp | 1 + .../dialogs/ObjectOrientEditorDialogModel.cpp | 1 + .../mission/dialogs/PropEditorDialogModel.cpp | 1 + .../ReinforcementsEditorDialogModel.cpp | 1 + .../dialogs/ShieldSystemDialogModel.cpp | 1 + .../ShipEditor/ShipAltShipClassModel.cpp | 1 + .../ShipEditor/ShipCustomWarpDialogModel.cpp | 1 + .../ShipEditor/ShipEditorDialogModel.cpp | 1 + .../ShipEditor/ShipFlagsDialogModel.cpp | 1 + .../ShipEditor/ShipGoalsDialogModel.cpp | 1 + .../ShipInitialStatusDialogModel.cpp | 1 + .../ShipSpecialStatsDialogModel.cpp | 1 + .../ShipTextureReplacementDialogModel.cpp | 1 + .../ShipEditor/ShipWeaponsDialogModel.cpp | 1 + .../src/mission/dialogs/TableViewerModel.cpp | 1 + .../dialogs/TeamLoadoutDialogModel.cpp | 1 + .../mission/dialogs/VariableDialogModel.cpp | 1 + .../dialogs/VoiceActingManagerModel.cpp | 1 + .../dialogs/VolumetricNebulaDialogModel.cpp | 1 + .../dialogs/WaypointEditorDialogModel.cpp | 1 + .../mission/dialogs/WingEditorDialogModel.cpp | 21 ++++++++++++------- .../mission/dialogs/WingEditorDialogModel.h | 1 + 34 files changed, 53 insertions(+), 14 deletions(-) diff --git a/qtfred/src/mission/Editor.cpp b/qtfred/src/mission/Editor.cpp index a24c5fd8e49..2f148873679 100644 --- a/qtfred/src/mission/Editor.cpp +++ b/qtfred/src/mission/Editor.cpp @@ -522,7 +522,7 @@ void Editor::unmark_all() { numMarked = 0; setupCurrentObjectIndices(-1); - missionChanged(); + updateAllViewports(); } } void Editor::markObject(int obj) { @@ -561,7 +561,7 @@ void Editor::unmarkObject(int obj) { setupCurrentObjectIndices(-1); // can't find one; nothing is marked. } - missionChanged(); + updateAllViewports(); } } @@ -755,8 +755,6 @@ void Editor::selectObject(int objId) { } setupCurrentObjectIndices(objId); // select the new object - - missionChanged(); } void Editor::updateAllViewports() { // This takes all renderers and issues an update request for each of them. For now that is only one but this allows diff --git a/qtfred/src/mission/dialogs/AsteroidEditorDialogModel.cpp b/qtfred/src/mission/dialogs/AsteroidEditorDialogModel.cpp index 4178e8c5412..3cbda7a44e8 100644 --- a/qtfred/src/mission/dialogs/AsteroidEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/AsteroidEditorDialogModel.cpp @@ -95,7 +95,7 @@ void AsteroidEditorDialogModel::initializeData() debrisOptions.emplace_back(std::make_pair(Asteroid_info[i].name, static_cast(i))); } } - + _modified = false; } void AsteroidEditorDialogModel::update_internal_field() diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp index 342f4205c13..74cdcc6e7c1 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -15,6 +15,11 @@ extern void parse_one_background(background_t* background); namespace fso::fred::dialogs { BackgroundEditorDialogModel::BackgroundEditorDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) +{ + initializeData(); +} + +void BackgroundEditorDialogModel::initializeData() { auto& bg = getActiveBackground(); auto& bm_list = bg.bitmaps; @@ -26,6 +31,7 @@ BackgroundEditorDialogModel::BackgroundEditorDialogModel(QObject* parent, Editor if (!sun_list.empty()) { _selectedSunIndex = 0; } + _modified = false; } bool BackgroundEditorDialogModel::apply() diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h index 24308687863..ce8a7aecd11 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h @@ -170,6 +170,7 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { void setLightingProfileName(const SCP_string& name); private: + void initializeData(); void refreshBackgroundPreview(); static background_t& getActiveBackground(); starfield_list_entry* getActiveBitmap() const; diff --git a/qtfred/src/mission/dialogs/BriefingEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BriefingEditorDialogModel.cpp index 87e121b7f34..4cb2652dc97 100644 --- a/qtfred/src/mission/dialogs/BriefingEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/BriefingEditorDialogModel.cpp @@ -126,6 +126,7 @@ void BriefingEditorDialogModel::initializeData() _currentTeam = 0; _currentStage = 0; _currentIcon = -1; + _modified = false; } void BriefingEditorDialogModel::stopSpeech() diff --git a/qtfred/src/mission/dialogs/CommandBriefingDialogModel.cpp b/qtfred/src/mission/dialogs/CommandBriefingDialogModel.cpp index 04260e46a52..28144d30f78 100644 --- a/qtfred/src/mission/dialogs/CommandBriefingDialogModel.cpp +++ b/qtfred/src/mission/dialogs/CommandBriefingDialogModel.cpp @@ -39,6 +39,7 @@ void CommandBriefingDialogModel::initializeData() _currentTeam = 0; // default to the first team _currentStage = 0; // default to the first stage + _modified = false; } void CommandBriefingDialogModel::gotoPreviousStage() diff --git a/qtfred/src/mission/dialogs/DebriefingDialogModel.cpp b/qtfred/src/mission/dialogs/DebriefingDialogModel.cpp index 9392961a7d9..0ad1cd89252 100644 --- a/qtfred/src/mission/dialogs/DebriefingDialogModel.cpp +++ b/qtfred/src/mission/dialogs/DebriefingDialogModel.cpp @@ -48,6 +48,7 @@ void DebriefingDialogModel::initializeData() _currentTeam = 0; _currentStage = 0; + _modified = false; } void DebriefingDialogModel::gotoPreviousStage() diff --git a/qtfred/src/mission/dialogs/FictionViewerDialogModel.cpp b/qtfred/src/mission/dialogs/FictionViewerDialogModel.cpp index f02f91c6255..f8fede9b783 100644 --- a/qtfred/src/mission/dialogs/FictionViewerDialogModel.cpp +++ b/qtfred/src/mission/dialogs/FictionViewerDialogModel.cpp @@ -19,7 +19,7 @@ bool FictionViewerDialogModel::apply() { _fictionViewerStages.clear(); Mission_music[SCORE_FICTION_VIEWER] = -1; } else { - // Keep whatever you’ve edited in _fictionViewerStages + // Keep whatever you've edited in _fictionViewerStages Mission_music[SCORE_FICTION_VIEWER] = _fictionMusic; // -1 for none is valid } @@ -52,6 +52,7 @@ void FictionViewerDialogModel::initializeData() { // music is managed through the mission _fictionMusic = Mission_music[SCORE_FICTION_VIEWER]; + _modified = false; } const SCP_vector>& FictionViewerDialogModel::getMusicOptions() diff --git a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp index f2ba413dab1..926c2f4b74c 100644 --- a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp @@ -117,6 +117,7 @@ void JumpNodeEditorDialogModel::initializeData() } Q_EMIT jumpNodeMarkingChanged(); + _modified = false; } void JumpNodeEditorDialogModel::buildNodeList() diff --git a/qtfred/src/mission/dialogs/MissionCutscenesDialogModel.cpp b/qtfred/src/mission/dialogs/MissionCutscenesDialogModel.cpp index 5815292dbaa..220676e57f2 100644 --- a/qtfred/src/mission/dialogs/MissionCutscenesDialogModel.cpp +++ b/qtfred/src/mission/dialogs/MissionCutscenesDialogModel.cpp @@ -57,6 +57,7 @@ void MissionCutscenesDialogModel::initializeData() cur_cutscene = -1; modelChanged(); + _modified = false; } SCP_vector& MissionCutscenesDialogModel::getCutscenes() { diff --git a/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp b/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp index 6065c3f0c85..23d7df99028 100644 --- a/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp @@ -115,6 +115,7 @@ void MissionEventsDialogModel::initializeData() initializeTeamList(); initializeEvents(); + _modified = false; } void MissionEventsDialogModel::initializeEvents() diff --git a/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp b/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp index ed9d5d605c0..293ccb51e8f 100644 --- a/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp @@ -85,6 +85,7 @@ void MissionGoalsDialogModel::initializeData() { cur_goal = -1; modelChanged(); + _modified = false; } SCP_vector& MissionGoalsDialogModel::getGoals() { return m_goals; diff --git a/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp b/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp index 6031d5dc60d..fa8101a3de4 100644 --- a/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp +++ b/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp @@ -91,6 +91,7 @@ void MissionSpecDialogModel::initializeData() { } modelChanged(); + _modified = false; } void MissionSpecDialogModel::prepareSquadLogoList() diff --git a/qtfred/src/mission/dialogs/ObjectOrientEditorDialogModel.cpp b/qtfred/src/mission/dialogs/ObjectOrientEditorDialogModel.cpp index ac4c3658f81..fd9e4a80e97 100644 --- a/qtfred/src/mission/dialogs/ObjectOrientEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ObjectOrientEditorDialogModel.cpp @@ -75,6 +75,7 @@ void ObjectOrientEditorDialogModel::initializeData() } modelChanged(); + _modified = false; } void ObjectOrientEditorDialogModel::updateObject(object* ptr) diff --git a/qtfred/src/mission/dialogs/PropEditorDialogModel.cpp b/qtfred/src/mission/dialogs/PropEditorDialogModel.cpp index 67b22c27738..653cd6d6b1d 100644 --- a/qtfred/src/mission/dialogs/PropEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/PropEditorDialogModel.cpp @@ -106,6 +106,7 @@ void PropEditorDialogModel::initializeData() { } Q_EMIT modelDataChanged(); + _modified = false; } bool PropEditorDialogModel::validateData() { diff --git a/qtfred/src/mission/dialogs/ReinforcementsEditorDialogModel.cpp b/qtfred/src/mission/dialogs/ReinforcementsEditorDialogModel.cpp index c51626b1bf9..2fe97baf0c2 100644 --- a/qtfred/src/mission/dialogs/ReinforcementsEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ReinforcementsEditorDialogModel.cpp @@ -76,6 +76,7 @@ void ReinforcementsDialogModel::initializeData() _selectedReinforcements.clear(); _selectedReinforcementIndices.clear(); + _modified = false; } bool ReinforcementsDialogModel::apply() diff --git a/qtfred/src/mission/dialogs/ShieldSystemDialogModel.cpp b/qtfred/src/mission/dialogs/ShieldSystemDialogModel.cpp index 07bcaebd142..1325fc77e41 100644 --- a/qtfred/src/mission/dialogs/ShieldSystemDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShieldSystemDialogModel.cpp @@ -26,6 +26,7 @@ void ShieldSystemDialogModel::initializeData() { for (const auto& iff : Iff_info) { _teamOptions.emplace_back(iff.iff_name); } + _modified = false; } bool ShieldSystemDialogModel::apply() { diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.cpp index 14d9353ccca..fb2f816bf1a 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.cpp @@ -115,6 +115,7 @@ void ShipAltShipClassModel::initializeData() } objp = GET_NEXT(objp); } + _modified = false; } } // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.cpp index 332071971f3..e9305c360fd 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.cpp @@ -247,6 +247,7 @@ void ShipCustomWarpDialogModel::initializeData() _m_player_warpout_speed = params->warpout_player_speed; } } + _modified = false; } void ShipCustomWarpDialogModel::setType(const int index) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp index ef59720bc83..ee598a313a9 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp @@ -534,6 +534,7 @@ void ShipEditorDialogModel::initializeData() } modelChanged(); + _modified = false; } std::vector> ShipEditorDialogModel::getArrivalPaths() const diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.cpp index bab3eef9f0b..2d56cdb4e7c 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.cpp @@ -361,5 +361,6 @@ void ShipFlagsDialogModel::initializeData() objp = GET_NEXT(objp); } modelChanged(); + _modified = false; } } // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.cpp index 498c7296224..d9c7b5f1759 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.cpp @@ -471,6 +471,7 @@ namespace fso { initialize_multi(); } modelChanged(); + _modified = false; } void ShipGoalsDialogModel::initialize(ai_goal* goals) { diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp index 8ff3f235b77..a648527f32a 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp @@ -290,6 +290,7 @@ void ShipInitialStatusDialogModel::initializeData(bool multi) m_velocity = BLANKFIELD; } modelChanged(); + _modified = false; } void ShipInitialStatusDialogModel::update_docking_info() diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipSpecialStatsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipSpecialStatsDialogModel.cpp index f3f82ad0292..aa1e6a60cc4 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipSpecialStatsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipSpecialStatsDialogModel.cpp @@ -96,6 +96,7 @@ namespace fso { m_special_exp = true; } modelChanged(); + _modified = false; } bool ShipSpecialStatsDialogModel::apply() diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp index 4119cf0494d..79f90eefdc7 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp @@ -184,6 +184,7 @@ namespace fso { } } modelChanged(); + _modified = false; } void ShipTextureReplacementDialogModel::initSubTypes(polymodel* model, int MapNum) { diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp index bbd03657db4..cfaff25571b 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -154,6 +154,7 @@ void ShipWeaponsDialogModel::initializeData(bool isMultiEdit) initPrimary(m_ship, true); initSecondary(m_ship, true); } + _modified = false; } void ShipWeaponsDialogModel::initPrimary(int inst, bool first) diff --git a/qtfred/src/mission/dialogs/TableViewerModel.cpp b/qtfred/src/mission/dialogs/TableViewerModel.cpp index c687c9cd742..d82c6a3facd 100644 --- a/qtfred/src/mission/dialogs/TableViewerModel.cpp +++ b/qtfred/src/mission/dialogs/TableViewerModel.cpp @@ -36,6 +36,7 @@ void TableViewerModel::initializeData(const char* table_filename, const char* mo _text = table_viewer::get_complete_table_text(table_filename, modular_pattern, msg.c_str()); } modelChanged(); + _modified = false; } SCP_string TableViewerModel::getText() const diff --git a/qtfred/src/mission/dialogs/TeamLoadoutDialogModel.cpp b/qtfred/src/mission/dialogs/TeamLoadoutDialogModel.cpp index 9776e2d1076..ee6388a4599 100644 --- a/qtfred/src/mission/dialogs/TeamLoadoutDialogModel.cpp +++ b/qtfred/src/mission/dialogs/TeamLoadoutDialogModel.cpp @@ -327,6 +327,7 @@ void TeamLoadoutDialogModel::initializeData() } } } + _modified = false; } void TeamLoadoutDialogModel::recalcShipCapacities(TeamLoadout& team) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 5276d98d259..fe04994b987 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -167,6 +167,7 @@ void VariableDialogModel::initializeData() } } } + _modified = false; } void VariableDialogModel::sortMap(int containerIndex) diff --git a/qtfred/src/mission/dialogs/VoiceActingManagerModel.cpp b/qtfred/src/mission/dialogs/VoiceActingManagerModel.cpp index 13de8b046c1..73b3432be7c 100644 --- a/qtfred/src/mission/dialogs/VoiceActingManagerModel.cpp +++ b/qtfred/src/mission/dialogs/VoiceActingManagerModel.cpp @@ -100,6 +100,7 @@ void VoiceActingManagerModel::initializeData() _suffix = Suffix::WAV; _includeSenderInFilename = false; _whichPersonaToSync = 0; + _modified = false; } SCP_vector VoiceActingManagerModel::personaChoices() diff --git a/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.cpp b/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.cpp index 6e336321380..9ce09c52034 100644 --- a/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.cpp @@ -40,6 +40,7 @@ void VolumetricNebulaDialogModel::initializeData() makeVolumetricsCopy(_volumetrics, volumetric_nebula{}); _volumetrics.enabled = false; } + _modified = false; } bool VolumetricNebulaDialogModel::validate_data() diff --git a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp index 980c536573f..48f4b95c30d 100644 --- a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp @@ -84,6 +84,7 @@ void WaypointEditorDialogModel::initializeData() } Q_EMIT waypointPathMarkingChanged(); + _modified = false; } void WaypointEditorDialogModel::updateWaypointPathList() diff --git a/qtfred/src/mission/dialogs/WingEditorDialogModel.cpp b/qtfred/src/mission/dialogs/WingEditorDialogModel.cpp index c797d9af0ea..459c9d46134 100644 --- a/qtfred/src/mission/dialogs/WingEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/WingEditorDialogModel.cpp @@ -12,13 +12,18 @@ namespace fso::fred::dialogs { WingEditorDialogModel::WingEditorDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) { - reloadFromCurWing(); - prepareSquadLogoList(); - + initializeData(); connect(_editor, &Editor::currentObjectChanged, this, &WingEditorDialogModel::onEditorSelectionChanged); connect(_editor, &Editor::missionChanged, this, &WingEditorDialogModel::onEditorMissionChanged); } +void WingEditorDialogModel::initializeData() +{ + reloadFromCurWing(); + prepareSquadLogoList(); + _modified = false; +} + void WingEditorDialogModel::onEditorSelectionChanged(int) { reloadFromCurWing(); @@ -39,15 +44,15 @@ void WingEditorDialogModel::reloadFromCurWing() _currentWingIndex = w; if (w < 0 || Wings[w].wave_count == 0) { - // No wing selected - modify(_currentWingIndex, -1); - modify(_currentWingName, SCP_string()); + // No wing selected — track view state without dirtying the model + _currentWingIndex = -1; + _currentWingName = SCP_string(); + Q_EMIT wingChanged(); return; } const auto& wing = Wings[w]; - modify(_currentWingIndex, w); - modify(_currentWingName, SCP_string(wing.name)); + _currentWingName = SCP_string(wing.name); Q_EMIT wingChanged(); } diff --git a/qtfred/src/mission/dialogs/WingEditorDialogModel.h b/qtfred/src/mission/dialogs/WingEditorDialogModel.h index 76ba442e3e1..b25195be769 100644 --- a/qtfred/src/mission/dialogs/WingEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/WingEditorDialogModel.h @@ -133,6 +133,7 @@ class WingEditorDialogModel : public AbstractDialogModel { void onEditorMissionChanged(); // missionChanged private: // NOLINT(readability-redundant-access-specifiers) + void initializeData(); void reloadFromCurWing(); wing* getCurrentWing() const; static std::vector> getDockBayPathsForWingMask(uint32_t mask, int anchorShipnum); From 1d6324475366ee91f5ffe393557e75834ce87257 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 2 May 2026 11:04:01 -0500 Subject: [PATCH 14/65] add escort to ship flags (#7404) --- code/ship/ship.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index a5eb8acb665..32454b762b0 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -667,6 +667,7 @@ ship_flag_name Ship_flag_names[] = { { Ship_Flags::Scramble_messages, "scramble-messages"}, { Ship_Flags::Maneuver_despite_engines, "maneuver-despite-engines" }, { Ship_Flags::No_scanned_cargo, "no-scanned-cargo"}, + { Ship_Flags::Escort, "escort" }, { Ship_Flags::EMP_doesnt_scramble_messages, "emp-doesn't-scramble-messages" }, }; From d8131ab90102c6588ac5470a6b59ec27e7e44d1d Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sun, 3 May 2026 11:26:06 -0500 Subject: [PATCH 15/65] QtFRED help dialog and documentation (#7380) * help dialog and documentation * compile help in headless mode * scene browser help file * attempt to get CI working for linux * debug attempt * Fix split method to use QString::SkipEmptyParts * Revert "debug attempt" This reverts commit cb046e1b3305577ff50e95d5b748f6d1dbf025fb. * clang * update help * correct name * Inject sexps.html into help and add find in page controls * update help based on recent commits * better tutorial asset loading * no const * update help again --- code/cfile/cfile.cpp | 2 +- qtfred/CMakeLists.txt | 77 +++- qtfred/help-src/doc/concepts/ai.html | 62 +++ .../help-src/doc/concepts/campaign-flow.html | 65 +++ qtfred/help-src/doc/concepts/custom-data.html | 14 + qtfred/help-src/doc/concepts/iff.html | 59 +++ qtfred/help-src/doc/concepts/messages.html | 56 +++ qtfred/help-src/doc/concepts/sexps.html | 53 +++ qtfred/help-src/doc/css/help.css | 69 +++ .../doc/dialogs/AsteroidEditorDialog.html | 61 +++ .../doc/dialogs/BackgroundEditorDialog.html | 128 ++++++ .../doc/dialogs/BriefingEditorDialog.html | 123 ++++++ .../doc/dialogs/CampaignEditorDialog.html | 109 +++++ .../doc/dialogs/CommandBriefingDialog.html | 52 +++ .../doc/dialogs/CustomDataDialog.html | 40 ++ .../doc/dialogs/CustomStringsDialog.html | 35 ++ .../doc/dialogs/CustomWingNamesDialog.html | 32 ++ .../doc/dialogs/DebriefingDialog.html | 58 +++ .../doc/dialogs/ErrorCheckerDialog.html | 14 + qtfred/help-src/doc/dialogs/EventEditor.html | 15 + .../doc/dialogs/FictionViewerDialog.html | 30 ++ .../doc/dialogs/GlobalShipFlagsDialog.html | 19 + .../doc/dialogs/JumpNodeEditorDialog.html | 34 ++ .../doc/dialogs/MissionCutscenesDialog.html | 46 ++ .../doc/dialogs/MissionEventsDialog.html | 136 ++++++ .../doc/dialogs/MissionGoalsDialog.html | 78 ++++ .../doc/dialogs/MissionSpecsDialog.html | 144 ++++++ .../doc/dialogs/MissionStatsDialog.html | 42 ++ .../doc/dialogs/MusicPlayerDialog.html | 31 ++ .../doc/dialogs/ObjectOrientEditorDialog.html | 59 +++ .../doc/dialogs/PreferencesDialog.html | 43 ++ .../doc/dialogs/PropEditorDialog.html | 31 ++ .../dialogs/ReinforcementsEditorDialog.html | 39 ++ .../dialogs/RelativeCoordinatesDialog.html | 21 + .../doc/dialogs/ShieldSystemDialog.html | 39 ++ .../doc/dialogs/ShipAltShipClass.html | 40 ++ .../doc/dialogs/ShipCustomWarpDialog.html | 52 +++ .../doc/dialogs/ShipEditorDialog.html | 148 +++++++ .../help-src/doc/dialogs/ShipFlagsDialog.html | 18 + .../doc/dialogs/ShipInitialOrdersDialog.html | 33 ++ .../doc/dialogs/ShipInitialStatusDialog.html | 76 ++++ .../doc/dialogs/ShipPlayerOrdersDialog.html | 15 + .../doc/dialogs/ShipSpecialStatsDialog.html | 51 +++ .../dialogs/ShipTextureReplacementDialog.html | 36 ++ .../doc/dialogs/ShipWeaponsDialog.html | 31 ++ .../doc/dialogs/SoundEnvironmentDialog.html | 29 ++ .../doc/dialogs/TeamLoadoutDialog.html | 86 ++++ .../dialogs/VariablesAndContainersDialog.html | 84 ++++ .../doc/dialogs/VoiceActingManager.html | 94 ++++ .../doc/dialogs/VolumetricNebulaDialog.html | 96 ++++ .../doc/dialogs/WaypointEditorDialog.html | 37 ++ .../dialogs/WaypointPathGeneratorDialog.html | 43 ++ .../doc/dialogs/WingEditorDialog.html | 111 +++++ qtfred/help-src/doc/fundamentals.html | 187 ++++++++ .../doc/general/LayerManagerDialog.html | 31 ++ .../doc/general/PreferencesDialog.html | 29 ++ .../doc/general/SceneBrowserDialog.html | 45 ++ qtfred/help-src/doc/general/Toolbars.html | 347 +++++++++++++++ qtfred/help-src/doc/general/Viewport.html | 38 ++ qtfred/help-src/doc/index.html | 90 ++++ qtfred/help-src/doc/qtfred.qhp | 208 +++++++++ qtfred/help-src/doc/quickstart.html | 67 +++ qtfred/help-src/qtfred.qhp | 205 +++++++++ qtfred/help-src/qtfred_help_resources.qrc.in | 5 + qtfred/help-src/tutorial-template.html | 137 ++++++ qtfred/source_groups.cmake | 5 + qtfred/src/main.cpp | 5 + .../mission/dialogs/HelpTopicsDialogModel.cpp | 247 +++++++++++ .../mission/dialogs/HelpTopicsDialogModel.h | 63 +++ qtfred/src/ui/FredView.cpp | 13 + qtfred/src/ui/FredView.h | 1 + qtfred/src/ui/dialogs/HelpTopicsDialog.cpp | 416 ++++++++++++++++++ qtfred/src/ui/dialogs/HelpTopicsDialog.h | 77 ++++ qtfred/ui/HelpTopicsDialog.ui | 142 ++++++ 74 files changed, 5351 insertions(+), 3 deletions(-) create mode 100644 qtfred/help-src/doc/concepts/ai.html create mode 100644 qtfred/help-src/doc/concepts/campaign-flow.html create mode 100644 qtfred/help-src/doc/concepts/custom-data.html create mode 100644 qtfred/help-src/doc/concepts/iff.html create mode 100644 qtfred/help-src/doc/concepts/messages.html create mode 100644 qtfred/help-src/doc/concepts/sexps.html create mode 100644 qtfred/help-src/doc/css/help.css create mode 100644 qtfred/help-src/doc/dialogs/AsteroidEditorDialog.html create mode 100644 qtfred/help-src/doc/dialogs/BackgroundEditorDialog.html create mode 100644 qtfred/help-src/doc/dialogs/BriefingEditorDialog.html create mode 100644 qtfred/help-src/doc/dialogs/CampaignEditorDialog.html create mode 100644 qtfred/help-src/doc/dialogs/CommandBriefingDialog.html create mode 100644 qtfred/help-src/doc/dialogs/CustomDataDialog.html create mode 100644 qtfred/help-src/doc/dialogs/CustomStringsDialog.html create mode 100644 qtfred/help-src/doc/dialogs/CustomWingNamesDialog.html create mode 100644 qtfred/help-src/doc/dialogs/DebriefingDialog.html create mode 100644 qtfred/help-src/doc/dialogs/ErrorCheckerDialog.html create mode 100644 qtfred/help-src/doc/dialogs/EventEditor.html create mode 100644 qtfred/help-src/doc/dialogs/FictionViewerDialog.html create mode 100644 qtfred/help-src/doc/dialogs/GlobalShipFlagsDialog.html create mode 100644 qtfred/help-src/doc/dialogs/JumpNodeEditorDialog.html create mode 100644 qtfred/help-src/doc/dialogs/MissionCutscenesDialog.html create mode 100644 qtfred/help-src/doc/dialogs/MissionEventsDialog.html create mode 100644 qtfred/help-src/doc/dialogs/MissionGoalsDialog.html create mode 100644 qtfred/help-src/doc/dialogs/MissionSpecsDialog.html create mode 100644 qtfred/help-src/doc/dialogs/MissionStatsDialog.html create mode 100644 qtfred/help-src/doc/dialogs/MusicPlayerDialog.html create mode 100644 qtfred/help-src/doc/dialogs/ObjectOrientEditorDialog.html create mode 100644 qtfred/help-src/doc/dialogs/PreferencesDialog.html create mode 100644 qtfred/help-src/doc/dialogs/PropEditorDialog.html create mode 100644 qtfred/help-src/doc/dialogs/ReinforcementsEditorDialog.html create mode 100644 qtfred/help-src/doc/dialogs/RelativeCoordinatesDialog.html create mode 100644 qtfred/help-src/doc/dialogs/ShieldSystemDialog.html create mode 100644 qtfred/help-src/doc/dialogs/ShipAltShipClass.html create mode 100644 qtfred/help-src/doc/dialogs/ShipCustomWarpDialog.html create mode 100644 qtfred/help-src/doc/dialogs/ShipEditorDialog.html create mode 100644 qtfred/help-src/doc/dialogs/ShipFlagsDialog.html create mode 100644 qtfred/help-src/doc/dialogs/ShipInitialOrdersDialog.html create mode 100644 qtfred/help-src/doc/dialogs/ShipInitialStatusDialog.html create mode 100644 qtfred/help-src/doc/dialogs/ShipPlayerOrdersDialog.html create mode 100644 qtfred/help-src/doc/dialogs/ShipSpecialStatsDialog.html create mode 100644 qtfred/help-src/doc/dialogs/ShipTextureReplacementDialog.html create mode 100644 qtfred/help-src/doc/dialogs/ShipWeaponsDialog.html create mode 100644 qtfred/help-src/doc/dialogs/SoundEnvironmentDialog.html create mode 100644 qtfred/help-src/doc/dialogs/TeamLoadoutDialog.html create mode 100644 qtfred/help-src/doc/dialogs/VariablesAndContainersDialog.html create mode 100644 qtfred/help-src/doc/dialogs/VoiceActingManager.html create mode 100644 qtfred/help-src/doc/dialogs/VolumetricNebulaDialog.html create mode 100644 qtfred/help-src/doc/dialogs/WaypointEditorDialog.html create mode 100644 qtfred/help-src/doc/dialogs/WaypointPathGeneratorDialog.html create mode 100644 qtfred/help-src/doc/dialogs/WingEditorDialog.html create mode 100644 qtfred/help-src/doc/fundamentals.html create mode 100644 qtfred/help-src/doc/general/LayerManagerDialog.html create mode 100644 qtfred/help-src/doc/general/PreferencesDialog.html create mode 100644 qtfred/help-src/doc/general/SceneBrowserDialog.html create mode 100644 qtfred/help-src/doc/general/Toolbars.html create mode 100644 qtfred/help-src/doc/general/Viewport.html create mode 100644 qtfred/help-src/doc/index.html create mode 100644 qtfred/help-src/doc/qtfred.qhp create mode 100644 qtfred/help-src/doc/quickstart.html create mode 100644 qtfred/help-src/qtfred.qhp create mode 100644 qtfred/help-src/qtfred_help_resources.qrc.in create mode 100644 qtfred/help-src/tutorial-template.html create mode 100644 qtfred/src/mission/dialogs/HelpTopicsDialogModel.cpp create mode 100644 qtfred/src/mission/dialogs/HelpTopicsDialogModel.h create mode 100644 qtfred/src/ui/dialogs/HelpTopicsDialog.cpp create mode 100644 qtfred/src/ui/dialogs/HelpTopicsDialog.h create mode 100644 qtfred/ui/HelpTopicsDialog.ui diff --git a/code/cfile/cfile.cpp b/code/cfile/cfile.cpp index 0fd43a137f5..b631ac0672a 100644 --- a/code/cfile/cfile.cpp +++ b/code/cfile/cfile.cpp @@ -85,7 +85,7 @@ cf_pathtype Pathtypes[CF_MAX_PATH_TYPES] = { { CF_TYPE_INTEL_ANIMS, "data" DIR_SEPARATOR_STR "intelanims", ".pcx .ani .eff .tga .jpg .png .dds", CF_TYPE_DATA }, { CF_TYPE_SCRIPTS, "data" DIR_SEPARATOR_STR "scripts", ".lua .lc .fnl", CF_TYPE_DATA }, { CF_TYPE_FICTION, "data" DIR_SEPARATOR_STR "fiction", ".txt", CF_TYPE_DATA }, - { CF_TYPE_FREDDOCS, "data" DIR_SEPARATOR_STR "freddocs", ".html", CF_TYPE_DATA } + { CF_TYPE_FREDDOCS, "data" DIR_SEPARATOR_STR "freddocs", ".html .qch .css .png .jpg", CF_TYPE_DATA } }; // clang-format on diff --git a/qtfred/CMakeLists.txt b/qtfred/CMakeLists.txt index 6d3211ee29f..94311a35409 100644 --- a/qtfred/CMakeLists.txt +++ b/qtfred/CMakeLists.txt @@ -8,7 +8,7 @@ SET(QT5_INSTALL_ROOT "" CACHE PATH list(APPEND CMAKE_PREFIX_PATH "${QT5_INSTALL_ROOT}") -find_package(Qt5 COMPONENTS Widgets OpenGL REQUIRED) +find_package(Qt5 COMPONENTS Widgets OpenGL Help REQUIRED) include(source_groups.cmake) @@ -56,7 +56,7 @@ set(CMAKE_MAP_IMPORTED_CONFIG_FASTDEBUG Release Debug) target_link_libraries(qtfred PUBLIC code - Qt5::Widgets Qt5::OpenGL) + Qt5::Widgets Qt5::OpenGL Qt5::Help) include(CreateLaunchers) create_target_launcher(qtfred @@ -72,6 +72,66 @@ INSTALL( ) COPY_FILES_TO_TARGET(qtfred) +# --- QtFRED built-in help --- +get_target_property(_QTFRED_QMAKE Qt5::qmake IMPORTED_LOCATION) +execute_process( + COMMAND "${_QTFRED_QMAKE}" -query QT_INSTALL_BINS + OUTPUT_VARIABLE _QTFRED_QT_BINS + OUTPUT_STRIP_TRAILING_WHITESPACE) +execute_process( + COMMAND "${_QTFRED_QMAKE}" -query QT_INSTALL_LIBS + OUTPUT_VARIABLE _QTFRED_QT_LIBS + OUTPUT_STRIP_TRAILING_WHITESPACE) +find_program(QHELPGENERATOR_EXECUTABLE + NAMES qhelpgenerator + HINTS "${_QTFRED_QT_BINS}" + REQUIRED) + +set(QTFRED_HELP_QHP "${CMAKE_CURRENT_SOURCE_DIR}/help-src/doc/qtfred.qhp") +set(QTFRED_HELP_QCH "${CMAKE_CURRENT_BINARY_DIR}/qtfred_help.qch") + +file(GLOB_RECURSE QTFRED_HELP_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/help-src/doc/*.html" + "${CMAKE_CURRENT_SOURCE_DIR}/help-src/doc/*.css" + "${CMAKE_CURRENT_SOURCE_DIR}/help-src/doc/*.qhp") + +# qhelpgenerator is a Qt application and needs a platform plugin at build time. +# On headless Linux CI the Qt platform plugins fail to dlopen because the Qt +# shared libs are not in LD_LIBRARY_PATH for dlopen-loaded plugins (DT_RUNPATH +# in the main binary does not propagate to dlopen). Pass the Qt lib dir and +# force the offscreen (no-display) platform explicitly. +add_custom_command( + OUTPUT "${QTFRED_HELP_QCH}" + COMMAND ${CMAKE_COMMAND} -E env + "QT_QPA_PLATFORM=offscreen" + "LD_LIBRARY_PATH=${_QTFRED_QT_LIBS}" + "${QHELPGENERATOR_EXECUTABLE}" "${QTFRED_HELP_QHP}" -o "${QTFRED_HELP_QCH}" + DEPENDS ${QTFRED_HELP_SOURCES} + COMMENT "Compiling QtFRED help (qtfred_help.qch)" + VERBATIM) + +add_custom_target(qtfred_help + DEPENDS "${QTFRED_HELP_QCH}" + SOURCES ${QTFRED_HELP_SOURCES}) + +add_dependencies(qtfred qtfred_help) + +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/help-src" + PREFIX "help-src" + FILES ${QTFRED_HELP_SOURCES}) + +add_custom_command(TARGET qtfred_help POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "$/help" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${QTFRED_HELP_QCH}" + "$/help/qtfred_help.qch" + VERBATIM) + +install(FILES "${QTFRED_HELP_QCH}" + DESTINATION ${BINARY_DESTINATION}/help + COMPONENT "qtFRED") +# --- end QtFRED built-in help --- + enable_clang_tidy(qtfred) if (WIN32) @@ -120,6 +180,19 @@ if (WIN32) DESTINATION ${BINARY_DESTINATION}/platforms COMPONENT "qtFRED" ) + + # Qt Help requires the SQLite SQL driver + set(qsqlite_path "${QT_INSTALL_PLUGINS}/sqldrivers/qsqlite$<$:d>.dll") + + add_custom_command(TARGET qtfred + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${qsqlite_path}" "$/sqldrivers/qsqlite$<$:d>.dll" + VERBATIM) + + install(FILES ${qsqlite_path} + DESTINATION ${BINARY_DESTINATION}/sqldrivers + COMPONENT "qtFRED" + ) elseif(FSO_BUILD_APPIMAGE) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/AppRun.in" "${CMAKE_CURRENT_BINARY_DIR}/AppRun.gen" @ONLY) file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/AppRun-$" diff --git a/qtfred/help-src/doc/concepts/ai.html b/qtfred/help-src/doc/concepts/ai.html new file mode 100644 index 00000000000..e2364578db7 --- /dev/null +++ b/qtfred/help-src/doc/concepts/ai.html @@ -0,0 +1,62 @@ + + + + + AI - QtFRED Help + + + + +

AI

+

Ship AI behavior in FSO is controlled by three layered systems: the +AI profile, the AI class, and +AI goals and orders. Each layer handles a different aspect of +behavior and they work together at runtime.

+ +

AI profile

+

The AI profile is a mission-wide setting chosen in +Mission Specs. It is primarily a +difficulty scaling system: it defines per-difficulty-level values +for things like damage multipliers, weapon fire delays, turn time, shield recharge +rates, and aim behavior; controlling how the mission feels across the Very Easy +through Insane difficulty range. Profiles also contain a large set of behavioral +flags that toggle specific AI behaviors on or off.

+

Profiles are defined in ai_profiles.tbl and missions select from +that list; custom profiles cannot be defined inside the mission file itself. If no +profile is specified, the table's default profile is used. Most missions are fine +with the default.

+ +

AI class

+

The AI class is a named skill preset assigned to individual ships. It controls +the skill level of that specific ship within the envelope set by the profile; +how well it aims, how it maneuvers, how it uses afterburners, and so on.

+

Every ship class has a default AI class defined in its table entry. The +Ship Editor lets you override that +default for a specific ship instance, and individual turrets can be given their +own AI class in the Weapons +dialog.

+ +

AI goals and orders

+

Goals and orders are specific tasks assigned to a ship or wing: attack a +target, guard a ship, dock with a vessel, fly a waypoint path, and so on. There +are two ways to set them:

+
    +
  • Initial orders - set in the + Initial Orders + dialog. These are the tasks a ship starts the mission with.
  • +
  • Runtime goals - issued via SEXPs during the mission, + typically from events. These override or supplement initial orders.
  • +
+

When a ship has multiple active goals, it pursues them in priority order. +Priority is a value from 1 to 200 where higher values take precedence. Goals +issued by the player via the comm menu use priorities in the 90–100 range, so +goals with a priority above 89 may outrank player orders.

+ +

How the layers interact

+

Think of it as: the profile sets the rules of the game, the +class sets how skilled the ship is within those rules, and the +goals and orders tell the ship what to do. A high-skill AI +class given a low-priority goal can still be overridden by a player order; a +locked-in high-priority goal will hold regardless of player input.

+ + diff --git a/qtfred/help-src/doc/concepts/campaign-flow.html b/qtfred/help-src/doc/concepts/campaign-flow.html new file mode 100644 index 00000000000..c83471fdcfa --- /dev/null +++ b/qtfred/help-src/doc/concepts/campaign-flow.html @@ -0,0 +1,65 @@ + + + + + Campaign Flow - QtFRED Help + + + + +

Campaign Flow

+

A campaign is a sequence of missions linked together in the +Campaign Editor. The campaign +file controls which mission plays next, branches the story based on outcomes, and +carries state between missions through goals and variables.

+ +

How missions end

+

A mission ends when the player reaches the debrief screen. There are two +distinct end conditions that affect what state is saved:

+ + + + +
ConditionWhen it occursWhat is saved
Mission completedThe player accepts the debriefing after + flying the mission.Variables with Save on Mission + Completed persistence, plus all goal states.
Mission closedThe player exits the mission by any means, + including failure or replaying.Variables with Save on Mission + Close persistence.
+ +

Goal states

+

Every mission goal ends in one of three states: complete, failed, or +incomplete. The campaign SEXP tree reads these states using +goal-true and goal-false operators to decide which +mission to branch to next. This is how outcomes carry forward; whether an +important ship survived, whether a bonus objective was met, and so on.

+

Goals do not automatically determine success or failure. The campaign SEXPs +are what interpret goal states and produce outcomes. A mission where all primary +goals are complete can still branch to a "defeat" mission if the campaign logic +is written that way.

+ +

Variables across missions

+

Variables set during a mission only persist into subsequent missions if their +persistence is configured accordingly. See the +Variables & Containers +dialog for the persistence options. Variables saved to the player file +(Eternal) persist for the lifetime of the player profile, even across +different campaigns.

+ +

Branching

+

The campaign editor arranges missions as nodes connected by branches. Each +branch has a SEXP condition. After a mission completes, the engine evaluates the +outgoing branches in order and follows the first one whose condition is true. If +no branch condition is met, the player is returned to the main menu. To end the +campaign properly (playing the end cutscene and triggering campaign completion) +use the end-campaign SEXP.

+

Common branching patterns include:

+
    +
  • Checking whether a primary goal succeeded to route to a victory or + defeat mission.
  • +
  • Checking whether an optional goal was completed to unlock bonus + missions.
  • +
  • Reading a variable set earlier in the campaign to take a story + branch.
  • +
+ + diff --git a/qtfred/help-src/doc/concepts/custom-data.html b/qtfred/help-src/doc/concepts/custom-data.html new file mode 100644 index 00000000000..27660f3d1f4 --- /dev/null +++ b/qtfred/help-src/doc/concepts/custom-data.html @@ -0,0 +1,14 @@ + + + + + Custom Data - QtFRED Help + + + + +

Custom Data

+ +
Full documentation for this concept is not yet written.
+ + diff --git a/qtfred/help-src/doc/concepts/iff.html b/qtfred/help-src/doc/concepts/iff.html new file mode 100644 index 00000000000..b8abe990f7e --- /dev/null +++ b/qtfred/help-src/doc/concepts/iff.html @@ -0,0 +1,59 @@ + + + + + IFF & Teams - QtFRED Help + + + + +

IFF & Teams

+

IFF stands for Identification Friend or Foe. Every ship in a mission +belongs to a team, and every team has a defined relationship with every other team. +Those relationships determine how ships behave toward one another: whether they +attack, ignore, or treat each other as allies.

+ +

Team relationships

+

Team relationships are defined in iff_defs.tbl, not in the mission +file. The common defaults are:

+ + + + + + + +
TeamTypical role
FriendlyPlayer's side. Ships the player can target for + orders and that friendly AI protects.
HostileEnemy. Ships that actively attack Friendly + ships.
NeutralNeither side. Not attacked unless attacked + first.
UnknownUnidentified. Treated as neutral until scanned or + otherwise identified.
TraitorFormerly friendly ships that have turned hostile. + Attacked by all sides.
+

Mods can define additional teams and custom relationships between them. The +teams available in your mission depend on what iff_defs.tbl provides +for the loaded mod.

+ +

What team membership affects

+
    +
  • AI targeting - ships attack any ship they have a hostile + relationship with and avoid attacking allies.
  • +
  • Escort and guard behavior - AI set to guard a ship will + also engage enemies threatening it.
  • +
  • Comm menu - the player can only issue orders to ships on + the Friendly team.
  • +
  • HUD - target brackets, colors, and the hostile/friendly + indicators on the radar all derive from IFF relationships.
  • +
  • SEXPs — many SEXP operators accept a team argument to + act on all ships of a given team at once.
  • +
+ +

Changing team at runtime

+

A ship's team can be changed during the mission using SEXPs. This is how +defection scenarios work: reassigning a ship to the Traitor or Hostile team +immediately causes former allies to engage it.

+ +
The Team field in the Ship Editor +sets the team at mission start. Runtime changes are made through SEXPs and are +not reflected back in the editor.
+ + diff --git a/qtfred/help-src/doc/concepts/messages.html b/qtfred/help-src/doc/concepts/messages.html new file mode 100644 index 00000000000..47b11c45c4a --- /dev/null +++ b/qtfred/help-src/doc/concepts/messages.html @@ -0,0 +1,56 @@ + + + + + Messages - QtFRED Help + + + + +

Messages

+

Messages are the in-mission communications the player sees and hears. These +include wingmate callouts, command briefings, enemy taunts. Each message is +defined once in the mission's global message list and can be triggered by any event.

+ +

Message anatomy

+ + + + + + + +
FieldDescription
NameInternal identifier used to reference the message from + events and SEXPs.
TextThe text displayed in the message box.
Audio fileThe voice file played with the message.
PersonaThe voice persona associated with this message, used + to select the appropriate voice variant.
SenderThe ship that sends the message.
+ +

Priority

+

Each message has a priority - Low, Normal, or +High - that controls how it interacts with other messages playing +at the same time.

+ + + + + +
PriorityBehavior
LowQueued behind any currently playing or queued messages. + Will not interrupt anything.
NormalQueued behind messages of equal or higher priority. + Interrupts low-priority messages.
HighInterrupts any lower-priority message currently + playing.
+

Within the same priority level, messages play in the order they were +triggered.

+ +

Triggering messages

+

Messages are sent by events in the +Mission Events dialog using a +send-message SEXP. The same message can be triggered by multiple events, and the +same event can send multiple messages.

+ +

Personas

+

Personas are defined in the game tables and control the voice set used for a +message. Assigning a persona to both a message and a ship creates a consistent +"voice" for that character across the mission. A ship with a Command persona +is eligible to serve as the Command sender in +Mission Specs.

+ + diff --git a/qtfred/help-src/doc/concepts/sexps.html b/qtfred/help-src/doc/concepts/sexps.html new file mode 100644 index 00000000000..a4d22375f7d --- /dev/null +++ b/qtfred/help-src/doc/concepts/sexps.html @@ -0,0 +1,53 @@ + + + + + SEXPs - QtFRED Help + + + + +

SEXPs

+ +

A SEXP (Symbolic EXPression) is the scripting language built into +FreeSpace missions. Every condition, trigger, and action in a mission — from +checking whether a ship has been destroyed to playing a message — is expressed +as a SEXP tree.

+ +

Structure

+

SEXPs use a Lisp-style prefix notation enclosed in parentheses:

+
(operator argument1 argument2 ...)
+ +

Operators can be nested, so arguments can themselves be SEXPs:

+
(when
+    (is-destroyed-delay 0 "Epsilon 1")
+    (send-message "" "Mission-Complete" "Command")
+)
+ +

Here when is the top-level operator. Its first argument is a condition +(is-destroyed-delay) and its second argument is an action +(send-message). When the condition becomes true the action fires.

+ +

Where SEXPs appear

+

SEXPs are used in:

+
    +
  • Mission Events — the primary place to author SEXP logic; + see the Mission Events dialog.
  • +
  • Mission Goals — the completion condition for each goal; + see the Mission Goals dialog.
  • +
  • Ship arrival/departure cues — conditions on when ships + enter and leave the mission.
  • +
  • Briefing and debriefing — stage-change conditions.
  • +
+ +

Editing SEXPs in QtFRED

+

The SEXP tree editor is embedded directly in the Mission Events and Mission Goals +dialogs. Right-click any node in the tree to add, replace, or remove operators and +arguments. Hover over an operator name for a brief description of what it does.

+ +
+ Note: The full SEXP reference is maintained on the + Hard Light Productions wiki. +
+ + diff --git a/qtfred/help-src/doc/css/help.css b/qtfred/help-src/doc/css/help.css new file mode 100644 index 00000000000..a8d952e9a99 --- /dev/null +++ b/qtfred/help-src/doc/css/help.css @@ -0,0 +1,69 @@ +body { + font-family: sans-serif; + font-size: 14px; + line-height: 1.6; + max-width: 900px; + margin: 0 auto; + padding: 1em 2em; + color: #222; +} + +h1 { font-size: 1.6em; border-bottom: 2px solid #0055a5; padding-bottom: 0.2em; } +h2 { font-size: 1.25em; border-bottom: 1px solid #ccc; padding-bottom: 0.1em; margin-top: 1.5em; } +h3 { font-size: 1.05em; margin-top: 1.2em; } + +a { color: #0055a5; } +a:hover { text-decoration: underline; } + +code, kbd { + font-family: monospace; + background: #f4f4f4; + padding: 0.1em 0.3em; + border-radius: 3px; + font-size: 0.95em; +} + +pre { + background: #f4f4f4; + border: 1px solid #ddd; + padding: 0.8em 1em; + overflow-x: auto; + border-radius: 4px; +} + +table { + border-collapse: collapse; + width: 100%; + margin: 1em 0; +} + +th, td { + border: 1px solid #ccc; + padding: 0.4em 0.7em; + text-align: left; +} + +th { background: #eef2f7; } + +.note { + background: #eef6ff; + border-left: 4px solid #0055a5; + padding: 0.5em 1em; + margin: 1em 0; +} + +.stub-notice { + background: #fff8e1; + border-left: 4px solid #f0a500; + padding: 0.5em 1em; + margin: 1em 0; + font-style: italic; +} + +nav.breadcrumb { + font-size: 0.85em; + color: #666; + margin-bottom: 1em; +} + +nav.breadcrumb a { color: #0055a5; } diff --git a/qtfred/help-src/doc/dialogs/AsteroidEditorDialog.html b/qtfred/help-src/doc/dialogs/AsteroidEditorDialog.html new file mode 100644 index 00000000000..f59de99f850 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/AsteroidEditorDialog.html @@ -0,0 +1,61 @@ + + + + + Asteroid Editor - QtFRED Help + + + + +

Asteroid Editor

+

Opens via Editors › Asteroid Editor.

+

Configures the single asteroid field that can exist in a mission. The field is defined +by a rectangular outer bounding box.

+ +

Field type

+ + + + +
OptionDescription
ActiveThe field actively throws asteroids at ships. New asteroids + are aimed directly at the ships listed in the Targets + section of this editor.
PassiveAsteroids drift freely and do not target ships.
+ +

Object type

+ + + + +
OptionDescription
AsteroidSpawns large asteroids that split into medium and small + pieces when destroyed.
DebrisSpawns specific debris pieces chosen from a weighted list. + Debris does not split when destroyed.
+ +

Key fields

+ + + + + +
FieldDescription
EnabledAdds the field to the mission. Uncheck to disable + it.
NumberThe number of asteroid or debris objects present in the + field at any one time.
Avg speedBase speed of objects in the field. Each individual + object's speed is randomized around this value; the actual range depends on + the current skill level and the object's maximum speed as defined in the + table.
+ +

Outer box

+

The bounding box of the field. Set the Min and Max +coordinates on each axis to size and position the box.

+ +

Inner box

+

When enabled, defines an exclusion zone inside the outer box where objects will not +spawn. Any object that would appear inside the inner box is repositioned just outside +it.

+ +

Use Enhanced

+

Changes how the engine decides whether to spawn a new asteroid or wrap an existing +asteroid when it leaves the outer box. By default, the engine checks both the player's +view cone and a maximum distance range. When Enhanced is enabled, the distance range +check is skipped and only the view cone is used.

+ + diff --git a/qtfred/help-src/doc/dialogs/BackgroundEditorDialog.html b/qtfred/help-src/doc/dialogs/BackgroundEditorDialog.html new file mode 100644 index 00000000000..09bcbcc5dd9 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/BackgroundEditorDialog.html @@ -0,0 +1,128 @@ + + + + + Background Editor - QtFRED Help + + + + +

Background Editor

+

Opens via Editors › Background Editor.

+

Controls the visual environment that surrounds the mission: background bitmap layers, +suns, nebula, skybox, ambient lighting, and stars.

+ +

Background slots

+

A mission can define multiple backgrounds. The left drop-down selects the +active background; the one currently loaded into the scene and visible in +the viewport. Use Add and Remove to create or delete +slots. Swap exchanges all data (bitmaps, suns, flags) between the two +selected backgrounds. The Import button lets you pull backgrounds from +another mission file.

+ +

Bitmaps

+

Full-sphere background images composited behind the scene. Use Add +to open a gallery and pick a new bitmap, Change to replace the +selected one (also gallery), and Delete to remove it. You can also +use the drop-down next to the list for a quicker replacement without the gallery. +Select a bitmap in the list to edit its properties:

+ + + + + +
FieldDescription
Pitch / Bank / HeadingOrientation of the bitmap on the + sphere.
Scale X / YStretches the bitmap horizontally and + vertically.
DivisionsNumber of mesh subdivisions (1–5) used when rendering + the bitmap. Higher values improve perspective correction at the cost of + slightly more geometry. Only the vertical (Y) axis division is currently + active in the engine.
+ +

Suns

+

Works the same as Bitmaps; Add, Change (gallery), Delete, and a quick-change +drop-down. Bank is omitted since suns are circular. Select a sun in the list to +edit:

+ + + + +
FieldDescription
Pitch / HeadingPosition of the sun in the sky. This also + determines the direction of dynamic lighting cast on ships.
ScaleUniform size of the sun flare/glow image.
+ +
Save angles in correct format - a legacy checkbox +that should always be checked for new missions. It controls whether pitch/bank/heading +angles are saved using the modern coordinate convention or the older one. Unchecking +it is only relevant when maintaining compatibility with very old mission +files.
+ +

Full Nebula

+

Enables the FS2-style nebula environment. When active, the mission takes place +inside a gas cloud with fog, particle poofs, and optional lightning. This is not the +same as the volumetric nebula renderer. For that see +Volumetric Nebula.

+ + + + + + + + + + +
FieldDescription
PatternThe background texture file used for the nebula + environment.
RangeAWACS and targeting range multiplier inside the nebula. + Lower values reduce how far sensors can detect targets.
Lightning StormSelects the lightning storm type active in the + nebula, or <None> for no lightning.
PoofsA list of cloud puff particle types defined in the tables. + Check the ones you want active in the nebula.
Ship TrailsToggles engine contrail streaks left by ships + moving through the nebula clouds.
Fog Near / Far MultipliersScale the near and far fog distances. + Lower near multiplier thickens fog close to the camera; lower far + multiplier reduces maximum visibility distance.
Display background bitmaps in nebulaWhen checked, background + bitmaps defined in the Bitmaps section are still visible through the + nebula. Uncheck to let the nebula obscure them completely.
Override Fog PaletteWhen checked, uses the RGB values specified + here as the fog color instead of the nebula's default palette.
+ +

Ambient Light

+

Sets the global ambient light color applied to all ship and object surfaces. Use +the RGB sliders to tint the ambient fill.

+ +

Skybox

+

Renders a POF model as a full-environment backdrop, replacing the default +procedural starfield. Select the POF file and set Pitch / Bank / +Heading to rotate it. Leave the file field blank for no skybox.

+

The rendering flags control how the model is drawn:

+ + + + + + + + +
FlagEffect
No LightingDisables dynamic and ambient lighting on the + skybox.
All TransparentRenders the model fully transparent.
No Z-bufferDisables depth buffer writes so the skybox always + renders behind everything else.
No CullRenders both front- and back-facing polygons.
No GlowmapsDisables glow/emission map rendering on the + model.
Force ClampForces texture coordinate clamping rather than + wrapping.
+
The default flags for a new skybox are No Z-buffer, No Cull, All +Transparent, and No Lighting, which is the correct setup for most cases.
+ +

Misc

+ + + + + + +
FieldDescription
Number of StarsCount of procedurally placed background stars. + These may not be visible when a skybox is in use.
Takes place in subspaceLoads the subspace tunnel effect for the + entire mission.
Environment mapTexture used for environment-mapped reflections + and lighting on ship surfaces.
Lighting profileSelects the lighting profile from the tables to + apply to the mission.
+ +

Old Nebula

+

A legacy FS1-style nebula system. These settings are read and written for +backwards compatibility with old mission files, but the renderer that drew them was +removed.

+ + diff --git a/qtfred/help-src/doc/dialogs/BriefingEditorDialog.html b/qtfred/help-src/doc/dialogs/BriefingEditorDialog.html new file mode 100644 index 00000000000..d1555ed48ae --- /dev/null +++ b/qtfred/help-src/doc/dialogs/BriefingEditorDialog.html @@ -0,0 +1,123 @@ + + + + + Briefing Editor - QtFRED Help + + + + +

Briefing Editor

+

Opens via Editors › Briefing.

+

Sets up the pre-mission briefing shown to the player before launch. The briefing is +made up of stages; each stage has its own camera view, map icons, text, and voice file.

+ +

Map view

+

The map is a top-down 2D projection of the mission space. Its aspect ratio matches +the aspect ratio defined for the current mod. You can drag icons around directly on +the map, and use the same camera controls as the main QtFRED viewport to adjust the +view. Camera movement speed can be set via the drop-down; it only affects editing and +has no in-game effect.

+ +

Teams

+

In multiplayer missions each team can have its own independent briefing. Use the +team selector to switch which team's briefing you are editing. Copy to other +team duplicates the entire briefing (all stages) to the other team, overwriting +it.

+ +

Music

+

Music and Alt music are briefing-wide; they are +not per-stage. Music sets the track played throughout the entire briefing and the +command briefing (if present). Alt music is used instead when that track is available +in the currently loaded mod.

+ +

Stage controls

+ + + + + + +
ControlDescription
Prev / NextNavigate between stages.
Add stageAppends a new stage at the end of the briefing.
Insert stageInserts a copy of the current stage immediately + after it.
Delete stageRemoves the current stage.
+ +

Camera controls

+ + + + + + + + + +
ControlDescription
Reset cameraRestores the stage's camera to the position it had + when you first navigated to it, discarding any unsaved + changes.
Copy / PasteCopies the current stage's camera position and + orientation to the clipboard, then pastes it onto another + stage.
Camera coordsManually enter the camera position and + orientation as numeric values.
Transition timeHow long the camera takes to move to this stage's + position when advancing forward. When moving backward, the departing + stage's transition time is used instead.
Cut to next stageDisables camera movement into the next stage; + the transition is an instant cut with a static effect + instead.
Cut to previous stageSame, but applies to the transition coming + from the previous stage.
Disable grid renderingHides the map grid during this + stage.
+ +

Icon controls

+

Make icon places a blank icon on the map. Make icon from +ship lets you pick a ship from the mission and creates an icon preset to that +ship's type, a shorthand for Make Icon followed by setting the ship type +manually.

+ +

Icon properties

+ + + + + + + + + + + + + + + +
FieldDescription
IDLinks this icon to the same icon across other stages. Icons + with matching IDs are treated as the same object moving between + stages.
LabelName visible on the map.
Closeup labelName shown when the player clicks the icon to zoom + in.
Icon imageThe 2D icon graphic from + icons.tbl.
Ship type3D model shown when the player clicks the icon. If the + ship class has a special briefing icon defined, that is used instead of + the icon image.
TeamTints the icon with that team's color.
ScaleScales the icon up or down on the map.
Draw linesDraws lines between icons that are marked for + it.
Flip iconMirrors the icon horizontally.
Use cargo iconUses the cargo variant of the ship type's icon, if + one exists. Only applies to Ship Type icons.
Wing iconUses the wing variant of the ship type's icon, if one + exists. Only applies to Ship Type icons.
HighlightThe icon receives an animated highlight effect during + this stage.
Change locallyWhen checked, changes to this icon apply only to + the current stage. When unchecked, changes propagate forward to all + later stages that share the same icon ID.
+ +

Icon actions

+ + + + + +
ButtonDescription
Icon coordsSet the icon's map position as numeric + values.
DeleteRemoves the icon from this stage.
PropagateCopies this icon's current state to all following + stages that share the same icon ID.
+ +

Stage text

+

The text displayed in the briefing text box during this stage.

+ +

Voice file

+

Audio narration played during this stage.

+ +

Usage formula

+

SEXP evaluated before the briefing plays. If +true, this stage is shown; if false, it is skipped entirely.

+ + diff --git a/qtfred/help-src/doc/dialogs/CampaignEditorDialog.html b/qtfred/help-src/doc/dialogs/CampaignEditorDialog.html new file mode 100644 index 00000000000..0458c770188 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/CampaignEditorDialog.html @@ -0,0 +1,109 @@ + + + + + Campaign Editor - QtFRED Help + + + + +

Campaign Editor

+

Opens via File › Campaign Editor. The Campaign Editor opens +as its own window rather than a modal dialog.

+

Links individual missions into a branching campaign. Each mission is a node in a +graph; directed edges between nodes carry SEXP conditions that determine which mission +the player flies next.

+ +

Use retail campaign file format - saves the campaign in the older +retail-compatible format. Leave unchecked for new campaigns. +Check campaign runs the error checker against the current +campaign. Both controls are available on both tabs.

+ +

Campaign Specs tab

+ +

Identity

+ + + + + + + +
FieldDescription
NameCampaign name shown in the campaign selection + screen.
TypeSingle player, multiplayer co-op, or multiplayer team + vs. team.
DescriptionLong-form campaign description shown in the + selection screen.
Reset tech roomWhen checked, the player's tech room is cleared + at the start of the campaign and only ships and weapons unlocked during + play are available.
Custom dataOpens the Custom Data dialog for attaching arbitrary + key-value pairs to the campaign file, used by scripting and + mods.
+ +

Starting ships and weapons

+

Check which ships and weapons are available to the player at the very start of +the campaign, before any missions have been completed.

+ +

Campaign Flow tab

+ +

Mission list

+

Lists all .fs2 mission files available to add to the campaign. +Select a mission from the list then right-click in the graph view to add it as a +node.

+ +

Mission properties

+

Select a mission node in the graph to edit its properties:

+ + + + + + +
FieldDescription
Briefing cutsceneVideo played before the briefing for this + mission.
Debriefing personaIndex of the debriefing persona used after + this mission.
MainhallWhich main hall is shown between missions.
Substitute mainhallPreferred main hall to use if present in + the currently loaded mod, analogous to alt music.
+ +

Branches

+

Shows all outgoing branches from the selected mission. Each branch has a +SEXP condition; the first branch whose condition +is true is the one taken. New branches are automatically given a true +formula. Use the up/down arrows to reorder branch priority.

+ + + + + + + +
Branch typeDescription
Branch to MissionProceeds to another mission in the campaign. + The SEXP dot is black.
Branch to Campaign LoopAn optional loop the player can accept + or skip. The SEXP dot is blue.
Repeat MissionLoops back to the current mission, allowing the + player to retry it. The SEXP dot is black.
End CampaignEnds the campaign. The SEXP dot is + black.
+ +

Selecting a loop branch reveals additional fields:

+ + + + + +
FieldDescription
Loop descriptionText shown on the loop accept/skip screen + in-game.
AnimationAnimation played on the loop screen.
Voice fileNarration audio for the loop screen.
+ +

Graph view

+

Missions appear as boxes; drag connectors between them to create branches:

+ + + + + +
ConnectorCreates
Blue → GreenA normal branch. The SEXP condition determines + whether this path is taken.
Orange → GreenA loop branch. The player is given the option to + accept or skip the loop before it executes.
Blue → End Campaign nodeAn end-of-campaign branch. The campaign + ends when this branch is taken. The same SEXP priority rules + apply.
+

The End Campaign node is created automatically. Right-click a node in the graph +for additional options, including Add Repeat Mission, which creates +a branch that loops back to the same mission, the standard way to let the player +retry a mission after failure.

+ + diff --git a/qtfred/help-src/doc/dialogs/CommandBriefingDialog.html b/qtfred/help-src/doc/dialogs/CommandBriefingDialog.html new file mode 100644 index 00000000000..24911813f4c --- /dev/null +++ b/qtfred/help-src/doc/dialogs/CommandBriefingDialog.html @@ -0,0 +1,52 @@ + + + + + Command Briefing - QtFRED Help + + + + +

Command Briefing

+

Opens via Editors › Command Briefing.

+

Builds the Command Briefing shown to the player after the +Fiction Viewer and before the main +Briefing. It consists of one or more stages, +each combining a looping animation with narrated text. Stages advance automatically +when the voice clip finishes, or the player can click to skip.

+ +

Teams

+

In multiplayer missions each team can have its own independent command briefing. Use +the team selector to switch which team's command briefing you are editing. +Copy to other team duplicates the entire command briefing (all stages) +to the other team, overwriting it.

+ +

Stage controls

+ + + + + + +
ControlDescription
Prev / NextNavigate between stages.
Add stageAppends a new stage at the end.
Insert stageInserts a new stage after the current one.
Delete stageRemoves the current stage.
+ +

Per-stage fields

+ + + + + +
FieldDescription
Briefing textText displayed on screen during this stage.
Animation fileThe ANI, APNG, or EFF animation that loops in the + background during this stage.
Speech fileVoice narration played during this stage. The stage + advances automatically when the clip ends.
+ +

Custom background images

+

Overrides the default command briefing background with a custom image. Two slots +are provided to accommodate different display resolutions:

+ + + + +
SlotUsed at
Background (lo-res)480-line resolutions (e.g. 640×480).
Background (hi-res)768-line resolutions and above.
+ + diff --git a/qtfred/help-src/doc/dialogs/CustomDataDialog.html b/qtfred/help-src/doc/dialogs/CustomDataDialog.html new file mode 100644 index 00000000000..38311d7706d --- /dev/null +++ b/qtfred/help-src/doc/dialogs/CustomDataDialog.html @@ -0,0 +1,40 @@ + + + + + Custom Data - QtFRED Help + + + + +

Custom Data

+

Accessed via Custom Data in the +Mission Specs dialog or the +Campaign Editor.

+

Attaches arbitrary key-value string pairs to a mission or campaign. Custom data +has no built-in effect on gameplay; it is intended to be read by Lua scripts that +know to look for specific keys.

+ +

Pre-defined keys

+

Lua scripts can register expected custom data keys by calling +mn.addDefaultCustomData(key, defaultValue, description). When a +mission is opened in FRED, any registered key that is not already present in the +mission is automatically added with its default value. This ensures the expected +entries exist without the mission designer having to create them by hand.

+

The Description box on the left of the dialog shows the +description the script author provided for whichever entry is currently selected. +If the selected key is not one registered by a script, the box reads +No help text provided.

+ +

Adding an entry

+

Fill in the Key and Value fields and click +Add to create a new entry.

+ +

Editing an entry

+

Select an entry in the list to load it into the Key and Value fields. Modify the +fields as needed and click Update to apply the changes.

+ +

Removing an entry

+

Select an entry in the list and click Remove to delete it.

+ + diff --git a/qtfred/help-src/doc/dialogs/CustomStringsDialog.html b/qtfred/help-src/doc/dialogs/CustomStringsDialog.html new file mode 100644 index 00000000000..9c3ef38be78 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/CustomStringsDialog.html @@ -0,0 +1,35 @@ + + + + + Custom Strings - QtFRED Help + + + + +

Custom Strings

+

Accessed via Custom Strings in the Mission Specs dialog.

+

Attaches arbitrary key-value-text entries to the mission. Each entry has three +fields: a key for lookup, a value, and a longer text field. The intended use is +localization support; the value holds an XSTR ID number and the text holds the +matching string, allowing Lua scripts to resolve translatable strings by key. +The fields are general purpose and can serve any other use case where a key, +a value, and a block of text need to be stored together.

+ +

Fields

+ + + + + +
FieldDescription
KeyThe name used to look up this entry.
ValueA short string associated with the key, typically an + XSTR ID number.
TextThe longer string content, typically the translatable text + matching the XSTR ID.
+ +

Editing

+

Fill in all three fields and click Add to create a new entry. +Select an entry in the list to load it into the fields, modify as needed, and click +Update to apply changes. Click Remove to delete +the selected entry.

+ + diff --git a/qtfred/help-src/doc/dialogs/CustomWingNamesDialog.html b/qtfred/help-src/doc/dialogs/CustomWingNamesDialog.html new file mode 100644 index 00000000000..8cfc54abfd3 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/CustomWingNamesDialog.html @@ -0,0 +1,32 @@ + + + + + Custom Wing Names - QtFRED Help + + + + +

Custom Wing Names

+

Accessed via Custom Wing Names in the Mission Specs dialog.

+

Overrides the default wing name sets used by the engine. All three sets are +independent; changing one does not automatically update the others.

+ +

Starting wing names

+

The three wings that appear on the pre-mission loadout screen, where players +select their ships before the mission begins. Defaults are +Alpha, Beta, and Gamma.

+ +

Squadron wing names

+

The five wings tracked on the HUD squad status display during the mission. +Defaults are Alpha, Beta, Gamma, +Delta, and Epsilon. These are independent of the starting +wing names. Conventionally the first three match, but the engine does not enforce +this.

+ +

Team-versus-team wing names

+

The two team wings used in multiplayer Team-vs-Team missions. One name per team. +The first TvT wing name must be identical to the first starting wing name; the +engine enforces this and will error if they differ.

+ + diff --git a/qtfred/help-src/doc/dialogs/DebriefingDialog.html b/qtfred/help-src/doc/dialogs/DebriefingDialog.html new file mode 100644 index 00000000000..ea73d30d27a --- /dev/null +++ b/qtfred/help-src/doc/dialogs/DebriefingDialog.html @@ -0,0 +1,58 @@ + + + + + Debriefing - QtFRED Help + + + + +

Debriefing

+

Opens via Editors › Debriefing.

+

Builds the post-mission debrief shown after the player returns to base. Each stage +has a usage formula; only stages whose formula evaluates to true are shown, so you +can deliver different feedback depending on how the mission went.

+ +

Teams

+

In multiplayer missions each team can have its own independent debriefing. Use the +team selector to switch which team's debriefing you are editing. Copy to other +team duplicates the entire debriefing (all stages) to the other team, +overwriting it.

+ +

Stage controls

+ + + + + + +
ControlDescription
Prev / NextNavigate between stages.
Add stageAppends a new stage at the end.
Insert stageInserts a new stage after the current one.
Delete stageRemoves the current stage.
+ +

Per-stage fields

+ + + + + + +
FieldDescription
Stage textThe main body text displayed and read aloud to the + player during this stage.
Recommendation textA follow-up line shown when the player presses + the Recommendations button in the debriefing screen. Optional - leave blank + to show nothing.
Voice fileAudio narration for this stage's text.
Usage formulaSEXP evaluated + after the mission ends. This stage is shown only if the formula is + true.
+ +

Music

+

Music applies to the entire debriefing, not individual stages. Three tracks can be +assigned depending on mission outcome:

+ + + + + +
TrackWhen played
SuccessAll primary and secondary goals were + completed.
AverageThe mission was passed but not all goals were + met.
FailureThe mission was failed, or the player turned + traitor.
+ + diff --git a/qtfred/help-src/doc/dialogs/ErrorCheckerDialog.html b/qtfred/help-src/doc/dialogs/ErrorCheckerDialog.html new file mode 100644 index 00000000000..1e49bf8168f --- /dev/null +++ b/qtfred/help-src/doc/dialogs/ErrorCheckerDialog.html @@ -0,0 +1,14 @@ + + + + + Error Checker - QtFRED Help + + + + +

Error Checker

+

Accessible via Tools › Error Checker.

+
Full documentation for this tool is not yet written.
+ + diff --git a/qtfred/help-src/doc/dialogs/EventEditor.html b/qtfred/help-src/doc/dialogs/EventEditor.html new file mode 100644 index 00000000000..c24debd501a --- /dev/null +++ b/qtfred/help-src/doc/dialogs/EventEditor.html @@ -0,0 +1,15 @@ + + + + + Event Editor - QtFRED Help + + + + +

Event Editor

+

Opens via Editors › Mission Events.

+

The Event Editor is another name for the Mission Events dialog. See +Mission Events for full documentation.

+ + diff --git a/qtfred/help-src/doc/dialogs/FictionViewerDialog.html b/qtfred/help-src/doc/dialogs/FictionViewerDialog.html new file mode 100644 index 00000000000..007156807fb --- /dev/null +++ b/qtfred/help-src/doc/dialogs/FictionViewerDialog.html @@ -0,0 +1,30 @@ + + + + + Fiction Viewer - QtFRED Help + + + + +

Fiction Viewer

+

Opens via Editors › Fiction Viewer.

+

Assigns a fiction document displayed to the player in the Fiction Viewer screen before +the mission. The fiction screen appears before the +Command Briefing and main Briefing +when a story file is configured here. Leave all fields blank to skip the screen +entirely.

+ +

Key fields

+ + + + + + +
FieldDescription
Story filePath to the text file containing the fiction, relative to + the mod's data/fiction/ folder.
FontThe typeface used to render the fiction text in-game.
Voice fileOptional narration audio played while the player reads. + Relative to data/voiceovers/.
MusicBackground music track played during the fiction screen, + selected from tracks defined in the game tables.
+ + diff --git a/qtfred/help-src/doc/dialogs/GlobalShipFlagsDialog.html b/qtfred/help-src/doc/dialogs/GlobalShipFlagsDialog.html new file mode 100644 index 00000000000..549219f5e80 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/GlobalShipFlagsDialog.html @@ -0,0 +1,19 @@ + + + + + Global Ship Flags - QtFRED Help + + + + +

Global Ship Flags

+

Opens via Editors › Global Ship Flags.

+

A batch tool that sets a flag on every ship currently in the mission in one +operation. Each button prompts for confirmation before applying. The change is +immediate and cannot be undone, and ships added to the mission afterward will not +automatically receive the flag.

+

The individual flags are self-explanatory. The same flags can also be set per ship +in the Ship Editor.

+ + diff --git a/qtfred/help-src/doc/dialogs/JumpNodeEditorDialog.html b/qtfred/help-src/doc/dialogs/JumpNodeEditorDialog.html new file mode 100644 index 00000000000..6440d073845 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/JumpNodeEditorDialog.html @@ -0,0 +1,34 @@ + + + + + Jump Node Editor - QtFRED Help + + + + +

Jump Node Editor

+

Opens via Editors › Jump Node Editor.

+

Creates and configures subspace jump nodes. Jump nodes are fixed points in space that +ships can use to enter or exit subspace. They appear on the HUD radar and can be targeted. +Select a node from the drop-down to edit its properties.

+ +

Key fields

+ + + + + + + + + +
FieldDescription
Jump nodeSelects which placed node to edit.
NameInternal identifier used in SEXPs and mission scripting. + Must be unique.
LayerThe layer the jump node is assigned to.
Display nameName shown to the player on the HUD and in targeting. + If left blank, the internal name is used instead.
Model filePOF model rendered at the node's position. Uses the + default jump node model if left blank.
Node color (RGB)Color used to render the node model in the + mission.
Hidden by defaultWhen checked, the node starts the mission hidden +
+ + + diff --git a/qtfred/help-src/doc/dialogs/MissionCutscenesDialog.html b/qtfred/help-src/doc/dialogs/MissionCutscenesDialog.html new file mode 100644 index 00000000000..f5bf9136963 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/MissionCutscenesDialog.html @@ -0,0 +1,46 @@ + + + + + Mission Cutscenes - QtFRED Help + + + + +

Mission Cutscenes

+

Opens via Editors › Mission Cutscenes.

+

Defines cutscene movies that play at specific points around the mission. Each +cutscene entry pairs a type, which determines when it can play, with a +movie filename and a SEXP condition. The +cutscene plays when its SEXP evaluates to true.

+ +

Cutscene types

+ + + + + + + + + +
TypeWhen it plays
Fiction ViewerJust before the fiction viewer game state.
Command BriefingJust before the command briefing game state.
BriefingJust before the briefing game state.
Pre-gameJust before the mission starts, after Accept has been + pressed.
DebriefingJust before the debriefing game state.
Post-debriefingWhen the debriefing has been accepted but before + exiting the mission.
End CampaignWhen the campaign has been completed.
+ +

Cutscene list and SEXP tree

+

The main area shows all cutscenes for the mission alongside their SEXP trees. The +Display Cutscene dropdown filters the list to show one type at a +time. A description of when the selected type plays appears beneath the dropdown. +Use New Cutscene to add a new entry of the currently displayed +type.

+ +

Cutscene properties

+ + + + +
FieldDescription
TypeWhen this cutscene will play. Changing the type moves the + entry into the corresponding type group.
FilenameThe movie file to play.
+ + diff --git a/qtfred/help-src/doc/dialogs/MissionEventsDialog.html b/qtfred/help-src/doc/dialogs/MissionEventsDialog.html new file mode 100644 index 00000000000..de1799ebb9d --- /dev/null +++ b/qtfred/help-src/doc/dialogs/MissionEventsDialog.html @@ -0,0 +1,136 @@ + + + + + Mission Events - QtFRED Help + + + + +

Mission Events

+

Opens via Editors › Mission Events.

+

Mission Events is the primary scripting interface in QtFRED and where the vast majority +of mission design work happens. Each event is a named unit that pairs a condition with one +or more actions expressed as SEXPs. The engine +evaluates every event every frame; when the condition becomes true the actions fire. +Events can repeat on a cooldown, depend on other events firing first, display objectives +on the HUD, trigger messages, and much more. The event list is the brain of a mission.

+ +

Event list and SEXP tree

+

The main area shows all events in the mission alongside their SEXP trees. Selecting an +event expands its condition and action expressions for editing. Right-click any SEXP node +to add, replace, or remove operators and arguments. See +SEXPs for a full overview of the expression +language and the operators available.

+

Events are evaluated in list order each frame. Order matters: events earlier in the +list are processed first, which can affect timing when events interact.

+ +

Event list buttons

+ + + + + + +
ButtonDescription
NewAppends a new blank event at the end of the list.
InsertInserts a new blank event above the currently selected + event.
DeleteRemoves the selected event.
Move Up / Move DownReorders the selected event within the + list.
+ +

Event properties

+

These fields configure the selected event.

+ + + + + + + + + + + + + +
FieldDescription
NameInternal label shown in the event list. Referenced by the + Chain feature of other events and by SEXPs such as + event-true and event-false.
Repeat countHow many times the event's actions will run in total. + -1 means unlimited.
Trigger countHow many times the event's condition must become true + before the event stops firing entirely. Once this count reaches zero the event + is permanently disabled, overriding any remaining repeat count.
IntervalMinimum time in seconds or milliseconds between successive + firings when the repeat count is greater than 1. The interval timer resets after + each firing.
ChainedWhen checked, this event's condition is not evaluated until + the immediately preceding event in the list has become true at least once. Used + to sequence events without adding explicit timing conditions to every + SEXP.
Chain delayNumber of seconds or milliseconds after the preceding + event becomes true before this event's condition begins evaluating. Only + meaningful when Chained is checked.
Interval & Chain Delay in MilllisecondsChekcbox that + when checked changes both Chain Delay and Interval to being milliseconds instead + of seconds.
ScorePoints awarded to the player when this event becomes + true.
TeamIn multiplayer missions, restricts the event to a specific + team. Disabled for non-multiplayer missions.
Directive textText shown in the player's objective display on the + HUD when this event is active. Writing text here makes the event a directive. + Directives turn red when they can no longer become true, and blue when they + do become true.
Directive keypress textHelper text displayed beneath the directive + in green on the HUD. Keybinds can be embedded by surrounding the bind name + with $$, for example $Alt-X$, and they will be + automatically translated to the player's current control bindings.
+ +

Log flags

+

These checkboxes control when the selected event writes an entry to the engine's +debug event log (event.log). They are useful for tracing event behavior +during mission testing.

+ + + + + + + + + + +
FlagLogs when…
TrueThe event condition evaluates to true.
FalseThe event condition evaluates to false.
Always FalseThe event condition is permanently false.
Log PreviousThe event changes state; logs the previous state + alongside the new one.
First RepeatThe event fires on its first repetition. Flag is + cleared after logging once.
Last RepeatThe event fires on its final repetition.
First TriggerThe event fires on its first trigger. Flag is + cleared after logging once.
Last TriggerThe event fires on its final trigger.
+ +

Messages

+

The messages panel lists all messages defined for this mission. SEXPs such as +send-message and send-message-list reference messages by +name. Use the arrow buttons to reorder messages in the list.

+ +

Message list buttons

+ + + + + + + +
ButtonDescription
NewAppends a new blank message at the end of the list.
InsertInserts a new blank message above the selected + message.
DeleteRemoves the selected message.
Add NoteAttaches a note to the selected message that is not + visible to the player. Notes function like stage directions and are printed in + the generated script exported from the + Voice Acting Manager.
Update StuffAutomatically assigns the head animation and persona + for the selected message based on the current mission settings.
+ +

Message properties

+ + + + + + + + +
FieldDescription
NameInternal identifier used to reference this message in SEXPs. + Not shown to the player.
Message textThe text displayed on the HUD when this message is + sent.
Head ANIThe talking-head animation file shown with the message. + Choose from the dropdown for quick access or browse the gallery.
Wave fileThe audio file played when this message is sent.
PersonaWhen a message has a persona assigned and is sent with + wingman as the source, the engine finds a random ship from the + player's wing that matches this persona to act as sender. If no matching ship + exists the message falls back to Terran Command.
TeamIn Team vs. Team multiplayer, restricts this message to + players on the specified team. None sends the message to all + players.
+ + diff --git a/qtfred/help-src/doc/dialogs/MissionGoalsDialog.html b/qtfred/help-src/doc/dialogs/MissionGoalsDialog.html new file mode 100644 index 00000000000..f7984c628a3 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/MissionGoalsDialog.html @@ -0,0 +1,78 @@ + + + + + Mission Goals - QtFRED Help + + + + +

Mission Goals

+

Opens via Editors › Mission Goals.

+

Defines the goals shown to the player at the end of the briefing and tracked on +the HUD during the mission. When a goal is completed or failed a HUD popup notifies +the player. Goals are hidden during the briefing if the +Toggle Showing Goals In Briefing flag is set in +Mission Specs.

+ +
The UI uses the term Objective in some places (such as +the New Obj. button) interchangeably with Goal. The +canonical term is Goal, which matches the SEXP operator naming +convention.
+ +

Goal types

+

Goal type is a classification that affects how goals are displayed and which music +plays on completion or failure. The actual consequences of a goal being met or failed +, which debriefing stage the player sees, whether the campaign advances, are +determined by SEXPs in the +Debriefing Editor and the +Campaign Editor, which can query goal state +using operators like goal-true and goal-false.

+ + + + + +
TypeDescription
PrimaryShown to the player from the start of the mission. + Completion or failure triggers the primary goal music stinger. By convention + these are the core objectives designers tie campaign progression and debriefing + outcomes to.
SecondaryShown to the player from the start of the mission. + Completion or failure triggers the secondary goal music stinger. Designers + typically use these for optional objectives whose state is queried in the + debriefing or campaign for bonus outcomes.
BonusHidden from the player until the goal is achieved, at which + point the goal text appears on the HUD. Their state is queryable by SEXPs the + same as any other goal type.
+ +

Goal list

+

The main area displays all goals as SEXP +trees. The dropdown at the top filters the list to show one type at a time; +Primary, Secondary, or Bonus. Use New Obj. to append a new goal +of the currently filtered type.

+ +

Goal properties

+ + + + + + + +
FieldDescription
TypePrimary, Secondary, or Bonus. Changing this moves the goal + into the corresponding type group.
NameInternal identifier used in SEXPs such as + goal-true and goal-false, and in mission log + entries. Not shown to the player.
Goal textText displayed to the player in the briefing goal list + and HUD objective display.
ScorePoints awarded when this goal is completed.
TeamIn multiplayer missions, the team this goal belongs + to.
+ +

Goal flags

+ + + + +
FlagDescription
Objective InvalidMarks the goal as invalid from the start of + the mission. Invalid goals are not shown to the player and are not evaluated. + Goals can also be marked valid or invalid at runtime by SEXPs, making this + useful for goals that are conditionally activated during play.
Don't Play Completion MusicSuppresses the event music stinger + that normally plays when this goal is completed or failed.
+ + diff --git a/qtfred/help-src/doc/dialogs/MissionSpecsDialog.html b/qtfred/help-src/doc/dialogs/MissionSpecsDialog.html new file mode 100644 index 00000000000..b05acd65eb1 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/MissionSpecsDialog.html @@ -0,0 +1,144 @@ + + + + + Mission Specs - QtFRED Help + + + + +

Mission Specs

+

Opens via Editors › Mission Specs.

+

Sets mission-wide properties: identity, type, music, support options, and gameplay +flags. This is typically one of the first dialogs to fill in when starting a new +mission.

+ +

Identity

+ + + + + + +
FieldDescription
TitleMission title shown in the mission selection screen and + campaign room.
DesignerAuthor name stored in the mission file for + reference.
DescriptionShort summary shown in the mission selection + screen.
Designer notesFree-form notes visible only in the editor; not + shown to players.
+ +

Mission Type

+

Determines the gameplay rules for the mission.

+ + + + + +
TypeDescription
Single PlayerStandard single-player mission.
MultiplayerSets the mission as a multiplayer mission. A sub-type + must then be selected: Co-op, Team vs. Team, or + Dogfight (all players against all others).
TrainingIntended for tutorial content. A Skip Training + button appears on the briefing screen; selecting it marks all mission goals + complete and advances the campaign without the player having to fly the + mission.
+ +

Multiplayer

+ + + + + + +
FieldDescription
Max RespawnsNumber of times each player may respawn before they + are out of the mission.
Max Respawn DelayNumber of seconds after death before a player + automatically respawns.
Player Entry DelayNumber of seconds of mission time that pass + before the player enters the mission.
Custom Wing NamesOpens + a sub-dialog for assigning special names to player wings. Used in both single + and multiplayer missions; affects the player's wing name, which wings appear + in the loadout editor, and how wings are labeled on the wing HUD + display.
+ +

Squadron

+

Overrides the player's squadron name and logo for this mission. The logo is chosen +from a gallery popup. In campaign mode, the reassignment persists and carries forward +to all subsequent missions.

+ +

Loading Screen

+ + + + +
FieldDescription
480 (lo-res)Custom loading screen image used at lower + resolutions.
768 (hi-res)Custom loading screen image used at higher + resolutions.
+ +

Support Ships

+ + + + + + +
ControlDescription
Disallow support shipsPrevents any support ship from appearing + in this mission.
Enable hull repairAllows the support ship to repair hull damage + in addition to subsystems. Subsystem repair is always available when a support + ship is present.
Hull repair ceilingMaximum percentage of hull the support ship + can restore, when hull repair is enabled.
Subsystem repair ceilingMaximum percentage of subsystem + integrity the support ship can restore.
+ +

Ship Trails

+

Controls engine contrail visibility. By default, trails are on in +full-nebula missions and off elsewhere. Enabling the toggle reverses this +default.

+ + + + + +
ControlDescription
Toggle (off in nebula; on elsewhere)Reverses the default + contrail behavior: trails become off in nebula and on in non-nebula + missions.
Enable minimum speedWhen checked, trails are only shown when a + ship is moving at or above the configured speed.
Minimum speedSpeed threshold in meters per second below which + trails are hidden.
+ +

Command Messages

+

Configures the identity of the Command sender used for built-in mission messages.

+ + + + + +
ControlDescription
SenderThe ship that speaks as Command. Populated from huge ships + present in the current mission, plus the default + Command entry.
PersonaVoice persona used for Command messages. Populated from + personas flagged as Command personas in the game tables.
Override #Command in event messagesWhen enabled, any event + message whose sender is the literal string #Command will use the + Sender and Persona configured here instead of the default Command + identity.
+ +

Mission Music

+

Assigns the music pack used during the mission. An alternate pack can also be +specified; it will be used instead if it is present in the current mod.

+ +

AI Profile

+

Selects which AI profile from the game tables governs NPC behavior in this +mission. Different profiles vary AI aggression, skill ceiling, and target selection +logic.

+ +

Sound Environment

+

Opens the Sound Environment sub-dialog, +which configures audio reverb and environmental effects applied during the +mission.

+ +

Custom Data

+

See the Custom Data concept page.

+ +

Custom Strings

+

Opens the Custom Strings sub-dialog for +attaching arbitrary named strings to the mission.

+ +

Flags

+

A searchable list of toggles that adjust global mission behavior. Use the filter +box to find a specific flag by name, or use Select All / +Select None to bulk-toggle. Hover over any flag label for a tooltip +description.

+ + diff --git a/qtfred/help-src/doc/dialogs/MissionStatsDialog.html b/qtfred/help-src/doc/dialogs/MissionStatsDialog.html new file mode 100644 index 00000000000..f725043826e --- /dev/null +++ b/qtfred/help-src/doc/dialogs/MissionStatsDialog.html @@ -0,0 +1,42 @@ + + + + + Mission Statistics - QtFRED Help + + + + +

Mission Statistics

+

Opens via View › Mission Stats.

+

Displays a read-only summary of everything currently in the mission. Use this to get a +quick overview of mission complexity or verify counts before testing.

+ +

Tabs

+ + + + + + +
TabContents
SummaryTotal counts: ships, wings, waypoint paths, jump nodes, + mission events, mission goals, and messages.
ShipsEvery ship in the mission listed with its name, class, team, + and wing assignment.
EscortShips with the escort flag enabled in the + Ship Editor. Ships added to or removed + from the escort list at runtime via SEXPs are not reflected here.
HotkeysHotkey assignments defined statically in the mission. + Assignments made at runtime via the add-remove-hotkey SEXP are + not reflected here.
+ +

Hard limits

+ + + + +
LimitValueNotes
Max ships500Maximum number of ship instances that can + exist in the mission at any one time.
Max objects5000Maximum total object count including + ships, props, weapons, and all other object types.
+ +
This dialog is read-only. To change anything shown here, open the +relevant editor from the Editors or View menu.
+ + diff --git a/qtfred/help-src/doc/dialogs/MusicPlayerDialog.html b/qtfred/help-src/doc/dialogs/MusicPlayerDialog.html new file mode 100644 index 00000000000..adf48bb5bc4 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/MusicPlayerDialog.html @@ -0,0 +1,31 @@ + + + + + Music Player - QtFRED Help + + + + +

Music Player

+

Opens via View › Music Player.

+

Plays back music files from the game's music folder while you work, so you can +audition tracks without launching FreeSpace Open. Useful when choosing tracks for +Mission Specs, +Briefing, +Debriefing, or +Command Briefing.

+ +

Controls

+ + + + + + +
ControlDescription
Track listAll music files found in the game's music folder. + Click a track to select it.
Play / StopStart or stop playback of the selected track.
Previous / NextJump to the previous or next track in the + list.
AutoplayWhen enabled, playback advances automatically to the + next track when the current one ends, like a playlist.
+ + diff --git a/qtfred/help-src/doc/dialogs/ObjectOrientEditorDialog.html b/qtfred/help-src/doc/dialogs/ObjectOrientEditorDialog.html new file mode 100644 index 00000000000..58dc9a953ed --- /dev/null +++ b/qtfred/help-src/doc/dialogs/ObjectOrientEditorDialog.html @@ -0,0 +1,59 @@ + + + + + Object Orientation Editor - QtFRED Help + + + + +

Object Orientation Editor

+

Opens via Editors › Object Orientation.

+

Precisely sets the position and orientation of one or more selected objects using +numeric input. Useful when you need exact coordinates or angles rather than dragging +objects in the viewport.

+ +

Position

+

X, Y, and Z set the world-space +coordinates of the object.

+ +

Orientation

+

P (pitch), B (bank), and H +(heading) set the object's facing angles. These controls are disabled when +Point To is active.

+ +

Point To

+

Instead of specifying PBH angles directly, Point To automatically +orients the object so that it faces a chosen target. Use the drop-down to select +another object in the mission to point at, or enter a specific set of world-space +coordinates to point toward instead.

+ +

Absolute vs. Relative

+ + + + +
ModeBehavior
Set absoluteThe values entered are treated as final world-space + coordinates and angles. The object is placed exactly where you specify, + regardless of where it currently is.
Set relativeThe values entered are added to the object's current + position and orientation. Entering X = 100 moves the object 100 + units further along the X axis from wherever it already is.
+ +

Transform Objects Independently vs. Relative to Origin Object

+

This option only matters when multiple objects are selected.

+ + + + +
ModeBehavior
Transform Objects IndependentlyEach selected object is transformed + on its own. The same position or orientation values are applied to every object + separately, so they all end up at the same location or facing the same + direction. Use this when you want to snap a group of objects to a common + position or give them all an identical heading.
Transform Relative to Origin ObjectThe selected objects are moved + or rotated as a group, preserving the spatial relationships between them. The + transform is applied to the group as a whole using one object as the reference + point, so the objects stay the same distance and orientation relative to each + other. Use this when repositioning or rotating a formation without breaking + it apart.
+ + diff --git a/qtfred/help-src/doc/dialogs/PreferencesDialog.html b/qtfred/help-src/doc/dialogs/PreferencesDialog.html new file mode 100644 index 00000000000..88783286106 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/PreferencesDialog.html @@ -0,0 +1,43 @@ + + + + + Preferences - QtFRED Help + + + + +

Preferences

+

Opens via File › Preferences.

+

Controls QtFRED editor settings. Changes take effect immediately; there is no +Apply button.

+ +

General

+ + + + + + +
SettingDescription
Move ships with arrow keysWhen enabled, the arrow keys nudge + selected objects in the viewport instead of scrolling the view.
Check for errors on saveRuns the built-in mission error checker + automatically each time the mission is saved.
Show SEXP help tooltipsDisplays documentation tooltips when + hovering over SEXP nodes in the condition and action trees.
Dark modeSwitches the editor UI to a dark color + theme.
+ +

Grid

+ + + + + +
SettingDescription
Grid planeWhich plane the editor grid lies on: + XZ (horizontal), XY, or YZ.
Grid centerThe world-space coordinates of the grid's center + point.
Reset gridResets the grid plane and center to defaults.
+ +

Keybindings

+

Lists every editor action alongside its current keyboard shortcut. Click a shortcut +field and press a new key combination to rebind it. Conflicts are highlighted. +Bindings are saved per-user and persist across sessions.

+ + diff --git a/qtfred/help-src/doc/dialogs/PropEditorDialog.html b/qtfred/help-src/doc/dialogs/PropEditorDialog.html new file mode 100644 index 00000000000..5802f65a4da --- /dev/null +++ b/qtfred/help-src/doc/dialogs/PropEditorDialog.html @@ -0,0 +1,31 @@ + + + + + Prop Editor - QtFRED Help + + + + +

Prop Editor

+

Opens via Editors › Props.

+

Configures prop objects placed in the mission. Props are static objects that cannot +move or be interacted with except for collisions.

+ +

Key fields

+ + + + + +
FieldDescription
NameUnique identifier for this prop instance, used in SEXPs and + mission scripting.
LayerThe layer the prop is assigned to.
Prev / NextCycles through all prop objects in the mission without + returning to the viewport.
+ +

Flags

+

A list of behavior flags that can be toggled on or off for this prop. Use the filter +field to search by name, and the Select All / Select +None buttons to bulk-toggle all visible flags. Hover over a flag for a +tooltip description.

+ + diff --git a/qtfred/help-src/doc/dialogs/ReinforcementsEditorDialog.html b/qtfred/help-src/doc/dialogs/ReinforcementsEditorDialog.html new file mode 100644 index 00000000000..1a3860df6dd --- /dev/null +++ b/qtfred/help-src/doc/dialogs/ReinforcementsEditorDialog.html @@ -0,0 +1,39 @@ + + + + + Reinforcements - QtFRED Help + + + + +

Reinforcements Editor

+

Opens via Editors › Reinforcements.

+

Designates wings that the player can call in as reinforcements via the comm menu +during the mission. Reinforcement wings do not spawn automatically at mission start. +Instead of auto-spawning when their arrival cue becomes true, the arrival cue +determines when the option to call that reinforcement becomes available in the comm +menu. The wing then warps in only when the player actually calls it.

+ +

Wing lists

+

The left panel lists all wings in the mission. Select one or more and use +Add to designate them as reinforcements; they appear in the right +panel. Select a wing in the right panel and use Remove to revoke +its reinforcement status.

+ +

Reinforcement properties

+ + + + +
FieldDescription
UsesMaximum number of times the player can call this + reinforcement. Once the limit is reached the option disappears from the comm + menu.
DelayTime in seconds between when the player calls the + reinforcement and when the wing actually warps in.
+ +
Reinforcement wings must still have their arrival cues and arrival +location configured in the Wing Editor. The +arrival cue controls when the reinforcement option appears in the comm menu; the +arrival location controls where the wing warps in when called.
+ + diff --git a/qtfred/help-src/doc/dialogs/RelativeCoordinatesDialog.html b/qtfred/help-src/doc/dialogs/RelativeCoordinatesDialog.html new file mode 100644 index 00000000000..0992d9dcd3a --- /dev/null +++ b/qtfred/help-src/doc/dialogs/RelativeCoordinatesDialog.html @@ -0,0 +1,21 @@ + + + + + Calculate Relative Coordinates - QtFRED Help + + + + +

Calculate Relative Coordinates

+

Opens via Tools › Calculate Relative Coordinates.

+

A read-only information tool that calculates the distance and orientation from one +object to another. Select an origin object from the left list and a +satellite object from the right list; the distance and P/B/H angles +from the origin to the satellite are displayed instantly. Angles are expressed in the +origin object's local coordinate frame. Both lists include ships and waypoints. If two +objects are already selected in the viewport when the dialog opens, they are +pre-selected as origin and satellite.

+

The dialog does not modify the mission in any way.

+ + diff --git a/qtfred/help-src/doc/dialogs/ShieldSystemDialog.html b/qtfred/help-src/doc/dialogs/ShieldSystemDialog.html new file mode 100644 index 00000000000..13507a6daca --- /dev/null +++ b/qtfred/help-src/doc/dialogs/ShieldSystemDialog.html @@ -0,0 +1,39 @@ + + + + + Shield System - QtFRED Help + + + + +

Shield System

+

Opens via Editors › Shield System.

+

A convenience tool for enabling or disabling shields on ships in bulk, by ship class +or by IFF team. When applied, the dialog directly sets the shield flag on every +existing ship in the mission; it is not a runtime override. If you add new ships +after using this dialog, reopen it and apply again to include them.

+ +

By ship class (left panel)

+

Select a ship class from the dropdown and choose +Has shield system or No shield system. Applying +sets the flag on all ships of that class currently in the mission.

+ +

By IFF team (right panel)

+

Select a team from the dropdown and choose +Has shield system or No shield system. Applying +sets the flag on all ships belonging to that team currently in the mission.

+ +
When both a ship class setting and a team setting apply to the same +ship, the ship class setting takes precedence.
+
The Shield System dialog, the Ship +Editor (Initial Status and Ship Flags tabs), and +Global Ship Flags all write to the same +per-ship shield flag. There is no fixed precedence between them; whichever is +applied last takes effect. A typical workflow is to use this dialog to establish a +baseline across the mission, then make individual adjustments per ship in the Ship +Editor. If you later reopen this dialog and a team or class shows as +Mixed (some ships differ), applying without changing that setting will +leave those individual ship values intact.
+ + diff --git a/qtfred/help-src/doc/dialogs/ShipAltShipClass.html b/qtfred/help-src/doc/dialogs/ShipAltShipClass.html new file mode 100644 index 00000000000..2edfcedf80c --- /dev/null +++ b/qtfred/help-src/doc/dialogs/ShipAltShipClass.html @@ -0,0 +1,40 @@ + + + + + Alt Ship Class - QtFRED Help + + + + +

Alt Ship Class

+

Accessed via the Alt Ship Class button in the Ship Editor.

+

Defines a prioritized fallback chain of ship classes to substitute if this ship's +primary class is unavailable at mission load. The engine tries the primary class +first, then works through the alt class list in order, assigning the first class +that qualifies. This is primarily useful in campaign missions where the available +ship pool can vary based on earlier choices or loadout configuration.

+ +

The alt class list

+

Each entry in the list is one fallback candidate. Order matters. The engine tries +them top to bottom and stops at the first one it can use. Use the +Up and Down buttons to reorder entries. +Add and Insert create new entries; +Delete removes the selected one.

+ +

Entry properties

+ + + + + +
FieldDescription
From ShipA fixed ship class chosen directly from the available + classes. The class is resolved at mission load.
From VariableA string variable whose value names the ship class + to use. The variable is read at mission load, so the class can be set + dynamically by earlier SEXPs in the campaign.
Default to this classControls how the engine checks availability. + When unchecked, the engine assigns this class immediately without checking + whether any slots remain in the loadout. When checked, the engine first + verifies that ships of this class are still available in the loadout; if + none remain, it skips this entry and tries the next one.
+ + diff --git a/qtfred/help-src/doc/dialogs/ShipCustomWarpDialog.html b/qtfred/help-src/doc/dialogs/ShipCustomWarpDialog.html new file mode 100644 index 00000000000..b2b77e84e9a --- /dev/null +++ b/qtfred/help-src/doc/dialogs/ShipCustomWarpDialog.html @@ -0,0 +1,52 @@ + + + + + Custom Warp Params - QtFRED Help + + + + +

Custom Warp Params

+

Accessed via the Custom Warp Params button in the Arrival or Departure section of the +Ship Editor or Wing Editor.

+

Overrides the warp-in or warp-out appearance and behavior for this instance. The +dialog opens separately for arrival and departure, so each direction can have +independent settings.

+ +

Fields

+ + + + + + + + + + + + + +
FieldDescription
Warp typeThe visual style of the warp effect. Available types + include the standard hyperspace portal, Knossos device, in-place animation + (BSG-style), sweeper (Homeworld-style), and hyperspace jump with an + acceleration curve. Some fields below are only active for certain + types.
Start soundSound played when the warp effect begins. Enter the + sound name as defined in the game sound tables.
End soundSound played when the warp effect finishes.
Warpout engage timeSeconds the ship waits after the departure cue + fires before the warp animation begins. Warp-out only.
Warping speedSpeed in game units per second at which the ship + translates through the warp effect during the animation. For the hyperspace + type, the current ship speed is used if it exceeds this value.
Warping timeDuration of the warp animation in seconds.
Acceleration / Deceleration exponentExponent applied to the + speed curve during the animation. Values above 1 produce a fast start + tapering to a slow finish; values below 1 do the opposite. Only available + when the warp type is Hyperspace.
RadiusRadius of the warp portal or effect in game units. For the + sweeper type, a value of 0 or less uses the ship's bounding box + dimensions.
AnimationFilename of the animation displayed during the effect. + Only available for the sweeper warp type.
Use supercap warp physicsEnables special physics handling + designed for large capital ships. During warp-out the ship decelerates after + clearing the effect; during warp-in it slows to match its normal maximum + speed.
Player warpout speedOverrides the default speed used to calculate + the player ship's warp-out animation timing. Only available for warp-out on + player ships.
+ + diff --git a/qtfred/help-src/doc/dialogs/ShipEditorDialog.html b/qtfred/help-src/doc/dialogs/ShipEditorDialog.html new file mode 100644 index 00000000000..8632771c088 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/ShipEditorDialog.html @@ -0,0 +1,148 @@ + + + + + Ship Editor - QtFRED Help + + + + +

Ship Editor

+

Opens by double-clicking a ship in the viewport, or via Editors › +Ships.

+

The Ship Editor configures individual ships. When multiple ships are selected in the +viewport, editing a field applies the change to all of them simultaneously; some +fields are disabled during multi-ship editing (such as Name, which must be +unique). Some fields also behave differently or are unavailable when the selected +ship is a Player Start.

+ +

General fields

+ + + + + + + + + + + + + + + + + + +
FieldDescription
NameUnique identifier used to reference this ship in SEXPs and + mission log entries. Often visible to the player in the targeting HUD.
Display nameIf set, shown to the player instead of the ship name + in the targeting HUD and other player-facing contexts.
Ship classThe ship class from the game tables, defining the + model, stats, and weapon hardpoints.
CallsignShort identifier appended to the ship name in HUD target + boxes (e.g. the ship name may read "Alpha 1 (Braveheart)" if a callsign is + set).
Alt nameIf set, replaces the ship's class name in HUD target + boxes. Use this to display a different class name to the player without + changing the actual ship class.
AI classOverrides the AI skill level for this ship, independent + of the mission's global AI profile.
TeamIFF team (Friendly, Hostile, Neutral, etc.) determining + which ships treat this one as ally or enemy.
CargoText displayed to the player when the ship is scanned. Use + Nothing for ships with no cargo.
Cargo TitleText title displayed to the player when the ship is scanned. + This is the text before the ":" and defaults to "Cargo:" if not set.
WingRead-only. Shows which wing this ship belongs to, if + any.
HotkeyFunction key (F5–F12) that selects this ship during + play.
PersonaVoice persona used when this ship sends messages and when + auto-selecting a wingmate sender for messages with a matching persona + assigned.
Kill scorePoints awarded to the player for destroying this + ship.
Assist %Percentage of the kill score awarded to players who earn + an assist on this ship. For example, a value of 50 with a kill score of 100 + gives assisters 50 points.
Respawn priorityIn multiplayer, ships with higher values are + prioritized when the engine selects which ship a player respawns into.
Player shipRead-only indicator. Use Set Player + Ship to change this.
+ +

Buttons

+ + + + + + + +
ButtonDescription
Set Player ShipConverts the selected ship between a Player Start + and a regular ship. This changes the ship's internal object type and updates + the player start count, affecting how the engine handles spawning and + player-specific logic.
Prev / NextMoves the selection to the previous or next ship in + the mission, allowing quick sequential editing without switching back to the + viewport.
LayerThe layer the ship is assigned to.
Delete ShipRemoves the selected ship from the mission.
ResetResets the selected ship(s) to their ship class defaults, + including cargo, AI class, goals, hull and shield percentages, and weapon + loadout.
+ +

Subdialogs

+ + + + + + + + + + +
DialogDescription
WeaponsPrimary and secondary + weapon bank loadout.
Player OrdersConfigures + which comm menu orders the player can issue to this ship.
Texture + ReplacementOverrides specific textures on this ship + instance.
Initial StatusStarting + hull, shields, subsystem damage, and weapon ammo.
Initial OrdersAI orders + this ship follows at mission start.
Special StatsPer-instance + overrides for ship statistics.
FlagsPer-ship behavior + flags.
Alt Ship ClassFallback ship + classes used if the primary class is unavailable at mission load.
+ +

Arrival & Departure

+

Controls when and where this ship enters and leaves the mission. The cue SEXP must +evaluate to true before the arrival or departure can occur. Most arrival locations +require a target ship to be selected. Departure location options are limited to +Hyperspace or To Dock Bay.

+ +

Arrival locations

+ + + + + + + + + + + +
LocationNotes
HyperspaceWarps in from hyperspace. No target required.
Near ShipAppears near the target ship at the specified + distance.
In Front of ShipAppears in front of the target at the specified + distance.
In Back of ShipAppears behind the target at the specified + distance.
Above ShipAppears above the target at the specified + distance.
Below ShipAppears below the target at the specified + distance.
To Left of ShipAppears to the left of the target at the specified + distance.
To Right of ShipAppears to the right of the target at the + specified distance.
From Dock BayLaunches from the target ship's dock bay. Use + Restrict Paths to limit which bay paths are + used.
+ +

Fields

+ + + + + + + + + +
FieldDescription
TargetThe ship this arrival or departure is relative to. Required + for all locations except Hyperspace.
DistanceDistance from the target in meters. Arrival only; + available for positional locations (Near Ship, In Front of Ship, + etc.).
DelaySeconds after the cue becomes true before the ship + arrives or departs.
Restrict PathsSelects which dock bay paths to use. Only available + when the location is From Dock Bay or To Dock Bay.
Custom Warp ParamsOpens + a sub-dialog to configure custom warp appearance and behavior.
CueSEXP that must evaluate + to true for arrival or departure to occur.
No warp effectWhen checked, the ship appears or leaves without + the visual warp effect.
+ + diff --git a/qtfred/help-src/doc/dialogs/ShipFlagsDialog.html b/qtfred/help-src/doc/dialogs/ShipFlagsDialog.html new file mode 100644 index 00000000000..10c11f7676e --- /dev/null +++ b/qtfred/help-src/doc/dialogs/ShipFlagsDialog.html @@ -0,0 +1,18 @@ + + + + + Ship Flags - QtFRED Help + + + + +

Ship Flags

+

Accessed via the Flags button in the Ship Editor.

+

A checklist of per-ship behavior flags that override the ship class defaults for +this instance. Each flag has a tooltip explaining its effect.

+

Some flags require an additional parameter when enabled. For example, the escort +flag takes a priority value. When such a flag is checked, its parameter field +appears and must be set before the flag takes effect.

+ + diff --git a/qtfred/help-src/doc/dialogs/ShipInitialOrdersDialog.html b/qtfred/help-src/doc/dialogs/ShipInitialOrdersDialog.html new file mode 100644 index 00000000000..06c5493ead4 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/ShipInitialOrdersDialog.html @@ -0,0 +1,33 @@ + + + + + Initial Orders - QtFRED Help + + + + +

Initial Orders

+

Accessed via the Initial Orders button in the +Ship Editor or Wing Editor.

+

Sets the AI orders the ship or wing follows at mission start. Orders are evaluated +in priority order and can be overridden at runtime by events or +SEXPs.

+ +

Order properties

+ + + + + + + +
FieldDescription
OrderThe AI order to issue (attack, guard, ignore, etc.).
TargetThe ship, wing, or other object the order applies to. + Not all orders require a target.
Target subsystemLimits the order to a specific subsystem on + the target. Only available for orders that support subsystem + targeting.
BayThe dock bay to use. Only available for orders that involve + docking or launching.
PriorityDetermines which order takes precedence when multiple + orders are active. Range is 1–200. Orders with a priority above 89 may + outrank orders issued by the player during play.
+ + diff --git a/qtfred/help-src/doc/dialogs/ShipInitialStatusDialog.html b/qtfred/help-src/doc/dialogs/ShipInitialStatusDialog.html new file mode 100644 index 00000000000..e0e9e40ae4d --- /dev/null +++ b/qtfred/help-src/doc/dialogs/ShipInitialStatusDialog.html @@ -0,0 +1,76 @@ + + + + + Ship Initial Status - QtFRED Help + + + + +

Ship Initial Status

+

Accessed via the Initial Status button in the Ship Editor.

+

Sets the ship's condition at mission start.

+ +

Hull & shields

+ + + + + + + + +
FieldDescription
VelocityThe ship's speed at mission start.
Hull integrityStarting hull strength as a percentage of + maximum.
Guardian thresholdMinimum hull percentage the ship can be + damaged down to. Once the ship's hull reaches this level, further damage is + absorbed without reducing it further. Set to 0 to disable. Applies to both + the hull and subsystems.
Has shield systemWhether this ship has an active shield system + at mission start.
Force shields onEnables shields regardless of other flags that + would suppress them. Only takes effect if the ship's class defines a shield + strength; it cannot create shields for a class that has none.
Shield integrityStarting shield strength as a percentage of + maximum.
+ +

Loadout locks

+ + + + +
FieldDescription
Ship lockedPrevents the player from changing this ship in the + loadout screen.
Weapons lockedPrevents the player from changing this ship's + weapons in the loadout screen.
+ +

Disabled systems

+ + + + + + +
FieldDescription
Primaries won't firePrimary weapons are disabled at mission + start.
Secondaries won't fireSecondary weapons are disabled at mission + start.
Turrets won't fireAll turrets are disabled at mission + start.
Afterburner disabledAfterburner is disabled at mission + start.
+ +

Dockpoints

+

Lists all dockpoints defined on this ship. Select a dockpoint to set it as +occupied by another ship and specify which dockpoint on that ship mates with this +one.

+ +

Subsystems

+

Lists all subsystems on this ship. Select a subsystem to set its starting +integrity as a percentage of maximum. For subsystems that can be scanned, a cargo +field may also available to set what the player finds when scanning that +subsystem. Additionally a cargo title field may be available which defaults to +"Cargo:" if not set. The cargo fields are only visible if the ship has scannable +subsystems. If the Unify Scanning Behavior mod flag is active then +all ships can have scannable cargo by setting the Toggle Subsystem Scanning +ship flag. If the mod flag is not set then huge ships always have scannable subsystems and +other ships will require the ship flag to be enabled.

+ +

Team color

+

Assigns a team color to this ship. Requires the ship to have a +-misc texture map and the desired colors to be defined in +colors.tbl.

+ + diff --git a/qtfred/help-src/doc/dialogs/ShipPlayerOrdersDialog.html b/qtfred/help-src/doc/dialogs/ShipPlayerOrdersDialog.html new file mode 100644 index 00000000000..12f1ada50f9 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/ShipPlayerOrdersDialog.html @@ -0,0 +1,15 @@ + + + + + Player Orders - QtFRED Help + + + + +

Player Orders

+

Accessed via the Player Orders button in the Ship Editor.

+

Lists all orders the player can issue via the comm menu. Check an order to allow +the player to send it to this ship; uncheck to prevent it.

+ + diff --git a/qtfred/help-src/doc/dialogs/ShipSpecialStatsDialog.html b/qtfred/help-src/doc/dialogs/ShipSpecialStatsDialog.html new file mode 100644 index 00000000000..24c80a80643 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/ShipSpecialStatsDialog.html @@ -0,0 +1,51 @@ + + + + + Special Stats - QtFRED Help + + + + +

Special Stats

+

Accessed via the Special Stats button in the Ship Editor.

+

Per-instance overrides for statistics normally defined by the ship class. All +sections are independently enabled and only apply when their checkbox is +checked.

+ +

Special explosion

+

Overrides the ship's death explosion with custom parameters.

+ + + + + + + + + +
FieldDescription
DamageAmount of damage dealt to nearby ships.
BlastPhysical force applied to nearby ships, pushing them + away from the explosion. Scales with distance the same way damage + does.
Inner radiusShips within this distance receive full damage + and blast.
Outer radiusShips beyond this distance receive no damage or + blast. Between the inner and outer radius, both values scale down with + distance.
Create shockwaveWhen checked, the explosion also spawns an + expanding shockwave.
Shockwave speedExpansion speed of the shockwave in game units + per second. Only available when Create shockwave is + checked.
Custom death roll timeWhen checked, overrides the duration of + the pre-death roll animation (the period of secondary explosions before the + final detonation). Set the duration in milliseconds.
+ +

Special hitpoints

+

Overrides the ship's maximum hitpoints with a flat value, replacing the ship +class table entry entirely for this instance.

+ + + + +
FieldDescription
Enable special shield hitpointsWhen checked, sets an absolute + maximum shield strength for this ship, replacing the class + default.
Enable special hitpointsWhen checked, sets an absolute maximum + hull strength for this ship, replacing the class default.
+ + diff --git a/qtfred/help-src/doc/dialogs/ShipTextureReplacementDialog.html b/qtfred/help-src/doc/dialogs/ShipTextureReplacementDialog.html new file mode 100644 index 00000000000..5efc835ad20 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/ShipTextureReplacementDialog.html @@ -0,0 +1,36 @@ + + + + + Texture Replacement - QtFRED Help + + + + +

Texture Replacement

+

Accessed via the Texture Replacement button in the Ship Editor.

+

Overrides textures on this ship instance without modifying the ship class or its +table entry.

+ +

Texture list

+

The left panel lists all base diffuse textures on the ship's model. Select one to +set its replacement. Type the replacement texture name in the New +Texture field, no file extension needed.

+ +

Other Maps

+

When the selected texture slot has additional map types defined on the model +(glow, normal, specular, etc.), this section shows a control for each one. Each +map type has:

+
    +
  • Replace - check to enable a replacement for this map + type.
  • +
  • Inherit - automatically derives the replacement name by + appending the map type suffix to the main texture replacement name (e.g. + replacing fighter01 with myfighter and inheriting + the normal map produces myfighter-normal). Uncheck to enter the + name manually.
  • +
+

Models that only define a base diffuse texture will show no controls in this +section.

+ + diff --git a/qtfred/help-src/doc/dialogs/ShipWeaponsDialog.html b/qtfred/help-src/doc/dialogs/ShipWeaponsDialog.html new file mode 100644 index 00000000000..32182e89bf1 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/ShipWeaponsDialog.html @@ -0,0 +1,31 @@ + + + + + Ship Weapons - QtFRED Help + + + + +

Ship Weapons

+

Accessed via the Weapons button in the Ship Editor.

+

Assigns weapons to the ship's banks and turrets for this mission instance, +overriding the ship class defaults.

+ +

Mode

+

Select Primary, Secondary, or +Turret to switch which set of banks is shown. The +Tertiary option is not currently implemented.

+ +

Assigning weapons

+

The right panel lists the banks or turrets for the selected mode. Select the bank +or turret you want to change. Then select the desired weapon from the left panel and +click Set Selected to assign it.

+

Where a weapon uses ammo, the ammo count appears as a second column next to the +weapon name. Click the value to edit the starting ammo for that bank directly.

+ +

Turret AI class

+

When in Turret mode, a dropdown and button at the bottom allow the AI class of the +selected turret to be changed independently of the ship's overall AI class.

+ + diff --git a/qtfred/help-src/doc/dialogs/SoundEnvironmentDialog.html b/qtfred/help-src/doc/dialogs/SoundEnvironmentDialog.html new file mode 100644 index 00000000000..75fe39a9b2b --- /dev/null +++ b/qtfred/help-src/doc/dialogs/SoundEnvironmentDialog.html @@ -0,0 +1,29 @@ + + + + + Sound Environment - QtFRED Help + + + + +

Sound Environment

+

Accessed via Sound Environment in the Mission Specs dialog.

+

Applies an environmental reverb effect to in-game sound effects and messages. +Select a preset from the list as a starting point, then adjust the individual +parameters to taste.

+ +

Parameters

+ + + + + +
ParameterDescription
VolumeOverall level of the reverb effect mixed in with the dry + signal. Higher values make the environment sound more pronounced.
DampingHow quickly high frequencies are absorbed as the reverb + tail decays. Higher damping produces a warmer, more muffled reverb, as if + the environment has soft or absorptive surfaces.
Decay timeHow long the reverb tail rings out after a sound + stops, in seconds. Longer values simulate large or reflective spaces such as + hangars or caverns; shorter values suggest small or dampened rooms.
+ + diff --git a/qtfred/help-src/doc/dialogs/TeamLoadoutDialog.html b/qtfred/help-src/doc/dialogs/TeamLoadoutDialog.html new file mode 100644 index 00000000000..44462617217 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/TeamLoadoutDialog.html @@ -0,0 +1,86 @@ + + + + + Team Loadout - QtFRED Help + + + + +

Team Loadout

+

Opens via Editors › Team Loadout.

+

Defines the pool of ships and weapons players can choose from at the loadout screen +before a mission. The dialog has two tabs: Static Loadout for fixed +class selections and Variable Loadout for runtime-dynamic +selections driven by SEXP variables.

+ +
Team Loadout only applies to missions with the type set to +Team vs. Team in Mission Specs.
+ +

Top controls

+ + + + + +
ControlDescription
Team selectorSwitches which team's loadout is being edited.
Copy to other teamCopies the current team's loadout to all other + teams.
Skip FRED's Weapon Loadout ValidationBypasses FRED's internal + validation of the weapon loadout across all teams. Applied globally regardless + of which team is selected. Required when using the Variable Loadout tab. + Enabling this marks the mission as requiring FSO 23.3 or + later.
+ +

Static Loadout tab

+

The left pane lists all player-flyable ships available in the current mod; the right +pane lists all player-usable weapons. Both panes can be filtered by name. The +Clear, Select All, and +Multi-select controls are UI conveniences for selecting list entries +and have no effect on the mission itself.

+ +

Per-item controls

+ + + + + + + +
ControlDescription
Enabled (checkbox)Adds or removes the ship or weapon from the + loadout pool. Items already present in the mission's starting wings (defined + in Mission Specs › + Custom Wing Names) are automatically + enabled and shown with a partial check. They cannot be removed from the pool + since they are already committed to those wings.
In wingsRead-only count of how many of this ship or weapon are + already accounted for in the mission's starting wings.
ExtraAdditional quantity made available beyond what is already in + wings.
Extra from variableA number variable whose value is added to the + available quantity at runtime. Useful for adjusting pool sizes dynamically + via SEXPs during the mission.
Required (weapons only)When checked, the mission requires that + this weapon be loaded onto a player or allied ship before the player is + allowed to launch.
+ +

Variable Loadout tab

+

Variable loadout entries use SEXP string variables to define which ship or weapon +class is available, allowing the class itself to change at runtime. This is useful for +missions where the available ships or weapons depend on earlier mission choices or +campaign state.

+

Each entry works as follows: a string variable's value is the name of the +ship or weapon class to offer. Because SEXPs can change that variable's value during +the mission (or in a previous mission if the variable is persistent), the class offered +to players can vary dynamically. An additional number variable can optionally control +the quantity available.

+

Variable entry controls

+ + + + + + + +
ControlDescription
Enabled (checkbox)Includes this variable entry in the + loadout.
VariableThe string variable whose value names the ship or weapon + class to offer.
ValueShows the variable's current default value (i.e. the class + it resolves to at mission start).
ExtraStatic quantity available in addition to any count + variable.
Extra variableA number variable controlling the available quantity + at runtime.
+ + diff --git a/qtfred/help-src/doc/dialogs/VariablesAndContainersDialog.html b/qtfred/help-src/doc/dialogs/VariablesAndContainersDialog.html new file mode 100644 index 00000000000..00a4aaea540 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/VariablesAndContainersDialog.html @@ -0,0 +1,84 @@ + + + + + Variables & Containers - QtFRED Help + + + + +

Variables & Containers

+

Opens via Editors › Variables.

+

Declares named variables and containers that SEXPs +can read and write during the mission. Variables hold a single value; containers hold +collections of values. Both are referenced in SEXPs by name and can be used throughout +the event system to track state, accumulate data, and communicate between events.

+ +

Variables tab

+

The main list shows all variables in the mission with their name and default value. +String type variables are colored blue while numeric type are color orange. +The list is filterable. Use Add, Copy, and +Delete to manage entries. Double-click a variable's name or value to +edit it inline.

+ +

Variable properties

+ + + + + +
FieldDescription
NameThe identifier used to reference this variable in + SEXPs.
Default valueThe value the variable starts with at the beginning + of the mission.
TypeNumber or String. Variables are explicitly + typed and the type cannot be changed at runtime.
+ +

Persistence

+

Controls when a variable's value is saved between missions. The +Eternal checkbox (only available when a persistence mode is +selected) controls where it is saved: unchecked saves to the campaign file +(value is scoped to the current campaign); checked saves to the player's pilot file +(value persists across all campaigns and is always available).

+ + + + + +
SettingDescription
No persistenceThe variable exists only for the duration of this + mission. Its value is reset each time the mission starts. Eternal is not + available.
Save on Mission CompletedThe variable's value is saved when the + player accepts the mission outcome in the debriefing. If the player quits + without accepting, the value is not saved.
Save on Mission CloseThe variable's value is saved whenever the + mission closes, whether the player accepted, quit, or restarted.
+ +

Network variable

+

When checked, the variable's value is broadcast to all players whenever it is +modified during a multiplayer mission, keeping it synchronized across the +session.

+ +

Containers tab

+

Containers hold collections of values. The upper list shows all containers in the +mission and is filterable. List containers are colored blue while map containers are +colored orange. Use Add, Copy, and Delete +to manage containers. Double-click a container's name to edit it inline. Containers +have the same persistence and network settings as variables.

+ +

Container type

+ + + + +
TypeDescription
ListAn ordered sequence of values, all of the same type (Number + or String). Items can be added, removed, and iterated in SEXPs.
MapA collection of key-value pairs where each key maps to a + value. Keys and values each have their own type (Number or String, + independently). For example, a map with string keys and number values can + associate ship names with scores, or a map with string keys and string values + can work like a set of named labels. SEXPs access map entries by key.
+ +

Container contents

+

The lower box shows the contents of the selected container and is also filterable. +Use Add, Copy, and Delete to +manage entries, and the arrow buttons to reorder them. Double-click a key or value to +edit it inline. For Map containers, the Swap button exchanges a key +and its value.

+ + diff --git a/qtfred/help-src/doc/dialogs/VoiceActingManager.html b/qtfred/help-src/doc/dialogs/VoiceActingManager.html new file mode 100644 index 00000000000..65233e45f2f --- /dev/null +++ b/qtfred/help-src/doc/dialogs/VoiceActingManager.html @@ -0,0 +1,94 @@ + + + + + Voice Acting Manager - QtFRED Help + + + + +

Voice Acting Manager

+

Opens via Editors › Voice Acting Manager.

+

A tool for generating voice acting scripts and programmatically assigning voice file +paths across all spoken lines in a mission. It covers messages created in the Mission +Events editor, briefing stages, command briefing stages, and debriefing stages.

+ +

Abbreviations

+

Abbreviations define how voice filenames are automatically constructed. The +Example box shows a live preview as you fill in the fields.

+ + + + + + + + + + +
FieldDescription
Campaign abbreviationShort code identifying the campaign. Placed + at the start of every filename, combined with the mission + abbreviation.
Mission abbreviationShort code identifying the mission. Combined + with the campaign abbreviation at the start of the filename.
Cmd. BriefingString inserted into filenames for command briefing + lines.
BriefingString inserted into filenames for briefing lines.
DebriefingString inserted into filenames for debriefing + lines.
MessageString inserted into filenames for in-mission message + lines.
ExtensionPreferred audio file extension: .wav or + .ogg.
Include sender nameWhen checked, the name of the sending ship or + persona is included in the filename for message lines.
+ +

Generating filenames

+

Replace existing filenames - when checked, the generator will +overwrite voice file assignments that are already set. When unchecked, only lines +with no assignment are filled in.

+

Generate filenames applies the abbreviation rules to every line in +the mission, setting voice file paths according to the options above.

+ +

Script options

+

Controls how the exported script document is formatted and what it contains.

+ + + + + +
FieldDescription
FormatA free-form template defining how each line appears in the + exported script. The available substitution tokens are listed in the dialog + itself.
ExportRadio buttons selecting which source to include in the + script: everything, messages only, briefing stages only, command briefing + stages only, or debriefing stages only.
Group sent-message-list messages togetherWhen checked, messages in + the script are reordered to match the sequence they appear in mission events + (send-message-list, send-message-chain, and send-random-message operations), + rather than the default message index order. Any messages not referenced in + events are appended at the end. This is useful for voice directors who want + to work through the script in mission flow order.
+

Generate script writes the script file to disk using the format +and export settings above.

+ +

Sync Personas

+

Tools for keeping persona assignments consistent between ships and the messages they +send. The persona filter drop-down scopes all operations in this +section to a specific set of personas: wingman personas only, non-wingman personas +only, or a single named persona.

+ + + + + + + +
ButtonDescription
Copy message personas to shipsFor each mission message that has a + persona assigned, finds the ship that sends it and sets that ship's persona + to match.
Copy ship personas to messagesReverse of the above; for each + ship with a persona set, updates the messages it sends to use that same + persona.
Clear personas from ships that don't send messagesRemoves the + persona assignment from any ship that has one but does not send any messages. + Useful for cleaning up personas that were set by mistake or are no longer + needed.
Set head ANIs using personas in messages.tblFor each message with + a persona assigned, looks up a matching built-in message in + messages.tbl with the same persona and copies its head + animation assignment.
Check if messages sent by <any wingman> have at least one ship with + that personaValidates that every message addressed to + <any wingman> has a wingman persona assigned and that at + least one ship in the mission carries that persona. Reports any + discrepancies.
+ + diff --git a/qtfred/help-src/doc/dialogs/VolumetricNebulaDialog.html b/qtfred/help-src/doc/dialogs/VolumetricNebulaDialog.html new file mode 100644 index 00000000000..5dc942bd901 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/VolumetricNebulaDialog.html @@ -0,0 +1,96 @@ + + + + + Volumetric Nebula - QtFRED Help + + + + +

Volumetric Nebula

+

Opens via Editors › Volumetric Nebula.

+

Configures the volumetric nebula renderer. Unlike the classic nebula in the +Background Editor, the volumetric nebula is +rendered as a true 3D volume with light scattering, emissive glow, and procedural noise +shaping.

+ +

Key fields

+ + + + + + +
FieldDescription
EnabledAdds the volumetric nebula to the mission. Uncheck to + disable without losing settings.
ModelPOF model that defines the outer boundary shape of the nebula + volume. The mesh must be watertight; multiple disjunct meshes are allowed as + long as their polygons do not intersect.
Position (X, Y, Z)World-space position of the nebula + volume.
Color (RGB)Primary color the nebula is rendered with.
+ +

Rendering

+ + + + + + + + + + + +
FieldDescription
OpacityTarget translucency - the maximum opacity the nebula + reaches at full depth. Cannot be set to 0.
Opacity distanceDistance in metres a viewer must be inside the + nebula before it reaches its target opacity.
Render quality stepsNumber of steps used to accumulate nebula + density along each ray. Higher values reduce banding on objects inside the + volume; primarily affects frame rate.
ResolutionResolution of the nebula's 3D density texture, + stored as a power of 2 (5–8 recommended). Higher values cost more + VRAM.
OversamplingOversampling factor for the 3D texture (1–3). + Each increment quadruples loading time but reduces edge banding, especially + at lower resolutions. Primarily affects load time.
SmoothingControls how softly the hull POF's edges blend into + the surrounding nebula. Expressed as a fraction of the nebula size.
Henyey-Greenstein coefficientControls light scattering + direction. Values greater than 0 produce a cloud-like shine-through effect; + values less than 0 produce a highly reflective nebula. Practical range is + roughly 0.05–0.75.
Sun falloff factorScales how the sun's illumination falls off + with nebula depth. Values greater than 1 make the nebula's interior brighter + than physically accurate; values less than 1 make it darker.
Sun quality stepsNumber of steps taken toward the sun per + nebula slice when computing shadowing. Higher values reduce light/shadow + banding; primarily affects frame rate.
+ +

Emissive

+ + + + + +
FieldDescription
Emissive lightIntensity of the emissive glow added by light + sources inside the nebula. Higher values produce brighter + emissives.
IntensityControls how quickly an emissive light source widens + inside the nebula. Larger values spread the glow more broadly even where + there is little nebula to obscure it.
Emissive light falloffCorrects emissive alpha near the source. + Values greater than 1 darken the emissive close to its origin, producing a + more even spread; values less than 1 brighten it near the origin, making it + more intense at the center.
+ +

Noise

+

Procedural noise is layered on top of the base volume to break up uniform density +and create wispy internal structure.

+ + + + + + + + + + +
FieldDescription
Enable noiseToggles procedural noise on or off without losing + the noise settings.
Color (RGB)Tint color applied to the noise layer.
Scale baseSize of the primary noise layer in metres. The ratio + between Scale base and Scale sub should have a large denominator to avoid + visible harmonic patterns in the result.
Scale subSize of the secondary noise layer in metres.
IntensityOverall strength of the noise effect.
ResolutionResolution of the noise 3D texture, stored as a + power of 2 (5–8 recommended). Higher values cost more VRAM.
Set Base Noise FunctionNot yet implemented. Will allow a custom + ANL noise expression to replace the default base noise layer.
Set Sub Noise FunctionNot yet implemented. Will allow a custom + ANL noise expression to replace the default sub noise layer.
+ + diff --git a/qtfred/help-src/doc/dialogs/WaypointEditorDialog.html b/qtfred/help-src/doc/dialogs/WaypointEditorDialog.html new file mode 100644 index 00000000000..32ec880f2e5 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/WaypointEditorDialog.html @@ -0,0 +1,37 @@ + + + + + Waypoint Editor - QtFRED Help + + + + +

Waypoint Editor

+

Opens via Editors › Waypoints.

+

Creates and edits waypoint paths. Each path is an ordered list of 3D points. Ships +given a waypoint goal fly through the points in sequence, then optionally loop. +Waypoint paths are simply ordered sequences of arbitrary positions in space and can +be used for any purpose a mission designer can express through SEXPs and AI goals.

+ +

Key fields

+ + + + + + + +
FieldDescription
Waypoint pathSelects which path to edit. New paths are created by + placing waypoint objects in the viewport.
NameIdentifies the path in SEXPs and AI goal assignments. Must be + unique.
LayerThe layer the waypoint path is assigned to.
No Draw LinesWhen checked, the connecting lines between waypoints + are hidden in the viewport. The waypoint points themselves are still + visible.
Custom ColorWhen checked, enables the RGB fields below to set a + custom color for rendering this path's points and lines in the viewport. + Does not affect in-game appearance.
+ +
Individual waypoint positions are moved by selecting the waypoint +object in the viewport and dragging, or by editing its coordinates in the +Object Orientation Editor.
+ + diff --git a/qtfred/help-src/doc/dialogs/WaypointPathGeneratorDialog.html b/qtfred/help-src/doc/dialogs/WaypointPathGeneratorDialog.html new file mode 100644 index 00000000000..874adc8ff7f --- /dev/null +++ b/qtfred/help-src/doc/dialogs/WaypointPathGeneratorDialog.html @@ -0,0 +1,43 @@ + + + + + Waypoint Path Generator - QtFRED Help + + + + +

Waypoint Path Generator

+

Opens via Tools › Waypoint Path Generator.

+

Automatically generates a waypoint path whose +points are arranged in a geometric shape around a center point. Useful for creating +patrol circuits, orbit paths, and similar regular formations without placing each +point by hand.

+ +

Fields

+ + + + + + + + + + +
FieldDescription
NameName assigned to the generated waypoint path.
Center pointThe origin around which all points are placed. Enter + coordinates manually, or select an object from the drop-down to use its + current position as the center.
AxisThe axis the shape revolves around. For example, choosing the + Y axis produces a flat horizontal ring; choosing X or Z tilts it + upright.
Number of pointsHow many waypoints to place per loop around the + center. Total waypoints created equals points × loops.
LoopsHow many full revolutions around the center point the path + makes. Combined with Number of points, this produces a helix or spiral + when Drift is also set.
RadiusDistance from the center point to each generated + waypoint.
DriftOffsets each successive point by this amount along the chosen + axis, perpendicular to the revolution plane. A non-zero drift turns a flat + ring into a helix.
Variance (X, Y, Z)Adds a random offset to each point's + coordinates. The X, Y, Z values set the approximate maximum displacement in + each axis; the actual offset applied to each point is randomized within that + range. Use this to break up overly regular paths.
+ + diff --git a/qtfred/help-src/doc/dialogs/WingEditorDialog.html b/qtfred/help-src/doc/dialogs/WingEditorDialog.html new file mode 100644 index 00000000000..29108ca0336 --- /dev/null +++ b/qtfred/help-src/doc/dialogs/WingEditorDialog.html @@ -0,0 +1,111 @@ + + + + + Wing Editor - QtFRED Help + + + + +

Wing Editor

+

Opens via Editors › Wings.

+

Creates and configures wings. A wing groups ships that share arrival and departure +cues and can receive collective AI goals. Wings support multiple waves and each wave is +a fresh copy of the wing that spawns when the previous wave drops below the threshold.

+ +

General fields

+ + + + + + + + + + +
FieldDescription
NameWing name shown on the HUD and used in SEXPs + (e.g. Alpha, Gamma).
Wing leaderWhich ship in the wing acts as leader. Other members + form up on the leader.
Number of wavesHow many waves of this wing will arrive over the + course of the mission. Each wave is a full copy of the wing's ships. The first + wave is included in this count.
Wave thresholdWhen the number of surviving ships in the current + wave drops to this value or below, the next wave spawns. Set to 0 to require + the entire wave to be destroyed before the next arrives.
HotkeyFunction key (F5–F12) that selects this wing during + play.
FormationThe flight formation wing members use relative to the + leader.
Formation scaleScales the formation spacing up or down.
Squad logoLogo displayed on the ships in this wing. Choose from + the gallery of available insignia.
+ +

Buttons

+ + + + + + + + +
ButtonDescription
Align FormationRepositions all ships in the wing in the editor + viewport to match the currently selected formation. Has no effect at + runtime.
Prev / NextMoves the selection to the previous or next wing, + allowing sequential editing without switching back to the viewport.
Delete WingRemoves the wing and all of its ships from the + mission.
Disband WingDissolves the wing without deleting its ships. Each + ship becomes an independent, unassociated object in the mission.
Initial OrdersAI orders + all ships in the wing follow at mission start.
Wing FlagsOpens a checklist of per-wing behavior flags. Each flag + has a tooltip describing its effect.
+ +

Arrival & Departure

+

Controls when and where the wing enters and leaves the mission. The cue SEXP must +evaluate to true before arrival or departure can occur. Most arrival locations require +a target ship or wing to be selected. Departure location options are limited to +Hyperspace or To Dock Bay.

+ +

Arrival locations

+ + + + + + + + + + + +
LocationNotes
HyperspaceWarps in from hyperspace. No target required.
Near ShipAppears near the target at the specified distance.
In Front of ShipAppears in front of the target at the specified + distance.
In Back of ShipAppears behind the target at the specified + distance.
Above ShipAppears above the target at the specified + distance.
Below ShipAppears below the target at the specified + distance.
To Left of ShipAppears to the left of the target at the specified + distance.
To Right of ShipAppears to the right of the target at the + specified distance.
From Dock BayLaunches from the target ship's dock bay. Use + Restrict Paths to limit which bay paths are used.
+ +

Fields

+ + + + + + + + + + + +
FieldDescription
TargetThe ship or wing this arrival or departure is relative to. + Required for all locations except Hyperspace.
DistanceDistance from the target in meters. Arrival only; + available for positional locations (Near Ship, In Front of Ship, + etc.).
DelaySeconds after the cue becomes true before the wing + arrives or departs.
Delay between waves (min / max)After a new wave is triggered by + the threshold, the engine waits a random number of seconds between these two + values before the wave actually spawns. Applies to all waves after the + first.
Restrict PathsSelects which dock bay paths to use. Only available + when the location is From Dock Bay or To Dock Bay.
Custom Warp ParamsOpens + a sub-dialog to configure custom warp appearance and behavior.
CueSEXP that must evaluate + to true for arrival or departure to occur.
No warp effectWhen checked, the wing appears or leaves without + the visual warp effect.
Don't Adjust Warp When DockedBy default, ships that are docked + together warp in or out with an enlarged effect sized to cover the combined + formation. When checked, each ship instead uses its individual warp effect + size, ignoring the docked aggregate.
+ + diff --git a/qtfred/help-src/doc/fundamentals.html b/qtfred/help-src/doc/fundamentals.html new file mode 100644 index 00000000000..c6eb1410efa --- /dev/null +++ b/qtfred/help-src/doc/fundamentals.html @@ -0,0 +1,187 @@ + + + + + Fundamentals - QtFRED Help + + + + +

Fundamentals

+ +

This tutorial walks you through building a small but complete mission with events, +waypoints, messages, and a briefing. It assumes you have read the +Quickstart. Budget about 30 minutes.

+ +

The mission we are building: the player escorts a friendly transport along a waypoint +route to rendezvous with a friendly capital ship. Hostile fighters arrive shortly +after the mission starts. If the transport completes its route, the mission +succeeds.

+ +
Save often. Press Ctrl+S every few +minutes. Test in-game after each major step. Catching problems early saves a lot of +rework later.
+ +

1. Plan before you open QtFRED

+

Before placing anything, sketch out what the mission needs:

+
    +
  • A player fighter wing (Alpha)
  • +
  • A friendly transport that needs to reach a rendezvous point
  • +
  • A friendly capital ship at the destination, set as a protect target
  • +
  • A hostile fighter wing that arrives ~30 seconds in
  • +
  • A waypoint path for the transport to follow
  • +
  • Events: a timed trigger for the hostile arrival, a radio message when they arrive
  • +
  • A primary mission goal tied to the transport completing its route
  • +
  • A two-stage briefing and a debriefing
  • +
+

Knowing this upfront prevents building yourself into a corner. You will still +adjust things as you go and that is normal.

+ +

2. Place the ships

+

Open a new mission. The player ship is already there. Select it and click +Form Wing, name the wing Alpha. Move Alpha 1 to roughly +0, 0, -2000 using the +Object Orientation Editor.

+ +

Place the friendly transport near the origin. Open the +Ship Editor, name it +Transport 1, set its team to Friendly, and set its +initial velocity to 0. It will start stationary and move when given orders.

+ +

Place a friendly capital ship some distance away at the end of where the +transport's route will go. Name it Command Ship and set it to +Friendly. Open its Ship Editor and click +Flags to open the +Ship Flags subdialog. Enable +Protect Ship. This tells hostile AI not to target or attack it, +making it a safe destination for the transport to reach. Velocity 0 is fine here +too.

+ +

Place two hostile fighters somewhere off to the side. Select both, click +Form Wing, name the wing Epsilon, set team to +Hostile. Open the Arrival / Departure tab in the Ship Editor for +each Epsilon ship and set arrival location to Hyperspace. Leave +the arrival cue as true for now.

+ +
Save and do a quick test. All ships should appear. Epsilon will +arrive immediately and that is fine for now.
+ +

3. Add a waypoint path for the transport

+

Select the waypoint tool in the toolbar and Ctrl+click in the viewport +to lay out a short route from the transport's position toward the Command Ship. +Three or four points is plenty. Open the +Waypoint Editor and name the path +Transport Route.

+ +

Open the Ship Editor for Transport 1, go to +Initial Orders, and set its initial goal to +Waypoints OnceTransport Route. The +transport will fly the route once from start to finish.

+ +

Save and test. The transport should start moving along the route.

+ +

4. Delay the hostile arrival with an event

+

Open Editors › Mission Events. Click Add +event and name it Epsilon Arrives. Set its +Condition SEXP to:

+
(has-time-elapsed 30)
+

No action is needed in this event. It acts purely as a named trigger that other +things can reference.

+ +

Now go back to the Ship Editor for each ship in Epsilon wing. In the +Arrival / Departure tab, change the +Arrival cue SEXP to:

+
(is-event-true-delay 0 "Epsilon Arrives")
+

This way all of Epsilon's ships key off the same named event. If you want to +change the delay later, you only need to edit the event's condition in one place.

+ +

Save and test. The first 30 seconds should be quiet, then Epsilon jumps in.

+ +

5. Send a message when the hostiles arrive

+

Messages are created and sent from the same +Mission Events editor. On the right +side of the dialog, click Add under the message list to create a +new message. Give it the name Epsilon Inbound and write a short line of +text such as "Hostile fighters inbound! Protect the transport!" Set a +sender if desired, or leave it as <mission>.

+ +

Now create a new event and name it Epsilon Arrival Message. Set its +Condition to:

+
(has-arrived-delay 0 "Epsilon 1")
+

In the Actions tree, add a send-message SEXP and +select Epsilon Inbound as the message. Leave the priority at +Normal.

+ +

Save and test. The message should appear on screen as Epsilon jumps in.

+ +

6. Add a mission goal

+

Open Editors › Mission Goals. Add a primary goal:

+
    +
  • Description: Escort the transport to the rendezvous
  • +
  • Completion condition: + (are-waypoints-done-delay 0 "Transport 1" "Transport Route")
  • +
+

Add a secondary goal for destroying Epsilon wing if you like. See +SEXPs for an overview of how conditions are +built.

+ +

7. Write the briefing

+

Write the briefing now. Be creative and give the player a reason to protect +the transport from Epsilon wing.

+ +

Open Editors › Briefing. Build two stages:

+ + + + +
StageCameraText
1Wide view of the whole sceneExplain the mission: + escort the transport safely to the Command Ship.
2Closer on the transport's routeWarn that hostile + fighters are expected; the player must intercept them while keeping + the transport moving.
+ +

For each stage, position the briefing map camera, add icons for the relevant +ships using Make icon from ship, and type the stage text at the +bottom. See the Briefing Editor +help for full details. Set the Music track at the top of the +dialog.

+ +

Save and walk through the briefing before launching to check camera positions and +icon placement.

+ +

8. Write the debriefing

+

Open Editors › Debriefing. Add two stages:

+ + + + + + + + +
StageUsage formulaText
Success(are-waypoints-done-delay 0 "Transport 1" "Transport Route")The transport completed its route. Commend the player.
Failure(is-destroyed-delay 0 "Transport 1")The transport was lost. Acknowledge the failure.
+

Assign music tracks for success and failure in the +Debriefing music section.

+ +

9. Final test

+

Save and run a full play-through. Check:

+
    +
  • The transport moves along its route from the start
  • +
  • The first 30 seconds are calm
  • +
  • Epsilon and the arrival message both trigger at 30 seconds
  • +
  • The primary goal completes when the transport finishes the route
  • +
  • The briefing camera moves and icons look correct
  • +
  • The correct debriefing stage shows depending on outcome
  • +
+ +

Next steps

+
    +
  • Add a second hostile wave that arrives when the transport is near the end + of its route, using are-waypoints-done-delay on an earlier + waypoint as the trigger.
  • +
  • Add a Command Briefing + before the main briefing.
  • +
  • Chain this into a campaign using the + Campaign Editor.
  • +
+ + diff --git a/qtfred/help-src/doc/general/LayerManagerDialog.html b/qtfred/help-src/doc/general/LayerManagerDialog.html new file mode 100644 index 00000000000..5b92bc38cd2 --- /dev/null +++ b/qtfred/help-src/doc/general/LayerManagerDialog.html @@ -0,0 +1,31 @@ + + + + + Layer Manager - QtFRED Help + + + + +

Layer Manager

+

Opens via View › Layer Manager.

+

Organizes mission objects into named layers. Layers are an editor-only concept and +have no effect on mission gameplay. They exist purely to help manage visibility in the +viewport while editing.

+ +

Layers tab

+

Lists all layers in the mission. Each layer has a checkbox; uncheck it to hide all +objects on that layer in the viewport, check it to show them again. Newly placed objects +are automatically assigned to the Default layer.

+

Use Add to create a new layer. Use Delete to remove +the selected layer. The Default layer cannot be deleted.

+ +

Filters tab

+

Works in conjunction with layers to provide additional visibility control. Allows +hiding objects by type or by IFF team regardless of which layer they are on.

+ +
Hiding objects via layers or filters does not delete or disable them. +Hidden objects are still present in the mission file and will function normally +in-game.
+ + diff --git a/qtfred/help-src/doc/general/PreferencesDialog.html b/qtfred/help-src/doc/general/PreferencesDialog.html new file mode 100644 index 00000000000..37bf7a137ef --- /dev/null +++ b/qtfred/help-src/doc/general/PreferencesDialog.html @@ -0,0 +1,29 @@ + + + + + Preferences - QtFRED Help + + + + +

Preferences

+

Opens via File › Preferences.

+

Controls QtFRED editor settings. Changes take effect immediately; there is no +Apply button.

+ +

General

+

Miscellaneous editor options. Hover over each setting for a tooltip +description.

+ +

Grid

+

Configures the reference grid shown in the main viewport. You can set which +world-space plane the grid lies on (XZ, XY, or YZ) and adjust the grid's center +point to move it to a different position in the world. Use Reset +grid to restore the plane and center to their defaults.

+ +

Controls

+

Allows remapping the default input controls, such as camera movement and +viewport navigation. Hover over each entry for a description of what it does.

+ + diff --git a/qtfred/help-src/doc/general/SceneBrowserDialog.html b/qtfred/help-src/doc/general/SceneBrowserDialog.html new file mode 100644 index 00000000000..07a6b9a477d --- /dev/null +++ b/qtfred/help-src/doc/general/SceneBrowserDialog.html @@ -0,0 +1,45 @@ + + + + + Scene Browser - QtFRED Help + + + + +

Scene Browser

+

Opens via View › Scene Browser.

+

Shows a complete list of every object in the mission, organized by layer and then by object +type within each layer. Provides an alternative to clicking objects in the viewport for +selection and contextual actions.

+ +

Selecting objects

+

Click any object in the list to select it. Hold Ctrl or Shift +to add to the selection.

+

Right-click an object in the list to open the same context menu that +appears when right-clicking that object in the viewport.

+ +

Filtering

+

Use the search box to show only objects whose names match the typed text.

+

Use the IFF checkboxes to show or hide objects by IFF team. Unchecking +an IFF removes all objects belonging to that team from the list.

+ +

Selection shortcuts

+

Three buttons act only on the objects currently visible in the filtered list. Objects +hidden by the search or IFF filters are not affected:

+
    +
  • All - selects every visible object in the list.
  • +
  • Clear - deselects everything.
  • +
  • Invert - selects all currently unselected visible objects and + deselects all currently selected ones.
  • +
+ +

Layer visibility

+

Each layer row in the list has a checkbox. Uncheck it to hide all objects on that layer +in the viewport; check it to show them again. This is the same visibility toggle available +in the Layer Manager.

+ +
Hiding objects via the layer checkbox does not delete or disable them. +Hidden objects remain in the mission file and function normally in-game.
+ + diff --git a/qtfred/help-src/doc/general/Toolbars.html b/qtfred/help-src/doc/general/Toolbars.html new file mode 100644 index 00000000000..4ffec618211 --- /dev/null +++ b/qtfred/help-src/doc/general/Toolbars.html @@ -0,0 +1,347 @@ + + + + + Toolbars - QtFRED Help + + + + +

Toolbars

+

QtFRED has four toolbars: the icon toolbar, the +context bar, the transform bar, +and the status bar.

+ +

Icon Toolbar

+

The icon toolbar provides quick access to the most common editing modes and actions. +The active mode determines how clicks and drags behave in the +viewport.

+ +

Tools

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ToolShortcutDescription
Mode
SelectSClick to select objects. Drag on empty space to box-select.
Select and MoveMClick to select objects; drag a selected object to move it.
Select and RotateRClick to select objects; drag a selected object to rotate it.
Axis constraints
X ConstraintConstrains move or rotate to the X axis.
Y ConstraintConstrains move or rotate to the Y axis.
Z ConstraintConstrains move or rotate to the Z axis.
XZ ConstraintConstrains move or rotate to the XZ plane.
YZ ConstraintConstrains move or rotate to the YZ plane.
XY ConstraintConstrains move or rotate to the XY plane.
Selection
Scene BrowserHToggles the Scene Browser panel, which lists all objects in the + mission.
Lock SelectionSpacePrevents the current selection from changing.
Wings
Form WingCtrl+WForms a wing from the currently selected ships.
Disband WingCtrl+DDisbands the wing of the currently selected ships, returning them to + individual objects.
View
Zoom on SelectedAlt+ZMoves the camera to focus on the selected objects.
Zoom ExtentsShift+ZZooms the camera to fit all objects in the mission into view.
Show DistancesDToggles display of distance measurements between selected objects.
Rotate Around ObjectAlt+RToggles camera orbit mode, rotating the view around the selected + object.
Layers
Manage View LayersOpens the Layer Manager to + show or hide categories of objects.
Show All LayersMakes all hidden layers visible.
+ +

Object creation dropdowns

+

Two dropdowns at the right end of the toolbar control what is created when you +click in the viewport.

+ + + + + + + + + + + + +
DropdownUsed byDescription
ShipsCtrl+clickSelects the ship class, waypoint, or jump node to place. The object + created on Ctrl+click is determined by this selection.
PropsCtrl+Shift+clickSelects the prop class to place. The prop created on + Ctrl+Shift+click is determined by this selection.
+ +

Context Bar

+

The context bar sits below the icon toolbar. Its left side shows a label describing +the current selection; the right side shows action buttons that change depending on +what is selected.

+ +

Selection label

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Selection stateLabel text
Nothing selectedNo Selection
Single ship or player startShip: <name> [<class>] - appends  | Wing: <name> when the + ship belongs to a wing.
Single waypointWaypoint: <name> - appends  | List: <name> when a waypoint + list is active.
Single jump nodeJump Node: <name>
Single propProp: <name>
Multiple objects selectedA comma-separated count by type, e.g. 2 ship(s), 1 waypoint(s) selected. + Appends  | Wing: <name> when all selected ships share the + same wing.
+ +

Action buttons

+

Buttons appear to the right of the label and are rebuilt whenever the selection +changes. The set of buttons depends on the type and count of selected objects.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ButtonAppears whenAction
RenameExactly one ship selectedPrompts for a new name and renames the ship immediately. For + waypoints, props, and jump nodes, use the object's editor instead.
Edit ShipOne or more ships selected (all must be ships)Opens the Ship Editor.
Edit Waypoint PathExactly one waypoint selectedOpens the Waypoint Editor.
Edit Jump NodeExactly one jump node selectedOpens the Jump Node Editor.
Edit PropExactly one prop selectedOpens the Prop Editor.
Position/OrientationAny single object selectedOpens the Object Orientation Editor.
Edit WingSelected ship(s) belong to a wingOpens the Wing Editor for that wing.
Select WingSelected ship(s) belong to a wingMarks all ships in the wing, replacing the current selection.
CloneExactly one object selectedDuplicates the selected object.
DeleteAnything selectedDeletes all marked objects.
+ +

Transform Bar

+

The transform bar sits below the context bar. It is divided into two sections: a +left section with camera speed controls, and a right section with selection-specific +fields that change based on the active editing mode.

+ +

Camera speed (left section)

+ + + + + + + + + + +
ControlDescription
Camera MoveDropdown setting the camera translation speed. Choices are x1, x2, x3, + x5, x8, x10, x50, x100. Mirrors the Speed › Movement menu.
Camera RotDropdown setting the camera rotation speed. Choices are x1, x5, x12, + x25, x50. Mirrors the Speed › Rotation menu.
+ +

Selection fields (right section)

+

These controls reflect and edit the current selection. Most are disabled when +nothing is selected or when the toolbar is in Select mode.

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ControlDescription
IFFDropdown showing the IFF of the selected ship(s). Enabled only + when at least one ship is selected. Shows blank when the marked ships + have different IFFs. Changing the value applies to all marked ships.
Local toggleIcon button that switches between local and group transform behaviour + for multi-object edits. In Move mode: when on, each object moves + along its own axes independently; when off, all objects move together + as a formation. In Rotate mode: when on, each object rotates + around its own centre; when off, all objects orbit around the primary + selected object. The last setting is remembered separately for Move and + Rotate modes.
Pos X / Y / ZThree spin boxes showing the world-space position of the selected + object. Visible in Select and Move modes. Editing a + value moves the object (or, with the Local toggle off and multiple + objects marked, the whole group) to the entered coordinate. Disabled + in Select mode.
Ori H / P / BThree spin boxes showing the heading, pitch, and bank of the selected + object in degrees. Visible in Rotate mode. Editing a value + rotates the object (or group, depending on the Local toggle).
RadiusRead-only field. For a single selection, shows the bounding radius of + the object. For multiple objects, shows the bounding radius of the + entire selection (distance from the centroid to the farthest marked + object). Shows “--” when nothing is selected.
LayerDropdown showing the layer the selected object(s) belong to. Changing + the value moves all marked objects to the chosen layer. Shows blank + when the marked objects are on different layers.
+ +

Status Bar

+

The status bar runs along the bottom of the window. It has three persistent fields +and a message area that shows transient notifications during loading.

+ + + + + + + + + + + + + + + + + +
FieldPositionDescription
Object countsLeftCounts of objects currently in the mission, e.g. + Ships: 4   WPs: 2   Nodes: 1. + Waypoints and jump nodes are omitted when their count is zero.
ViewpointRightViewpoint: Camera normally; changes to + Viewpoint: <name> when the camera is locked to a + ship's perspective.
UnitsRightUnits = N Meters - the size of one grid square in metres.
+ + diff --git a/qtfred/help-src/doc/general/Viewport.html b/qtfred/help-src/doc/general/Viewport.html new file mode 100644 index 00000000000..28ce1eb5626 --- /dev/null +++ b/qtfred/help-src/doc/general/Viewport.html @@ -0,0 +1,38 @@ + + + + + Viewport - QtFRED Help + + + + +

Viewport

+

The main 3D view where mission objects are placed and arranged.

+ +

Creating objects

+

The toolbar dropdowns determine what type of object is created when you click in +the viewport.

+
    +
  • Ctrl+click - creates a ship, waypoint, or jump node, + depending on the toolbar selection.
  • +
  • Ctrl+Shift+click - creates a prop.
  • +
+ +

Selecting objects

+
    +
  • Click an object to select it.
  • +
  • Click and drag on empty space to draw a selection box and + select all objects within it.
  • +
  • Double-click an object to open its editor dialog.
  • +
+ +

Moving objects

+

Drag a selected object to reposition it in the mission.

+ +

Context menu

+

Right-click anywhere in the viewport to open a context menu with +options relevant to what was clicked. Available options vary depending on whether +you clicked an object, a specific object type, or empty space.

+ + diff --git a/qtfred/help-src/doc/index.html b/qtfred/help-src/doc/index.html new file mode 100644 index 00000000000..1f5c991e196 --- /dev/null +++ b/qtfred/help-src/doc/index.html @@ -0,0 +1,90 @@ + + + + + QtFRED Help + + + +

QtFRED Help

+ +

Welcome to QtFRED - the mission editor for FreeSpace Open. This help +covers the basics of mission creation, the editor dialogs, and advanced topics.

+ +

Getting Started

+
    +
  • Quickstart — create your first flyable mission in a few minutes.
  • +
  • Fundamentals — build a complete mission with events, waypoints, messages, and a briefing.
  • +
+ +

Concepts

+ + +

General

+ + +

Dialogs

+ +

Environment

+ + +

Mission

+ + +

Narrative

+ + +

Objects

+ + +

Tools

+ + + diff --git a/qtfred/help-src/doc/qtfred.qhp b/qtfred/help-src/doc/qtfred.qhp new file mode 100644 index 00000000000..1ed36d9a3fb --- /dev/null +++ b/qtfred/help-src/doc/qtfred.qhp @@ -0,0 +1,208 @@ + + + org.hard-light.qtfred + doc + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + index.html + quickstart.html + fundamentals.html + concepts/sexps.html + concepts/iff.html + concepts/ai.html + concepts/messages.html + concepts/campaign-flow.html + dialogs/CustomDataDialog.html + css/help.css + dialogs/AsteroidEditorDialog.html + dialogs/BackgroundEditorDialog.html + dialogs/BriefingEditorDialog.html + dialogs/CampaignEditorDialog.html + dialogs/CommandBriefingDialog.html + dialogs/DebriefingDialog.html + dialogs/ErrorCheckerDialog.html + dialogs/EventEditor.html + dialogs/FictionViewerDialog.html + dialogs/GlobalShipFlagsDialog.html + dialogs/JumpNodeEditorDialog.html + general/Viewport.html + general/Toolbars.html + general/LayerManagerDialog.html + general/SceneBrowserDialog.html + dialogs/MissionCutscenesDialog.html + dialogs/MissionEventsDialog.html + dialogs/MissionGoalsDialog.html + dialogs/MissionSpecsDialog.html + dialogs/CustomWingNamesDialog.html + dialogs/CustomStringsDialog.html + dialogs/SoundEnvironmentDialog.html + dialogs/MissionStatsDialog.html + dialogs/MusicPlayerDialog.html + dialogs/ObjectOrientEditorDialog.html + general/PreferencesDialog.html + dialogs/PropEditorDialog.html + dialogs/ReinforcementsEditorDialog.html + dialogs/RelativeCoordinatesDialog.html + dialogs/ShieldSystemDialog.html + dialogs/ShipEditorDialog.html + dialogs/ShipAltShipClass.html + dialogs/ShipCustomWarpDialog.html + dialogs/ShipFlagsDialog.html + dialogs/ShipInitialOrdersDialog.html + dialogs/ShipInitialStatusDialog.html + dialogs/ShipPlayerOrdersDialog.html + dialogs/ShipSpecialStatsDialog.html + dialogs/ShipTextureReplacementDialog.html + dialogs/ShipWeaponsDialog.html + dialogs/TeamLoadoutDialog.html + dialogs/VariablesAndContainersDialog.html + dialogs/VoiceActingManager.html + dialogs/VolumetricNebulaDialog.html + dialogs/WaypointEditorDialog.html + dialogs/WaypointPathGeneratorDialog.html + dialogs/WingEditorDialog.html + + + diff --git a/qtfred/help-src/doc/quickstart.html b/qtfred/help-src/doc/quickstart.html new file mode 100644 index 00000000000..b07705ef12d --- /dev/null +++ b/qtfred/help-src/doc/quickstart.html @@ -0,0 +1,67 @@ + + + + + Quickstart - QtFRED Help + + + + +

Quickstart: Your First Mission

+ +

This guide walks you through creating a simple flyable mission from scratch. By the +end you will have a player ship and a hostile wing to fight. The whole thing takes +about five minutes.

+ +

1. Open a new mission

+

Launch QtFRED. Go to File › New. The viewport shows empty +space with a reference grid. QtFRED automatically places one ship, this is your +player start.

+ +

2. Put the player in Alpha wing

+

Click the player ship in the viewport to select it, then click the +Form Wing button in the toolbar. Type Alpha and click +OK. The ship is renamed Alpha 1.

+
The player ship must be in Alpha wing. The engine looks for Alpha 1 +as the default player start.
+ +

3. Add enemies

+

Use the ship class drop-down in the toolbar to select a hostile ship class. It +will appear in red. Hold Ctrl and left-click in the viewport twice to place +two ships. Select both of them (drag a box or shift-click), then click +Form Wing again. Name the wing Epsilon. The ships are +renamed Epsilon 1 and Epsilon 2 and are hostile by default.

+ +

4. Optionally add more waves

+

With Epsilon wing selected, open Editors › Wings to open the +Wing Editor. Increase +Number of waves to 2 and set a short delay between waves. A second +wave will jump in after the first is destroyed.

+ +

5. Fill in Mission Specs

+

Open Editors › Mission Specs. Give the mission a +Title and a short Description. These appear in the +mission selection screen. Set music tracks if desired.

+ +

6. Add a mission goal

+

Open Editors › Mission Goals. Add a primary goal with a +description such as Destroy all hostiles. Set the completion condition to a +SEXP that evaluates to true when Epsilon wing is destroyed, for example +is-destroyed-delay. See SEXPs for an +overview of the scripting system.

+ +

7. Save and play

+

Press Ctrl+S to save. QtFRED saves to your mod's +data/missions/ folder by default. Launch FreeSpace Open, go to the +Tech Room › Mission Simulator › Single Missions, and your mission will +appear there.

+ +

Next steps

+ + + diff --git a/qtfred/help-src/qtfred.qhp b/qtfred/help-src/qtfred.qhp new file mode 100644 index 00000000000..59956fe7416 --- /dev/null +++ b/qtfred/help-src/qtfred.qhp @@ -0,0 +1,205 @@ + + + org.hard-light.qtfred + doc + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + doc/index.html + doc/quickstart.html + doc/fundamentals.html + doc/concepts/sexps.html + doc/concepts/iff.html + doc/concepts/ai.html + doc/concepts/messages.html + doc/concepts/campaign-flow.html + doc/dialogs/CustomDataDialog.html + doc/css/help.css + doc/dialogs/AsteroidEditorDialog.html + doc/dialogs/BackgroundEditorDialog.html + doc/dialogs/BriefingEditorDialog.html + doc/dialogs/CampaignEditorDialog.html + doc/dialogs/CommandBriefingDialog.html + doc/dialogs/DebriefingDialog.html + doc/dialogs/ErrorCheckerDialog.html + doc/dialogs/EventEditor.html + doc/dialogs/FictionViewerDialog.html + doc/dialogs/GlobalShipFlagsDialog.html + doc/dialogs/JumpNodeEditorDialog.html + doc/general/Viewport.html + doc/general/Toolbars.html + doc/general/LayerManagerDialog.html + doc/dialogs/MissionCutscenesDialog.html + doc/dialogs/MissionEventsDialog.html + doc/dialogs/MissionGoalsDialog.html + doc/dialogs/MissionSpecsDialog.html + doc/dialogs/CustomWingNamesDialog.html + doc/dialogs/CustomStringsDialog.html + doc/dialogs/SoundEnvironmentDialog.html + doc/dialogs/MissionStatsDialog.html + doc/dialogs/MusicPlayerDialog.html + doc/dialogs/ObjectOrientEditorDialog.html + doc/general/PreferencesDialog.html + doc/dialogs/PropEditorDialog.html + doc/dialogs/ReinforcementsEditorDialog.html + doc/dialogs/RelativeCoordinatesDialog.html + doc/dialogs/ShieldSystemDialog.html + doc/dialogs/ShipEditorDialog.html + doc/dialogs/ShipAltShipClass.html + doc/dialogs/ShipCustomWarpDialog.html + doc/dialogs/ShipFlagsDialog.html + doc/dialogs/ShipInitialOrdersDialog.html + doc/dialogs/ShipInitialStatusDialog.html + doc/dialogs/ShipPlayerOrdersDialog.html + doc/dialogs/ShipSpecialStatsDialog.html + doc/dialogs/ShipTextureReplacementDialog.html + doc/dialogs/ShipWeaponsDialog.html + doc/dialogs/TeamLoadoutDialog.html + doc/dialogs/VariablesAndContainersDialog.html + doc/dialogs/VoiceActingManager.html + doc/dialogs/VolumetricNebulaDialog.html + doc/dialogs/WaypointEditorDialog.html + doc/dialogs/WaypointPathGeneratorDialog.html + doc/dialogs/WingEditorDialog.html + + + diff --git a/qtfred/help-src/qtfred_help_resources.qrc.in b/qtfred/help-src/qtfred_help_resources.qrc.in new file mode 100644 index 00000000000..1bda2fff0fb --- /dev/null +++ b/qtfred/help-src/qtfred_help_resources.qrc.in @@ -0,0 +1,5 @@ + + + @QTFRED_HELP_QCH@ + + diff --git a/qtfred/help-src/tutorial-template.html b/qtfred/help-src/tutorial-template.html new file mode 100644 index 00000000000..b73b283828a --- /dev/null +++ b/qtfred/help-src/tutorial-template.html @@ -0,0 +1,137 @@ + + + + + + + Your Tutorial Title Here + + + + +

Your Tutorial Title Here

+ +

+ A short paragraph introducing what this tutorial covers and what the reader + will be able to do by the end of it. +

+ + +
+ This is a note box. Use it for tips, prerequisites, or warnings that + should stand out from the body text. +
+ +

1. First Step

+ +

+ Walk through the first thing the reader needs to do. Reference dialog names + in bold. Use Ctrl+Click for keyboard shortcuts + and inline code for values the user types. +

+ + +

+ Double-click the ship to open the + Ship Editor. + Set the team to Friendly and close the dialog. +

+ +

2. Second Step

+ +

+ Continue with the next task. For multi-line commands or SEXP expressions, + use a <pre> block: +

+ +
(has-time-elapsed 60)
+ + +

Reference Table (optional)

+ + + + + + + + + + + + + + + + + + + + + +
SettingRecommended valueNotes
TeamFriendlyRequired for player ships
AI classNonePlayer-controlled ships ignore AI class
+ +

Next Steps

+ +
    +
  • + Learn about + SEXPs + to drive mission logic with conditions and actions. +
  • +
  • + Set up arrival and departure cues in the + Ship Editor. +
  • +
  • + Add mission objectives in + Mission Goals. +
  • +
+ + + diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 6c2d6019681..43b06165d9b 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -104,6 +104,8 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/WaypointPathGeneratorDialogModel.h src/mission/dialogs/WingEditorDialogModel.cpp src/mission/dialogs/WingEditorDialogModel.h + src/mission/dialogs/HelpTopicsDialogModel.cpp + src/mission/dialogs/HelpTopicsDialogModel.h ) add_file_folder("Source/Mission/Dialogs/MissionSpecs" src/mission/dialogs/MissionSpecs/CustomDataDialogModel.cpp @@ -170,6 +172,8 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/FormWingDialog.h src/ui/dialogs/GlobalShipFlagsDialog.cpp src/ui/dialogs/GlobalShipFlagsDialog.h + src/ui/dialogs/HelpTopicsDialog.cpp + src/ui/dialogs/HelpTopicsDialog.h src/ui/dialogs/JumpNodeEditorDialog.cpp src/ui/dialogs/JumpNodeEditorDialog.h src/ui/dialogs/LayerManagerDialog.cpp @@ -371,6 +375,7 @@ add_file_folder("UI" ui/ShipAltShipClass.ui ui/ShipWeaponsDialog.ui ui/VariableDialog.ui + ui/HelpTopicsDialog.ui ui/WingEditorDialog.ui ui/SaveAsTemplateDialog.ui ui/SceneBrowserPanel.ui diff --git a/qtfred/src/main.cpp b/qtfred/src/main.cpp index af25bf86c56..e52d1068377 100644 --- a/qtfred/src/main.cpp +++ b/qtfred/src/main.cpp @@ -20,6 +20,7 @@ #include "globalincs/pstypes.h" #include "ui/FredView.h" +#include "ui/dialogs/HelpTopicsDialog.h" #include "ui/Theme.h" #include "FredApplication.h" @@ -216,6 +217,10 @@ int main(int argc, char* argv[]) { // Allow other parts of the code to execute code that needs to run after everything has been set up fredApp->initializeComplete(); + // Initialize the help engine and kick off search indexing in the background + // so the Search tab is ready before the user first opens Help Topics. + QTimer::singleShot(0, [] { fso::fred::dialogs::HelpTopicsDialog::prewarm(); }); + if (Cmdline_start_mission) { // Automatically load a mission if specified on the command line QTimer::singleShot(500, [&]() { diff --git a/qtfred/src/mission/dialogs/HelpTopicsDialogModel.cpp b/qtfred/src/mission/dialogs/HelpTopicsDialogModel.cpp new file mode 100644 index 00000000000..c1d03328ec8 --- /dev/null +++ b/qtfred/src/mission/dialogs/HelpTopicsDialogModel.cpp @@ -0,0 +1,247 @@ +#include "HelpTopicsDialogModel.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cfile/cfile.h" + +// Declared in cfile... must be at file scope so the tutorial discovery code can see it. +extern int cfile_inited; + +namespace fso::fred::dialogs { + +// --------------------------------------------------------------------------- +// Static member definitions +// --------------------------------------------------------------------------- +QHelpEngine* HelpTopicsDialogModel::_helpEngine = nullptr; +QList HelpTopicsDialogModel::_tutorials; +QHash HelpTopicsDialogModel::_tutorialContent; +QHash HelpTopicsDialogModel::_assetCache; +TutorialEntry HelpTopicsDialogModel::_sexpOperatorReference; +bool HelpTopicsDialogModel::_hasSexpOperatorReference = false; + +// --------------------------------------------------------------------------- +HelpTopicsDialogModel::HelpTopicsDialogModel(QObject* parent) : QObject(parent) {} + +// --------------------------------------------------------------------------- +void HelpTopicsDialogModel::prewarm() { + if (ensureEngineReady()) + _helpEngine->searchEngine()->scheduleIndexDocumentation(); + _tutorialContent.clear(); + _assetCache.clear(); + _hasSexpOperatorReference = false; + _tutorials = discoverTutorials(); +} + +QHelpEngine* HelpTopicsDialogModel::helpEngine() { + return _helpEngine; +} + +const QList& HelpTopicsDialogModel::tutorials() { + return _tutorials; +} + +const QHash& HelpTopicsDialogModel::tutorialContent() { + return _tutorialContent; +} + +const TutorialEntry* HelpTopicsDialogModel::sexpOperatorReference() { + return _hasSexpOperatorReference ? &_sexpOperatorReference : nullptr; +} + +// --------------------------------------------------------------------------- +bool HelpTopicsDialogModel::ensureEngineReady() { + if (_helpEngine != nullptr) + return !_helpEngine->registeredDocumentations().isEmpty(); + + const QString collectionFile = resolveCollectionFile(); + if (collectionFile.isEmpty()) { + mprintf(("QtFRED help: could not determine a writable location for the help collection\n")); + return false; + } + + // Parent to qApp so Qt manages the lifetime. Destroyed cleanly before the event loop exits. + _helpEngine = new QHelpEngine(collectionFile, qApp); + if (!_helpEngine->setupData()) { + mprintf(("QtFRED help: could not initialize engine: %s\n", + _helpEngine->error().toUtf8().constData())); + delete _helpEngine; + _helpEngine = nullptr; + return false; + } + + const QString contentFile = resolveContentFile(); + if (contentFile.isEmpty()) { + mprintf(("QtFRED help: could not find qtfred_help.qch\n")); + delete _helpEngine; + _helpEngine = nullptr; + return false; + } + + const QString ns = QHelpEngineCore::namespaceName(contentFile); + if (!ns.isEmpty()) { + // Always unregister and re-register. Qt copies file content from the .qch into the + // .qhc SQLite database at registration time. If the .qch is rebuilt without + // re-registration the .qhc tables become stale: findFile() succeeds but fileData() + // returns 0 bytes. Forcing re-registration each startup is cheap and guarantees the + // collection database is always in sync with the current .qch. + if (_helpEngine->registeredDocumentations().contains(ns)) + _helpEngine->unregisterDocumentation(ns); + + if (!_helpEngine->registerDocumentation(contentFile)) { + mprintf(("QtFRED help: failed to register %s: %s\n", + contentFile.toUtf8().constData(), + _helpEngine->error().toUtf8().constData())); + } else { + mprintf(("QtFRED help: registered namespace '%s' from %s\n", + ns.toUtf8().constData(), + contentFile.toUtf8().constData())); + } + } + + return !_helpEngine->registeredDocumentations().isEmpty(); +} + +// --------------------------------------------------------------------------- +QString HelpTopicsDialogModel::resolveCollectionFile() { + const QString appDataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + if (appDataDir.isEmpty()) + return {}; + QDir(appDataDir).mkpath(QStringLiteral("help")); + return QDir(appDataDir).filePath(QStringLiteral("help/qtfred.qhc")); +} + +QString HelpTopicsDialogModel::resolveContentFile() { + // Check the FSO VFS for a mod override in data/freddocs/. + if (cfile_inited) { + CFileLocation loc = cf_find_file_location("qtfred_help.qch", CF_TYPE_FREDDOCS); + if (loc.found && !loc.full_name.empty()) + return QString::fromStdString(loc.full_name); + } + + // Fall back to the built-in .qch deployed next to the executable. + return QDir(QCoreApplication::applicationDirPath()).filePath( + QStringLiteral("help/qtfred_help.qch")); +} + +// --------------------------------------------------------------------------- +QString HelpTopicsDialogModel::extractHtmlTitle(const QString& filePath) { + QFile f(filePath); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) + return {}; + // Titles are always in the , so a small read is enough. + const QString head = QString::fromUtf8(f.read(4096)); + const QRegularExpression re(QStringLiteral("]*>([^<]+)"), + QRegularExpression::CaseInsensitiveOption); + const auto m = re.match(head); + if (m.hasMatch()) + return m.captured(1).trimmed(); + return QFileInfo(filePath).completeBaseName(); +} + +QList HelpTopicsDialogModel::discoverTutorials() { + QList result; + if (cfile_inited) { + SCP_vector filenames; + cf_get_file_list(filenames, CF_TYPE_FREDDOCS, "*.html"); + for (const auto& filename : filenames) { + // Skip files in subdirectories. Only surface top-level tutorials. + // Mods can use subdirectories for assets or linked pages without flooding the list. + if (filename.find('/') != SCP_string::npos || filename.find('\\') != SCP_string::npos) + continue; + + // cf_get_file_list strips extensions; re-add it for the lookup and URL. + const std::string fullFilename = filename + ".html"; + CFileLocation loc = cf_find_file_location(fullFilename.c_str(), CF_TYPE_FREDDOCS); + if (!loc.found) + continue; + + const QString fullPath = QString::fromStdString(loc.full_name); + const QString urlPath = QStringLiteral("/") + QString::fromStdString(fullFilename); + + QFile f(fullPath); + if (!f.open(QIODevice::ReadOnly)) + continue; + + _tutorialContent[urlPath] = f.readAll(); + + if (QString::compare(QString::fromStdString(fullFilename), + QStringLiteral("sexps.html"), + Qt::CaseInsensitive) == 0) { + _sexpOperatorReference = {QStringLiteral("SEXP Operator Reference"), fullPath, urlPath}; + _hasSexpOperatorReference = true; + continue; + } + + result.append({extractHtmlTitle(fullPath), fullPath, urlPath}); + } + } + + // Optional fallback for developers: also accept a loose sexps.html shipped + // next to the QtFRED executable (or in its help/ subdirectory). + if (!_hasSexpOperatorReference) { + const QDir appDir(QCoreApplication::applicationDirPath()); + const QStringList fallbackPaths = { + appDir.filePath(QStringLiteral("sexps.html")), + appDir.filePath(QStringLiteral("help/sexps.html")) + }; + + for (const auto& candidate : fallbackPaths) { + QFile f(candidate); + if (!f.open(QIODevice::ReadOnly)) + continue; + + const QString urlPath = QStringLiteral("/sexps.html"); + _tutorialContent[urlPath] = f.readAll(); + _sexpOperatorReference = {QStringLiteral("SEXP Operator Reference"), candidate, urlPath}; + _hasSexpOperatorReference = true; + break; + } + } + + return result; +} + +QByteArray HelpTopicsDialogModel::loadTutorialAsset(const QString& urlPath) { + if (!cfile_inited) + return {}; + + // Reject empty, root-only, non-rooted, or path-traversal requests. + if (urlPath.isEmpty() || urlPath == QStringLiteral("/")) + return {}; + if (!urlPath.startsWith(QLatin1Char('/'))) + return {}; + if (urlPath.contains(QStringLiteral(".."))) + return {}; + + // Cache both hits and misses to avoid repeated VFS lookups for the same asset. + const auto cached = _assetCache.constFind(urlPath); + if (cached != _assetCache.constEnd()) + return *cached; + + // Strip the leading '/' before the VFS lookup (e.g. "/images/logo.png" -> "images/logo.png"). + // QUrl::path() already decodes percent-encoded characters, so no extra decoding needed. + const std::string relPath = urlPath.mid(1).toStdString(); + CFileLocation loc = cf_find_file_location(relPath.c_str(), CF_TYPE_FREDDOCS); + if (!loc.found || loc.full_name.empty()) { + _assetCache.insert(urlPath, {}); + return {}; + } + QFile f(QString::fromStdString(loc.full_name)); + if (!f.open(QIODevice::ReadOnly)) { + _assetCache.insert(urlPath, {}); + return {}; + } + QByteArray data = f.readAll(); + _assetCache.insert(urlPath, data); + return data; +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/HelpTopicsDialogModel.h b/qtfred/src/mission/dialogs/HelpTopicsDialogModel.h new file mode 100644 index 00000000000..9a627176890 --- /dev/null +++ b/qtfred/src/mission/dialogs/HelpTopicsDialogModel.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include +#include + +class QHelpEngine; + +namespace fso::fred::dialogs { + +struct TutorialEntry { + QString title; + QString fullPath; // absolute path on disk + QString urlPath; // e.g. "/getting-started.html" — key in tutorialContent() +}; + +/** + * @brief Service model for the HelpTopicsDialog. + * + * Manages the singleton QHelpEngine and the list of mod tutorial pages discovered + * in data/freddocs/. All state is static so it is shared across the application + * lifetime regardless of how many times the dialog is opened or closed. + * + * Call prewarm() once at startup (after the VFS is ready) to initialise the engine + * and kick off search indexing in the background before the user opens Help Topics. + */ +class HelpTopicsDialogModel : public QObject { + Q_OBJECT + +public: + explicit HelpTopicsDialogModel(QObject* parent = nullptr); + + // Call once at startup... initialises the help engine, schedules full-text + // search indexing, and discovers mod tutorial pages, all before the dialog opens. + static void prewarm(); + + static QHelpEngine* helpEngine(); + static const QList& tutorials(); + static const QHash& tutorialContent(); + static const TutorialEntry* sexpOperatorReference(); + + // Exposed so HelpTopicsDialog can call it directly and show an error on failure. + static bool ensureEngineReady(); + + // On-demand VFS lookup for assets (CSS, images, scripts) referenced by tutorial pages. + static QByteArray loadTutorialAsset(const QString& urlPath); + +private: + static QString resolveCollectionFile(); + static QString resolveContentFile(); + static QString extractHtmlTitle(const QString& filePath); + static QList discoverTutorials(); + + static QHelpEngine* _helpEngine; + static QList _tutorials; + static QHash _tutorialContent; + static QHash _assetCache; + static TutorialEntry _sexpOperatorReference; + static bool _hasSexpOperatorReference; +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 13d745df76f..c186c79737d 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -38,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -2774,6 +2776,17 @@ void FredView::on_actionMark_Wing_triggered(bool) { void FredView::on_actionError_Checker_triggered(bool) { fred->global_error_check(); } +void FredView::on_actionHelp_Topics_triggered(bool) { + // Keep a single instance alive for the session. The help engine's contentWidget(), + // indexWidget(), and search widgets are singletons owned by the engine. + static QPointer s_helpDialog; + if (!s_helpDialog) + s_helpDialog = new dialogs::HelpTopicsDialog(this); + s_helpDialog->show(); + s_helpDialog->raise(); + s_helpDialog->activateWindow(); +} + void FredView::on_actionAbout_triggered(bool) { auto dialog = new dialogs::AboutDialog(this, _viewport); dialog->setAttribute(Qt::WA_DeleteOnClose); diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index 695a2cc5913..ffc272c2c12 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -165,6 +165,7 @@ class FredView: public QMainWindow, public IDialogProvider { void on_actionError_Checker_triggered(bool); + void on_actionHelp_Topics_triggered(bool); void on_actionAbout_triggered(bool); void on_actionMission_Statistics_triggered(bool); void on_actionBackground_triggered(bool); diff --git a/qtfred/src/ui/dialogs/HelpTopicsDialog.cpp b/qtfred/src/ui/dialogs/HelpTopicsDialog.cpp new file mode 100644 index 00000000000..25ddb0277b1 --- /dev/null +++ b/qtfred/src/ui/dialogs/HelpTopicsDialog.cpp @@ -0,0 +1,416 @@ +#include "HelpTopicsDialog.h" +#include "ui_HelpTopicsDialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mission/dialogs/HelpTopicsDialogModel.h" +#include "ui/Theme.h" + +namespace fso::fred::dialogs { + +// --------------------------------------------------------------------------- +// HelpBrowser +// --------------------------------------------------------------------------- +HelpBrowser::HelpBrowser(QWidget* parent) : QTextBrowser(parent) { + setOpenLinks(false); +} + +void HelpBrowser::changeEvent(QEvent* e) { + QTextBrowser::changeEvent(e); + if (e->type() == QEvent::PaletteChange) + reload(); +} + +QVariant HelpBrowser::loadResource(int type, const QUrl& name) { + if (name.scheme() == QStringLiteral("fredtutorial")) { + // Serve pre-loaded HTML tutorial pages. + const auto& content = HelpTopicsDialogModel::tutorialContent(); + const auto it = content.constFind(name.path()); + if (it != content.constEnd()) { + QByteArray tutorialBytes = *it; + if (name.path().endsWith(QStringLiteral(".html")) && isDarkMode()) { + const int insertAt = tutorialBytes.indexOf(""); + if (insertAt != -1) + tutorialBytes.insert(insertAt, darkModeStyleBlock()); + } + return tutorialBytes; + } + // On-demand loading for assets (CSS, images, scripts) referenced by tutorial pages. + const QByteArray asset = HelpTopicsDialogModel::loadTutorialAsset(name.path()); + return asset.isEmpty() ? QVariant{} : asset; + } + + if (name.scheme() == QStringLiteral("qthelp")) { + QHelpEngine* engine = HelpTopicsDialogModel::helpEngine(); + if (engine == nullptr) + return {}; + const QUrl canonical = engine->findFile(name); + if (!canonical.isValid()) + return {}; + QByteArray helpBytes = engine->fileData(canonical); + if (canonical.path().endsWith(QStringLiteral(".html")) && isDarkMode()) { + const int insertAt = helpBytes.indexOf(""); + if (insertAt != -1) + helpBytes.insert(insertAt, darkModeStyleBlock()); + } + return helpBytes; + } + + return QTextBrowser::loadResource(type, name); +} + +bool HelpBrowser::isDarkMode() const { + return palette().color(QPalette::Window).lightness() < 128; +} + +QByteArray HelpBrowser::darkModeStyleBlock() { + return QByteArrayLiteral( + ""); +} + +// --------------------------------------------------------------------------- +// HelpTopicsDialog +// --------------------------------------------------------------------------- +HelpTopicsDialog::HelpTopicsDialog(QWidget* parent) + : QDialog(parent), ui(new Ui::HelpTopicsDialog) { + ui->setupUi(this); + + bindStandardIcon(ui->backButton, QStyle::SP_ArrowLeft); + bindStandardIcon(ui->forwardButton, QStyle::SP_ArrowRight); + bindStandardIcon(ui->findPreviousButton, QStyle::SP_ArrowUp); + bindStandardIcon(ui->findNextButton, QStyle::SP_ArrowDown); + + if (!HelpTopicsDialogModel::ensureEngineReady()) { + ui->splitter->setDisabled(true); + QMessageBox::warning(this, tr("Help error"), tr("Could not load QtFRED help content.")); + return; + } + + QHelpEngine* engine = HelpTopicsDialogModel::helpEngine(); + + buildContentsTab(); + ui->navigationTabs->addTab(engine->indexWidget(), tr("Index")); + + // Search tab: full-text search of built-in help, plus tutorial matches below. + { + QHelpSearchEngine* searchEngine = engine->searchEngine(); + auto* container = new QWidget(ui->navigationTabs); + auto* layout = new QVBoxLayout(container); + layout->setContentsMargins(4, 4, 4, 4); + layout->setSpacing(4); + layout->addWidget(searchEngine->queryWidget()); + layout->addWidget(searchEngine->resultWidget(), 1); + + _tutorialSearchLabel = new QLabel(tr("Also found in custom pages:"), container); + _tutorialSearchLabel->setVisible(false); + layout->addWidget(_tutorialSearchLabel); + + _tutorialSearchResults = new QListWidget(container); + _tutorialSearchResults->setVisible(false); + layout->addWidget(_tutorialSearchResults, 1); + + ui->navigationTabs->addTab(container, tr("Search")); + + connect(searchEngine->queryWidget(), &QHelpSearchQueryWidget::search, + this, [this, searchEngine]() { + const QString query = searchEngine->queryWidget()->searchInput(); + searchEngine->search(query); + searchTutorials(query); + }); + connect(searchEngine->resultWidget(), &QHelpSearchResultWidget::requestShowLink, + this, &HelpTopicsDialog::loadHelpPage); + connect(_tutorialSearchResults, &QListWidget::itemClicked, + this, [this](QListWidgetItem* item) { + const QString urlPath = item->data(Qt::UserRole).toString(); + if (!urlPath.isEmpty()) + loadHelpPage(QUrl(QStringLiteral("fredtutorial://") + urlPath)); + }); + + searchEngine->scheduleIndexDocumentation(); + } + + // Tutorials tab: only shown when mod tutorial pages exist in data/freddocs/. + if (!HelpTopicsDialogModel::tutorials().isEmpty()) + buildTutorialsTab(); + + ui->splitter->setStretchFactor(0, 1); + ui->splitter->setStretchFactor(1, 3); + ui->splitter->setSizes({380, 820}); + + connect(ui->backButton, &QToolButton::clicked, ui->helpBrowser, &QTextBrowser::backward); + connect(ui->forwardButton, &QToolButton::clicked, ui->helpBrowser, &QTextBrowser::forward); + connect(ui->helpBrowser, &QTextBrowser::backwardAvailable, ui->backButton, &QToolButton::setEnabled); + connect(ui->helpBrowser, &QTextBrowser::forwardAvailable, ui->forwardButton, &QToolButton::setEnabled); + + connect(engine->indexWidget(), &QHelpIndexWidget::linkActivated, + this, &HelpTopicsDialog::loadHelpPage); + connect(ui->helpBrowser, &QTextBrowser::anchorClicked, + this, &HelpTopicsDialog::loadHelpPage); + + auto* findShortcut = new QShortcut(QKeySequence::Find, this); + connect(findShortcut, &QShortcut::activated, this, [this] { + ui->findLineEdit->setFocus(); + ui->findLineEdit->selectAll(); + }); + auto* findNextShortcut = new QShortcut(QKeySequence::FindNext, this); + connect(findNextShortcut, &QShortcut::activated, this, [this] { findInPage(false); }); + auto* findPreviousShortcut = new QShortcut(QKeySequence::FindPrevious, this); + connect(findPreviousShortcut, &QShortcut::activated, this, [this] { findInPage(true); }); + + connect(ui->findLineEdit, &QLineEdit::textChanged, this, [this](const QString& text) { + const bool hasQuery = !text.trimmed().isEmpty(); + ui->findPreviousButton->setEnabled(hasQuery); + ui->findNextButton->setEnabled(hasQuery); + }); + connect(ui->findLineEdit, &QLineEdit::returnPressed, this, [this] { findInPage(false); }); + connect(ui->findPreviousButton, &QToolButton::clicked, this, [this] { findInPage(true); }); + connect(ui->findNextButton, &QToolButton::clicked, this, [this] { findInPage(false); }); + + // Load the help home page. + const auto registeredDocuments = engine->registeredDocumentations(); + if (!registeredDocuments.isEmpty()) { + const auto homePage = engine->findFile( + QUrl(QStringLiteral("qthelp://%1/doc/index.html").arg(registeredDocuments.front()))); + if (homePage.isValid()) + loadHelpPage(homePage); + } +} + +HelpTopicsDialog::~HelpTopicsDialog() = default; + +// --------------------------------------------------------------------------- +void HelpTopicsDialog::prewarm() { + HelpTopicsDialogModel::prewarm(); +} + +void HelpTopicsDialog::findInPage(bool backward) { + const QString query = ui->findLineEdit->text().trimmed(); + if (query.isEmpty()) + return; + + const QTextDocument::FindFlags flags = backward ? QTextDocument::FindBackward + : QTextDocument::FindFlags{}; + if (ui->helpBrowser->find(query, flags)) + return; + + // Wrap around when no further match is found from the current cursor. + QTextCursor cursor = ui->helpBrowser->textCursor(); + cursor.movePosition(backward ? QTextCursor::End : QTextCursor::Start); + ui->helpBrowser->setTextCursor(cursor); + if (!ui->helpBrowser->find(query, flags)) { + QMessageBox::information(this, + tr("Find in page"), + tr("No matches found for \"%1\".").arg(query)); + } +} + +void HelpTopicsDialog::buildContentsTab() { + QHelpEngine* engine = HelpTopicsDialogModel::helpEngine(); + if (engine == nullptr) + return; + + auto* container = new QWidget(ui->navigationTabs); + auto* layout = new QVBoxLayout(container); + layout->setContentsMargins(0, 0, 0, 0); + + _contentsTree = new QTreeView(container); + _contentsTree->setHeaderHidden(true); + _contentsTree->setEditTriggers(QAbstractItemView::NoEditTriggers); + _contentsModel = new QStandardItemModel(_contentsTree); + + auto* helpContentModel = qobject_cast(engine->contentWidget()->model()); + if (helpContentModel != nullptr) { + for (int row = 0; row < helpContentModel->rowCount(); ++row) { + const QModelIndex index = helpContentModel->index(row, 0); + addHelpContentItem(_contentsModel->invisibleRootItem(), + helpContentModel->contentItemAt(index)); + } + } + + const TutorialEntry* sexpReference = HelpTopicsDialogModel::sexpOperatorReference(); + if (sexpReference != nullptr) { + QStandardItem* fundamentals = findContentItemByTitle(_contentsModel->invisibleRootItem(), + QStringLiteral("Fundamentals")); + if (fundamentals != nullptr) { + auto* extraItem = new QStandardItem(sexpReference->title); + extraItem->setData(QUrl(QStringLiteral("fredtutorial://") + sexpReference->urlPath), + Qt::UserRole); + QStandardItem* parentItem = fundamentals->parent(); + if (parentItem == nullptr) + parentItem = _contentsModel->invisibleRootItem(); + parentItem->insertRow(fundamentals->row() + 1, extraItem); + } + } + + _contentsTree->setModel(_contentsModel); + layout->addWidget(_contentsTree); + + ui->navigationTabs->addTab(container, tr("Contents")); + + connect(_contentsTree, &QTreeView::clicked, this, [this](const QModelIndex& index) { + if (!index.isValid() || _contentsModel == nullptr) + return; + const auto* item = _contentsModel->itemFromIndex(index); + if (item == nullptr) + return; + const QUrl url = item->data(Qt::UserRole).toUrl(); + if (url.isValid()) + loadHelpPage(url); + }); +} + +void HelpTopicsDialog::addHelpContentItem(QStandardItem* parent, QHelpContentItem* contentItem) { + if (parent == nullptr || contentItem == nullptr) + return; + + auto* item = new QStandardItem(contentItem->title()); + item->setData(contentItem->url(), Qt::UserRole); + parent->appendRow(item); + + for (int i = 0; i < contentItem->childCount(); ++i) + addHelpContentItem(item, contentItem->child(i)); +} + +QStandardItem* HelpTopicsDialog::findContentItemByTitle(QStandardItem* root, const QString& title) const { + if (root == nullptr) + return nullptr; + + for (int i = 0; i < root->rowCount(); ++i) { + QStandardItem* child = root->child(i); + if (child == nullptr) + continue; + if (child->text() == title) + return child; + if (QStandardItem* nested = findContentItemByTitle(child, title)) + return nested; + } + return nullptr; +} + +void HelpTopicsDialog::buildTutorialsTab() { + _tutorialsWidget = new QListWidget(ui->navigationTabs); + for (const auto& t : HelpTopicsDialogModel::tutorials()) { + auto* item = new QListWidgetItem(t.title, _tutorialsWidget); + item->setData(Qt::UserRole, t.urlPath); + item->setToolTip(t.fullPath); + } + ui->navigationTabs->addTab(_tutorialsWidget, tr("Tutorials")); + + connect(_tutorialsWidget, &QListWidget::itemClicked, this, [this](QListWidgetItem* item) { + const QString urlPath = item->data(Qt::UserRole).toString(); + if (!urlPath.isEmpty()) + loadHelpPage(QUrl(QStringLiteral("fredtutorial://") + urlPath)); + }); +} + +void HelpTopicsDialog::searchTutorials(const QString& query) { + _tutorialSearchResults->clear(); + + const QString trimmed = query.trimmed(); + QList searchablePages = HelpTopicsDialogModel::tutorials(); + if (const TutorialEntry* sexpReference = HelpTopicsDialogModel::sexpOperatorReference()) + searchablePages.append(*sexpReference); + + if (trimmed.isEmpty() || searchablePages.isEmpty()) { + _tutorialSearchLabel->setVisible(false); + _tutorialSearchResults->setVisible(false); + return; + } + + static const QRegularExpression tagRe(QStringLiteral("<[^>]+>")); + const QStringList terms = trimmed.split(QLatin1Char(' '), QString::SkipEmptyParts); + + for (const auto& t : searchablePages) { + const auto& content = HelpTopicsDialogModel::tutorialContent(); + const auto it = content.constFind(t.urlPath); + if (it == content.constEnd()) + continue; + + // Strip tags for plain-text matching. + QString text = QString::fromUtf8(*it); + text.remove(tagRe); + text = text.simplified(); + + // All terms must be present. + bool allFound = true; + int firstPos = -1; + for (const auto& term : qAsConst(terms)) { + const int pos = text.indexOf(term, 0, Qt::CaseInsensitive); + if (pos < 0) { allFound = false; break; } + if (firstPos < 0 || pos < firstPos) firstPos = pos; + } + if (!allFound) + continue; + + // Build a short context snippet around the first match. + const int start = qMax(0, firstPos - 60); + const int len = qMin(160, text.length() - start); + QString snippet = text.mid(start, len).simplified(); + if (start > 0) snippet.prepend(QStringLiteral("\u2026 ")); + if (start + len < text.length()) snippet.append(QStringLiteral(" \u2026")); + + auto* item = new QListWidgetItem(t.title, _tutorialSearchResults); + item->setData(Qt::UserRole, t.urlPath); + item->setToolTip(snippet); + } + + const bool hasResults = _tutorialSearchResults->count() > 0; + _tutorialSearchLabel->setVisible(hasResults); + _tutorialSearchResults->setVisible(hasResults); +} + +void HelpTopicsDialog::loadHelpPage(const QUrl& url) { + if (!url.isValid()) + return; + // Open http/https links in the system browser rather than the help viewer. + const QString scheme = url.scheme(); + if (scheme == QStringLiteral("http") || scheme == QStringLiteral("https")) { + QDesktopServices::openUrl(url); + return; + } + ui->helpBrowser->setSource(url); +} + +void HelpTopicsDialog::navigateTo(const QString& keywordId) { + QHelpEngine* engine = HelpTopicsDialogModel::helpEngine(); + if (engine == nullptr) + return; + const auto links = engine->linksForIdentifier(keywordId); + if (!links.isEmpty()) + loadHelpPage(links.constBegin().value()); +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/HelpTopicsDialog.h b/qtfred/src/ui/dialogs/HelpTopicsDialog.h new file mode 100644 index 00000000000..2de9b945e7f --- /dev/null +++ b/qtfred/src/ui/dialogs/HelpTopicsDialog.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include + +class QLabel; +class QListWidget; +class QListWidgetItem; +class QHelpContentItem; +class QStandardItem; +class QStandardItemModel; +class QTreeView; + +namespace fso::fred::dialogs { + +namespace Ui { +class HelpTopicsDialog; +} + +/** + * @brief Custom text browser that serves qthelp:// and fredtutorial:// URLs. + * + * Declared here (rather than as a private inner class) so that the .ui file can + * promote a QTextBrowser to this type via Qt Designer's custom-widget mechanism. + */ +class HelpBrowser : public QTextBrowser { + Q_OBJECT + +public: + explicit HelpBrowser(QWidget* parent = nullptr); + +protected: + void changeEvent(QEvent* e) override; + QVariant loadResource(int type, const QUrl& name) override; + +private: + bool isDarkMode() const; + static QByteArray darkModeStyleBlock(); +}; + +// --------------------------------------------------------------------------- + +class HelpTopicsDialog : public QDialog { + Q_OBJECT + +public: + explicit HelpTopicsDialog(QWidget* parent = nullptr); + ~HelpTopicsDialog() override; + + // Call once at startup to initialise the help engine, schedule search indexing, + // and discover mod tutorial pages... all before the user opens Help Topics. + static void prewarm(); + +public slots: // NOLINT(readability-redundant-access-specifiers) + void navigateTo(const QString& keywordId); + +private: + void loadHelpPage(const QUrl& url); + void findInPage(bool backward = false); + void buildContentsTab(); + void addHelpContentItem(QStandardItem* parent, QHelpContentItem* contentItem); + QStandardItem* findContentItemByTitle(QStandardItem* root, const QString& title) const; + void buildTutorialsTab(); + void searchTutorials(const QString& query); + + std::unique_ptr ui; + + // Widgets created dynamically (not in .ui) for the Search and Tutorials tabs. + QLabel* _tutorialSearchLabel = nullptr; + QListWidget* _tutorialSearchResults = nullptr; + QListWidget* _tutorialsWidget = nullptr; + QTreeView* _contentsTree = nullptr; + QStandardItemModel* _contentsModel = nullptr; +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/HelpTopicsDialog.ui b/qtfred/ui/HelpTopicsDialog.ui new file mode 100644 index 00000000000..99039adada3 --- /dev/null +++ b/qtfred/ui/HelpTopicsDialog.ui @@ -0,0 +1,142 @@ + + + fso::fred::dialogs::HelpTopicsDialog + + + + 0 + 0 + 1200 + 800 + + + + QtFRED Help + + + + + + Qt::Horizontal + + + + + 0 + 0 + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Find in page + + + true + + + + + + + false + + + Find previous + + + + + + + false + + + Find next + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + Back + + + + + + + false + + + Forward + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + fso::fred::dialogs::HelpBrowser + QTextBrowser +
ui/dialogs/HelpTopicsDialog.h
+
+
+ + +
From 09a848a2249668aef0b0c8c6719527f34c43b000 Mon Sep 17 00:00:00 2001 From: LuytenKy Date: Sun, 3 May 2026 19:26:45 +0300 Subject: [PATCH 16/65] Refactor player-set-target SEXP (#7393) * Refactor of logic based on feedback * resolve requested changes --- code/parse/sexp.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 7eb6eed8653..2a8dc37e90a 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -13935,27 +13935,27 @@ void sexp_set_friendly_damage_caps(int n) { * Sets the player's target to the specified, either ship, and or the subsystem on said ship. */ void sexp_set_player_target(int node) -{ - int shipnum = ship_name_lookup(CTEXT(node), 1); +{ + const ship_registry_entry *ship_entry = eval_ship(node); + if (ship_entry == nullptr) + return; + + int shipnum = ship_entry->shipnum; if (shipnum < 0) return; + ship* shipp = &Ships[shipnum]; int objnum = shipp->objnum; int n = CDR(node); + ship_subsys * new_subsys = nullptr; if (n >= 0) { const char* subsys_name = CTEXT(n); if (stricmp(subsys_name, SEXP_NONE_STRING) != 0) { - ship_subsys* ss = ship_get_subsys(shipp, subsys_name); - // target ship regardless of whether subsystem is destroyed - shipp->last_targeted_subobject[Player_num] = ss; - } else { - shipp->last_targeted_subobject[Player_num] = nullptr; + new_subsys = ship_get_subsys(shipp, subsys_name); } - } else { - shipp->last_targeted_subobject[Player_num] = nullptr; } - // set_target_objnum will call hud_restore_subsystem_target which reads last_targeted_subobject - set_target_objnum(Player_ai, objnum); + set_target_objnum(Player_ai, objnum); + set_targeted_subsys(Player_ai, new_subsys, objnum); } // Luytenky From 0642dcceab43363666b2ba9aa6f97e157f8d8e6c Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Mon, 4 May 2026 19:15:27 -0400 Subject: [PATCH 17/65] fix a few bugs in model loading Each time `model_load` is called with a `ship_info` pointer, which causes it to load subsystems, the model number MUST be assigned to the ship_info. --- code/mission/missionparse.cpp | 3 +++ code/model/model.h | 2 ++ 2 files changed, 5 insertions(+) diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index 057a62afe4f..188eb51b7c8 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -5347,6 +5347,7 @@ void resolve_path_masks(bool path_user_is_ship, const char *path_user, anchor_t // Load the anchor ship model with subsystems and all; it'll need to be done for this mission anyway auto anchor_sip = anchor_ship_entry->sip(); modelnum = model_load(anchor_sip->pof_file, anchor_sip); + anchor_sip->model_num = modelnum; // resolve names to indexes *path_mask = 0; @@ -7075,6 +7076,7 @@ bool post_process_mission(mission *pm) if (valid) { ship_info* sip = &Ship_info[icon.ship_class]; stage.icons[j].modelnum = model_load(sip->pof_file, sip); + sip->model_num = stage.icons[j].modelnum; } } } @@ -8944,6 +8946,7 @@ void check_anchor_for_hangar_bay(SCP_string &message, SCP_set &anchors // Load the anchor ship model with subsystems and all; it'll need to be done for this mission anyway auto anchor_sip = anchor_ship_entry->sip(); int modelnum = model_load(anchor_sip->pof_file, anchor_sip); + anchor_sip->model_num = modelnum; // Check if this model has a hangar bay if (!model_has_hangar_bay(modelnum)) diff --git a/code/model/model.h b/code/model/model.h index 83ed520ca15..5e4558c30dd 100644 --- a/code/model/model.h +++ b/code/model/model.h @@ -1005,9 +1005,11 @@ void model_free_all(); void model_instance_free_all(); // Alias to model_load, checks if a pof tech model exists and loads it if specified, otherwise loads the default pof. --wookieejedi +// NOTE: Each time model_load is called with a ship_info pointer, which causes it to load subsystems, the model number MUST be assigned to the ship_info. int model_load(ship_info* sip, bool prefer_tech_model); // Loads a model from disk and returns the model number it loaded into. +// NOTE: Each time model_load is called with a ship_info pointer, which causes it to load subsystems, the model number MUST be assigned to the ship_info. int model_load(const char *filename, ship_info* sip = nullptr, ErrorType error_type = ErrorType::FATAL_ERROR, bool allow_redundant_load = false); int model_create_instance(int objnum, int model_num); From 2b6e20360df324dd03b761684eb5a3018780bdd2 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Mon, 4 May 2026 23:00:30 -0400 Subject: [PATCH 18/65] assign sip->model_num from inside model_load, plus a few tweaks --- code/hud/hudshield.cpp | 7 +++---- code/mission/missionparse.cpp | 17 ++++++++--------- code/model/model.h | 4 ++-- code/model/modelread.cpp | 4 ++++ freespace2/freespace.cpp | 8 ++++---- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/code/hud/hudshield.cpp b/code/hud/hudshield.cpp index 98075d9d01b..e75c073d007 100644 --- a/code/hud/hudshield.cpp +++ b/code/hud/hudshield.cpp @@ -697,7 +697,6 @@ void HudGaugeShield::showShields(const object *objp, ShieldGaugeType mode, bool model_render_params render_info; // If this comment is here then I have not tested this - int model_num = sip->model_num; int model_instance_number = -1; if (!config) { @@ -705,15 +704,15 @@ void HudGaugeShield::showShields(const object *objp, ShieldGaugeType mode, bool render_info.set_replacement_textures(model_get_instance(model_instance_number)->texture_replace); } - if (model_num < 0 && config) { - model_num = model_load(sip, false); + if (sip->model_num < 0 && config) { + sip->model_num = model_load(sip, false); } render_info.set_flags(MR_NO_LIGHTING | MR_AUTOCENTER | MR_NO_FOGGING); render_info.set_detail_level_lock(1); render_info.set_object_number(OBJ_INDEX(objp)); - model_render_immediate( &render_info, model_num, model_instance_number, &object_orient, &vmd_zero_vector ); + model_render_immediate( &render_info, sip->model_num, model_instance_number, &object_orient, &vmd_zero_vector ); } //We're done diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index 188eb51b7c8..3f9c444d6d1 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -5337,7 +5337,7 @@ void resolve_path_masks(bool path_user_is_ship, const char *path_user, anchor_t // uninitialized; compute the mask from scratch if (prp->cached_mask & (1 << MAX_SHIP_BAY_PATHS)) { - int j, bay_path, modelnum; + int j, bay_path; // get anchor ship Assertion(anchor.isValid() && !(anchor.value() & ANCHOR_SPECIAL_ARRIVAL), "%s %s anchor %d is invalid or is a special arrival. Get a coder!", path_user_is_ship ? "Ship" : "Wing", path_user, anchor.value()); @@ -5346,14 +5346,13 @@ void resolve_path_masks(bool path_user_is_ship, const char *path_user, anchor_t // Load the anchor ship model with subsystems and all; it'll need to be done for this mission anyway auto anchor_sip = anchor_ship_entry->sip(); - modelnum = model_load(anchor_sip->pof_file, anchor_sip); - anchor_sip->model_num = modelnum; + anchor_sip->model_num = model_load(anchor_sip->pof_file, anchor_sip); // resolve names to indexes *path_mask = 0; for (j = 0; j < prp->num_paths; j++) { - bay_path = model_find_bay_path(modelnum, prp->path_names[j]); + bay_path = model_find_bay_path(anchor_sip->model_num, prp->path_names[j]); if (bay_path < 0) continue; @@ -7075,8 +7074,9 @@ bool post_process_mission(mission *pm) if (valid) { ship_info* sip = &Ship_info[icon.ship_class]; - stage.icons[j].modelnum = model_load(sip->pof_file, sip); - sip->model_num = stage.icons[j].modelnum; + int modelnum = model_load(sip->pof_file, sip); + stage.icons[j].modelnum = modelnum; + sip->model_num = modelnum; } } } @@ -8945,11 +8945,10 @@ void check_anchor_for_hangar_bay(SCP_string &message, SCP_set &anchors { // Load the anchor ship model with subsystems and all; it'll need to be done for this mission anyway auto anchor_sip = anchor_ship_entry->sip(); - int modelnum = model_load(anchor_sip->pof_file, anchor_sip); - anchor_sip->model_num = modelnum; + anchor_sip->model_num = model_load(anchor_sip->pof_file, anchor_sip); // Check if this model has a hangar bay - if (!model_has_hangar_bay(modelnum)) + if (!model_has_hangar_bay(anchor_sip->model_num)) { sprintf(message, "%s (%s) is used as a%s anchor by %s %s (and possibly elsewhere too), but it does not have a hangar bay!", anchor_ship_entry->name, anchor_sip->name, is_arrival ? "n arrival" : " departure", other_is_ship ? "ship" : "wing", other_name); diff --git a/code/model/model.h b/code/model/model.h index 5e4558c30dd..e9bff59a7f6 100644 --- a/code/model/model.h +++ b/code/model/model.h @@ -1005,11 +1005,11 @@ void model_free_all(); void model_instance_free_all(); // Alias to model_load, checks if a pof tech model exists and loads it if specified, otherwise loads the default pof. --wookieejedi -// NOTE: Each time model_load is called with a ship_info pointer, which causes it to load subsystems, the model number MUST be assigned to the ship_info. +// NOTE: Each time model_load is called with a ship_info pointer, which causes it to load subsystems, the model number is also assigned to the ship_info. int model_load(ship_info* sip, bool prefer_tech_model); // Loads a model from disk and returns the model number it loaded into. -// NOTE: Each time model_load is called with a ship_info pointer, which causes it to load subsystems, the model number MUST be assigned to the ship_info. +// NOTE: Each time model_load is called with a ship_info pointer, which causes it to load subsystems, the model number is also assigned to the ship_info. int model_load(const char *filename, ship_info* sip = nullptr, ErrorType error_type = ErrorType::FATAL_ERROR, bool allow_redundant_load = false); int model_create_instance(int objnum, int model_num); diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index c522bb52943..a607ebc8b6d 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -3245,6 +3245,8 @@ int model_load(const char* filename, ship_info* sip, ErrorType error_type, bool if (!stricmp(filename , Polygon_models[i]->filename) && !allow_redundant_load) { // Model already loaded; just return. Polygon_models[i]->used_this_mission++; + if (sip != nullptr) + sip->model_num = Polygon_models[i]->id; return Polygon_models[i]->id; } } else if ( num == -1 ) { @@ -3519,6 +3521,8 @@ int model_load(const char* filename, ship_info* sip, ErrorType error_type, bool model_set_bay_path_nums(pm); unpause_parse(); + if (sip != nullptr) + sip->model_num = pm->id; return pm->id; } diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index ca28087d648..d3c937f6d1e 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -7437,11 +7437,11 @@ void Do_model_timings_test() int model_id[MAX_POLYGON_MODELS]; // Load them all - for (auto & sip : Ship_info) { - sip.model_num = model_load(sip.pof_file); + for (auto & si : Ship_info) { + si.model_num = model_load(&si, false); - model_used[sip.model_num % MAX_POLYGON_MODELS]++; - model_id[sip.model_num % MAX_POLYGON_MODELS] = sip.model_num; + model_used[si.model_num % MAX_POLYGON_MODELS]++; + model_id[si.model_num % MAX_POLYGON_MODELS] = si.model_num; } Texture_fp = fopen( NOX("ShipTextures.txt"), "wt" ); From 811b41252f63499e1ec670869360e02e3ef6734c Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 5 May 2026 06:39:03 -0500 Subject: [PATCH 19/65] Make showing 2D radar icons a toggle-able option (#7409) * Make showing 2D radar icons a toggle-able option * better options --- code/localization/localize.cpp | 2 +- code/radar/radar.cpp | 4 +++- code/radar/radardradis.cpp | 10 +++++++--- code/radar/radarorb.cpp | 4 +++- code/radar/radarsetup.cpp | 26 ++++++++++++++++++++++++++ code/radar/radarsetup.h | 8 ++++++++ code/ship/ship.cpp | 2 ++ 7 files changed, 50 insertions(+), 6 deletions(-) diff --git a/code/localization/localize.cpp b/code/localization/localize.cpp index 96c0c9b992c..207596154fa 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 1915 // This is the next available ID +// #define XSTR_SIZE 1918 // 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/radar/radar.cpp b/code/radar/radar.cpp index 9bf32bf9ee2..1623054f2bd 100644 --- a/code/radar/radar.cpp +++ b/code/radar/radar.cpp @@ -161,7 +161,9 @@ void HudGaugeRadarStd::drawBlips(int blip_type, int bright, int distort) } else { - if (b->radar_image_2d == -1 && b->radar_color_image_2d == -1) + bool show_icon = (Radar_2d_icon_mode == RadarIconMode::On || + (Radar_2d_icon_mode == RadarIconMode::TargetOnly && (b->flags & BLIP_CURRENT_TARGET))); + if (!show_icon || (b->radar_image_2d == -1 && b->radar_color_image_2d == -1)) drawContactCircle(x, y, b->rad); else drawContactImage(x, y, b->rad, b->radar_image_2d, b->radar_color_image_2d, b->radar_image_size); diff --git a/code/radar/radardradis.cpp b/code/radar/radardradis.cpp index 36450865d59..e6fe7668e43 100644 --- a/code/radar/radardradis.cpp +++ b/code/radar/radardradis.cpp @@ -319,10 +319,14 @@ void HudGaugeRadarDradis::drawBlips(int blip_type, int bright, int distort) } else { if (b->flags & BLIP_DRAW_DISTORTED) { blipDrawFlicker(b, &pos, alpha); - } else if (b->radar_image_2d >= 0 || b->radar_color_image_2d >= 0) { - drawContact(&pos, b->radar_image_2d, b->radar_color_image_2d, b->dist, alpha, scale_factor); } else { - drawContact(&pos, -1, unknown_contact_icon, b->dist, alpha, scale_factor); + bool show_icon = (Radar_2d_icon_mode == RadarIconMode::On || + (Radar_2d_icon_mode == RadarIconMode::TargetOnly && (b->flags & BLIP_CURRENT_TARGET))); + if (show_icon && (b->radar_image_2d >= 0 || b->radar_color_image_2d >= 0)) { + drawContact(&pos, b->radar_image_2d, b->radar_color_image_2d, b->dist, alpha, scale_factor); + } else { + drawContact(&pos, -1, unknown_contact_icon, b->dist, alpha, scale_factor); + } } } } diff --git a/code/radar/radarorb.cpp b/code/radar/radarorb.cpp index f8a51e46427..988e59a5e48 100644 --- a/code/radar/radarorb.cpp +++ b/code/radar/radarorb.cpp @@ -281,7 +281,9 @@ void HudGaugeRadarOrb::drawBlips(int blip_type, int bright, int distort) } else { - if (b->radar_image_2d >= 0 || b->radar_color_image_2d >= 0) + bool show_icon = (Radar_2d_icon_mode == RadarIconMode::On || + (Radar_2d_icon_mode == RadarIconMode::TargetOnly && (b->flags & BLIP_CURRENT_TARGET))); + if (show_icon && (b->radar_image_2d >= 0 || b->radar_color_image_2d >= 0)) { drawContactImage(&pos, b->rad, b->radar_image_2d, b->radar_color_image_2d, b->radar_projection_size); } diff --git a/code/radar/radarsetup.cpp b/code/radar/radarsetup.cpp index e8d2649ea0d..1f48e0a4a70 100644 --- a/code/radar/radarsetup.cpp +++ b/code/radar/radarsetup.cpp @@ -21,6 +21,7 @@ #include "localization/localize.h" #include "network/multi.h" #include "object/object.h" +#include "options/Option.h" #include "playerman/player.h" #include "radar/radar.h" #include "radar/radarorb.h" @@ -88,6 +89,31 @@ int See_all = 0; DCF_BOOL(see_all, See_all); +RadarIconMode Radar_2d_icon_mode = RadarIconMode::On; + +static auto RadarIconModeOption __UNUSED = options::OptionBuilder("HUD.Radar2dIconMode", + std::pair{"Radar 2D Icons", 1915}, + std::pair{"Controls how custom 2D ship icons are displayed on the radar", 1916}) + .category(std::make_pair("Game", 1824)) + .values({{RadarIconMode::Off, {"Off", 1286}}, + {RadarIconMode::On, {"On", 1285}}, + {RadarIconMode::TargetOnly, {"Target Only", 1917}}}) + .default_val(RadarIconMode::On) + .bind_to(&Radar_2d_icon_mode) + .importance(56) + .finish(); + +void radar_check_2d_icon_options() +{ + bool has_icons = std::any_of(Ship_info.begin(), Ship_info.end(), [](const ship_info& sip) { + return sip.radar_image_2d_idx >= 0 || sip.radar_color_image_2d_idx >= 0; + }); + + if (!has_icons) { + options::OptionsManager::instance()->removeOption(RadarIconModeOption); + } +} + void radar_stuff_blip_info(object *objp, int is_bright, color **blip_color, int *blip_type) { ship *shipp = NULL; diff --git a/code/radar/radarsetup.h b/code/radar/radarsetup.h index b0d01a7478d..84fc93e7cac 100644 --- a/code/radar/radarsetup.h +++ b/code/radar/radarsetup.h @@ -89,10 +89,18 @@ enum RadarVisibility DISTORTED //!< Visible but not fully }; +enum class RadarIconMode { + Off = 0, + On = 1, + TargetOnly = 2 +}; +extern RadarIconMode Radar_2d_icon_mode; + void radar_frame_init(); void radar_mission_init(); void radar_plot_object( object *objp ); RadarVisibility radar_is_visible( object *objp ); +void radar_check_2d_icon_options(); extern sound_handle Radar_static_looping; diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 32454b762b0..94e953b2245 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -6753,6 +6753,8 @@ void ship_init() // We shouldn't already have any subsystem pointers at this point. Assertion(Ship_subsystems.empty(), "Some pre-allocated subsystems didn't get cleared out: " SIZE_T_ARG " batches present during ship_init(); get a coder!\n", Ship_subsystems.size()); + + radar_check_2d_icon_options(); } } From 9129a5871b73493b93d973c7098d8c2215124d7b Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 5 May 2026 23:29:11 -0400 Subject: [PATCH 20/65] fix cmake warning Per Claude: > The `IF` opens with `(MSVC_USE_RUNTIME_DLL OR FSO_BUILD_QTFRED)` but the `ELSE` and `ENDIF` only reference `(MSVC_USE_RUNTIME_DLL)`. CMake requires the argument in `ELSE`/`ENDIF` to match the `IF` exactly (or be omitted). --- cmake/toolchain-msvc.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/toolchain-msvc.cmake b/cmake/toolchain-msvc.cmake index ae4fd67ad44..30195ac81b5 100644 --- a/cmake/toolchain-msvc.cmake +++ b/cmake/toolchain-msvc.cmake @@ -74,9 +74,9 @@ endif() IF(MSVC_USE_RUNTIME_DLL OR FSO_BUILD_QTFRED) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$>:Debug>DLL") add_compile_definitions(_AFXDLL) -ELSE(MSVC_USE_RUNTIME_DLL) +ELSE() set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$>:Debug>") -ENDIF(MSVC_USE_RUNTIME_DLL) +ENDIF() # Debug set(CMAKE_C_FLAGS_DEBUG "/W4 /Gy /Zi /Od /RTC1 /Gd /Oy-") From 1b0e63d92483fc342bcdd6afc953f8afc3a0981d Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Thu, 7 May 2026 02:54:07 +0200 Subject: [PATCH 21/65] Add animation support for particle spawning (#7416) * Add particle during animation * cleanup --- code/model/animation/modelanimation.cpp | 1 + .../animation/modelanimation_segments.cpp | 108 ++++++++++++++++++ .../model/animation/modelanimation_segments.h | 29 +++++ 3 files changed, 138 insertions(+) diff --git a/code/model/animation/modelanimation.cpp b/code/model/animation/modelanimation.cpp index 38e89d742ba..bbd4f008dac 100644 --- a/code/model/animation/modelanimation.cpp +++ b/code/model/animation/modelanimation.cpp @@ -1816,6 +1816,7 @@ namespace animation { {"$Axis Rotation:", ModelAnimationSegmentAxisRotation::parser}, {"$Translation:", ModelAnimationSegmentTranslation::parser}, {"$Sound During:", ModelAnimationSegmentSoundDuring::parser}, + {"$Particles During:", ModelAnimationSegmentParticlesDuring::parser}, {"$Inverse Kinematics:", ModelAnimationSegmentIK::parser} }; diff --git a/code/model/animation/modelanimation_segments.cpp b/code/model/animation/modelanimation_segments.cpp index c5085ee4dc2..f2a48ed634a 100644 --- a/code/model/animation/modelanimation_segments.cpp +++ b/code/model/animation/modelanimation_segments.cpp @@ -3,6 +3,10 @@ #include #include "render/3d.h" +#include "particle/ParticleManager.h" +#include "particle/hosts/EffectHostObject.h" +#include "particle/hosts/EffectHostSubmodel.h" +#include "particle/hosts/EffectHostVector.h" namespace animation { @@ -1287,6 +1291,8 @@ namespace animation { instance.interruptableSound = false; instance.currentlyPlaying = sound_handle::invalid(); } + + m_segment->forceStopAnimation(pmi_id); } void ModelAnimationSegmentSoundDuring::playLoopSnd(polymodel_instance* pmi) { @@ -1381,6 +1387,108 @@ namespace animation { } + ModelAnimationSegmentParticlesDuring::ModelAnimationSegmentParticlesDuring(std::shared_ptr segment, particle::ParticleEffectHandle effect, float atTime, std::shared_ptr submodel, std::optional position, std::optional orientation) : + m_segment(std::move(segment)), m_submodel(std::move(submodel)), m_position(std::move(position)), m_orientation(std::move(orientation)), m_effect(effect), m_atTime(atTime) { } + + ModelAnimationSegment* ModelAnimationSegmentParticlesDuring::copy() const { + auto newCopy = new ModelAnimationSegmentParticlesDuring(*this); + newCopy->m_segment = std::shared_ptr(newCopy->m_segment->copy()); + return newCopy; + } + + void ModelAnimationSegmentParticlesDuring::recalculate(ModelAnimationSubmodelBuffer& base, ModelAnimationSubmodelBuffer& currentAnimDelta, polymodel_instance* pmi) { + m_segment->recalculate(base, currentAnimDelta, pmi); + m_duration[pmi->id] = m_segment->getDuration(pmi->id); + } + + void ModelAnimationSegmentParticlesDuring::calculateAnimation(ModelAnimationSubmodelBuffer& base, float time, int pmi_id) const { + m_segment->calculateAnimation(base, time, pmi_id); + } + + void ModelAnimationSegmentParticlesDuring::executeAnimation(const ModelAnimationSubmodelBuffer& state, float timeboundLower, float timeboundUpper, ModelAnimationDirection direction, int pmi_id) { + float atTime = fminf(fmaxf(m_atTime, 0.0f), m_duration.at(pmi_id)); + if (timeboundLower <= atTime && atTime <= timeboundUpper) { + createParticleSource(model_get_instance(pmi_id)); + } + m_segment->executeAnimation(state, timeboundLower, timeboundUpper, direction, pmi_id); + } + + void ModelAnimationSegmentParticlesDuring::exchangeSubmodelPointers(ModelAnimationSet& replaceWith) { + m_segment->exchangeSubmodelPointers(replaceWith); + } + + void ModelAnimationSegmentParticlesDuring::forceStopAnimation(int pmi_id) { + m_segment->forceStopAnimation(pmi_id); + } + + void ModelAnimationSegmentParticlesDuring::createParticleSource(polymodel_instance* pmi) const { + if (!m_effect.isValid()) + return; + + auto source = particle::ParticleManager::get()->createSource(m_effect); + if (!source) + return; + + matrix orient = m_orientation.value_or(vmd_identity_matrix); + vec3d pos = m_position.value_or(vmd_zero_vector); + + std::unique_ptr host; + + if (m_submodel != nullptr && pmi->objnum >= 0) { + auto submodel = m_submodel->findSubmodel(pmi); + if (submodel.first != nullptr) { + host = std::make_unique(&Objects[pmi->objnum], static_cast(submodel.first - pmi->submodel), pos, orient); + } + } + + if (!host && pmi->objnum >= 0) { + host = std::make_unique(&Objects[pmi->objnum], pos, orient); + } + + if (!host) { + host = std::make_unique(pos, orient, vmd_zero_vector); + } + + source->setHost(std::move(host)); + source->finishCreation(); + } + + std::shared_ptr ModelAnimationSegmentParticlesDuring::parser(ModelAnimationParseHelper* data) { + auto submodel = ModelAnimationParseHelper::parseSubmodel(); + if (!submodel) { + if (data->parentSubmodel) + submodel = data->parentSubmodel; + } + + required_string("+Effect:"); + auto effect = particle::util::parseEffect(data->m_animationName); + + required_string("+At Time:"); + float atTime = 0.0f; + stuff_float(&atTime); + + std::optional position = std::nullopt; + if (optional_string("+Position:")) { + vec3d parse; + stuff_vec3d(&parse); + position = std::move(parse); + } + + std::optional orientation = std::nullopt; + if (optional_string("+Orientation:")) { + angles angle; + stuff_angles_deg_phb(&angle); + matrix mat; + vm_angles_2_matrix(&mat, &angle); + orientation = std::move(mat); + } + + auto segment = std::make_shared(data->parseSegment(), effect, atTime, submodel, position, orientation); + + return segment; + } + + ModelAnimationSegmentIK::ModelAnimationSegmentIK(const vec3d& targetPosition, const std::optional& targetRotation) : m_targetPosition(targetPosition), m_targetRotation(targetRotation) { } diff --git a/code/model/animation/modelanimation_segments.h b/code/model/animation/modelanimation_segments.h index efa8217b064..7996e905864 100644 --- a/code/model/animation/modelanimation_segments.h +++ b/code/model/animation/modelanimation_segments.h @@ -2,6 +2,7 @@ #include "math/ik_solver.h" #include "model/animation/modelanimation.h" +#include "particle/particle.h" namespace animation { @@ -290,6 +291,34 @@ namespace animation { }; + class ModelAnimationSegmentParticlesDuring : public ModelAnimationSegment { + std::shared_ptr m_segment; + + std::shared_ptr m_submodel; + std::optional m_position; + std::optional m_orientation; + + //configurables: + public: + particle::ParticleEffectHandle m_effect; + float m_atTime; + + private: + ModelAnimationSegment* copy() const override; + void recalculate(ModelAnimationSubmodelBuffer& base, ModelAnimationSubmodelBuffer& currentAnimDelta, polymodel_instance* pmi) override; + void calculateAnimation(ModelAnimationSubmodelBuffer& base, float time, int pmi_id) const override; + void executeAnimation(const ModelAnimationSubmodelBuffer& state, float timeboundLower, float timeboundUpper, ModelAnimationDirection direction, int pmi_id) override; + void exchangeSubmodelPointers(ModelAnimationSet& replaceWith) override; + void forceStopAnimation(int pmi_id) override; + + void createParticleSource(polymodel_instance* pmi) const; + + public: + static std::shared_ptr parser(ModelAnimationParseHelper* data); + ModelAnimationSegmentParticlesDuring(std::shared_ptr segment, particle::ParticleEffectHandle effect, float atTime, std::shared_ptr submodel = nullptr, std::optional position = std::nullopt, std::optional orientation = std::nullopt); + + }; + class ModelAnimationSegmentIK : public ModelAnimationSegment { struct instance_data { From 3f85d6172ac8cc06798db30e78801dc43c1c13d3 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Thu, 7 May 2026 01:40:38 -0400 Subject: [PATCH 22/65] use std::move() for local variables that are copied but not reused (#7308) * use std::move() for local variables that are copied but not reused Addresses ~140 Coverity "Variable copied when it could be moved" defects across 74 files. Every std::move() targets a local variable whose value is consumed by push_back, emplace, assignment, lambda capture, or a function call and is never read again afterward. Co-Authored-By: Claude Opus 4.6 (1M context) * clang: use std::make_shared * clang: use std::make_unique * address feedback --------- Co-authored-by: Claude Opus 4.6 (1M context) --- code/actions/BuiltinActionDefinition.h | 2 +- code/actions/expression/ActionExpression.cpp | 4 +- code/actions/expression/ProgramVariables.cpp | 2 +- code/actions/types/MoveToSubmodel.cpp | 2 +- code/actions/types/ParticleEffectAction.cpp | 4 +- code/actions/types/PlaySoundAction.cpp | 2 +- code/actions/types/SetDirectionAction.cpp | 2 +- code/actions/types/SetPositionAction.cpp | 2 +- code/actions/types/WaitAction.cpp | 2 +- code/ai/aiturret.cpp | 4 +- code/asteroid/asteroid.cpp | 6 +- code/cfile/cfilesystem.cpp | 10 ++-- code/cheats_table/cheats_table.cpp | 4 +- code/cheats_table/cheats_table.h | 2 +- code/cmdline/cmdline.cpp | 2 +- code/cutscene/movie.cpp | 2 +- code/debris/debris.cpp | 2 +- code/debugconsole/console.cpp | 2 +- code/gamehelp/gameplayhelp.cpp | 2 +- code/globalincs/vmallocator.h | 8 --- code/graphics/2d.cpp | 4 +- code/graphics/opengl/ShaderProgram.cpp | 2 +- code/graphics/paths/PathRenderer.cpp | 2 +- code/graphics/post_processing.cpp | 2 +- code/hud/hud.cpp | 4 +- code/hud/hudartillery.cpp | 2 +- code/hud/hudconfig.cpp | 2 +- code/hud/hudmessage.cpp | 4 +- code/hud/hudparse.cpp | 2 +- code/hud/hudsquadmsg.cpp | 2 +- code/math/curve.cpp | 2 +- code/menuui/credits.cpp | 2 +- code/menuui/mainhallmenu.cpp | 4 +- code/menuui/readyroom.cpp | 4 +- code/menuui/techmenu.cpp | 2 +- code/mission/missiongoals.cpp | 2 +- code/mission/missionmessage.cpp | 8 +-- code/mission/missionparse.cpp | 4 +- code/missioneditor/missionsave.cpp | 4 +- code/missionui/missionweaponchoice.cpp | 6 +- code/missionui/redalert.cpp | 10 ++-- code/mod_table/mod_table.cpp | 2 +- code/model/animation/modelanimation.cpp | 42 +++++++------- .../animation/modelanimation_moveables.cpp | 56 +++++++++---------- .../animation/modelanimation_segments.cpp | 32 +++++------ code/model/modelread.cpp | 4 +- code/model/modelrender.cpp | 4 +- code/model/modelreplace.cpp | 28 +++++----- code/network/multi_fstracker.cpp | 4 +- code/network/multi_obj.cpp | 4 +- code/network/multi_pxo.cpp | 2 +- code/network/multimsgs.cpp | 2 +- code/network/psnet2.cpp | 4 +- code/object/collideshipweapon.cpp | 4 +- code/object/object.cpp | 2 +- code/options/Option.h | 6 +- code/options/OptionsManager.cpp | 8 +-- code/parse/parselo.cpp | 2 +- code/parse/sexp.cpp | 2 +- code/parse/sexp/LuaAISEXP.cpp | 6 +- code/parse/sexp/LuaSEXP.cpp | 10 ++-- code/parse/sexp/sexp_lookup.cpp | 2 +- code/particle/ParticleParse.cpp | 4 +- code/pilotfile/csg.cpp | 4 +- code/pilotfile/csg_convert.cpp | 2 +- code/pilotfile/plr.cpp | 4 +- code/prop/prop.cpp | 6 +- code/scpui/RocketLuaSystemInterface.cpp | 2 +- code/scpui/rocket_ui.cpp | 2 +- code/scripting/ade.cpp | 2 +- code/scripting/api/libs/async.cpp | 2 +- code/scripting/api/libs/mission.cpp | 4 +- code/scripting/api/libs/options.cpp | 2 +- code/scripting/api/objs/executor.cpp | 2 +- code/scripting/api/objs/modelinstance.cpp | 2 +- code/scripting/api/objs/promise.cpp | 4 +- code/scripting/api/objs/rpc.cpp | 2 +- code/scripting/doc_luastub.cpp | 2 +- code/scripting/hook_conditions.cpp | 4 +- code/scripting/hook_conditions.h | 2 +- code/scripting/lua.cpp | 4 +- code/scripting/lua/LuaThread.cpp | 6 +- code/ship/ship.cpp | 14 ++--- code/ship/shipfx.cpp | 10 ++-- code/ship/shiphit.cpp | 4 +- code/sound/ds.cpp | 2 +- code/sound/openal.cpp | 4 +- code/species_defs/species_defs.cpp | 2 +- code/starfield/starfield.cpp | 10 ++-- code/starfield/supernova.cpp | 6 +- code/stats/medals.cpp | 4 +- code/stats/scoring.cpp | 2 +- code/tracing/FrameProfiler.cpp | 4 +- code/weapon/beam.cpp | 2 +- code/weapon/weapons.cpp | 12 ++-- freespace2/freespace.cpp | 4 +- 96 files changed, 250 insertions(+), 258 deletions(-) diff --git a/code/actions/BuiltinActionDefinition.h b/code/actions/BuiltinActionDefinition.h index 8ee59be3d14..9ef23320668 100644 --- a/code/actions/BuiltinActionDefinition.h +++ b/code/actions/BuiltinActionDefinition.h @@ -30,7 +30,7 @@ class BuiltinActionDefinition : public ActionDefinition { // This should have been caught earlier Assertion(paramIter != parameterExpressions.cend(), "Could not find built-in parameter!"); - return std::unique_ptr(new TAction(paramIter->second.template asTyped())); + return std::make_unique(paramIter->second.template asTyped()); } }; diff --git a/code/actions/expression/ActionExpression.cpp b/code/actions/expression/ActionExpression.cpp index c06d67a9a4e..d169feee884 100644 --- a/code/actions/expression/ActionExpression.cpp +++ b/code/actions/expression/ActionExpression.cpp @@ -24,7 +24,7 @@ ActionExpression ActionExpression::parseFromTable(ValueType expectedReturnType, stuff_string(expressionText, F_NAME); - ExpressionParser parser(expressionText); + ExpressionParser parser(std::move(expressionText)); auto expression = parser.parse(context); @@ -45,7 +45,7 @@ ActionExpression ActionExpression::parseFromTable(ValueType expectedReturnType, } // Everything is valid - return ActionExpression(expression); + return ActionExpression(std::move(expression)); } Value ActionExpression::execute(const ProgramVariables& variables) const diff --git a/code/actions/expression/ProgramVariables.cpp b/code/actions/expression/ProgramVariables.cpp index c1df81fc6a8..8ac7558110c 100644 --- a/code/actions/expression/ProgramVariables.cpp +++ b/code/actions/expression/ProgramVariables.cpp @@ -6,7 +6,7 @@ namespace expression { ProgramVariablesDefinition& ProgramVariablesDefinition::addScope(const SCP_string& name) { - auto scope = std::unique_ptr(new ProgramVariablesDefinition()); + auto scope = std::make_unique(); auto scopePtr = scope.get(); diff --git a/code/actions/types/MoveToSubmodel.cpp b/code/actions/types/MoveToSubmodel.cpp index 658df5b8075..c190715c682 100644 --- a/code/actions/types/MoveToSubmodel.cpp +++ b/code/actions/types/MoveToSubmodel.cpp @@ -70,7 +70,7 @@ ActionResult MoveToSubmodel::execute(ProgramLocals& locals) const std::unique_ptr MoveToSubmodel::clone() const { - return std::unique_ptr(new MoveToSubmodel(*this)); + return std::make_unique(*this); } } // namespace types diff --git a/code/actions/types/ParticleEffectAction.cpp b/code/actions/types/ParticleEffectAction.cpp index dca5192cda6..2d7ed8883ae 100644 --- a/code/actions/types/ParticleEffectAction.cpp +++ b/code/actions/types/ParticleEffectAction.cpp @@ -61,7 +61,7 @@ ActionResult ParticleEffectAction::execute(ProgramLocals& locals) const matrix orientation; vm_vector_2_matrix_norm(&orientation, &direction); // direction is normalized in SetDirectionAction::execute - source->setHost(make_unique(locals.host.objp(), local_pos, orientation, true)); + source->setHost(std::make_unique(locals.host.objp(), local_pos, orientation, true)); source->finishCreation(); return ActionResult::Finished; @@ -69,7 +69,7 @@ ActionResult ParticleEffectAction::execute(ProgramLocals& locals) const std::unique_ptr ParticleEffectAction::clone() const { - return std::unique_ptr(new ParticleEffectAction(*this)); + return std::make_unique(*this); } } // namespace types diff --git a/code/actions/types/PlaySoundAction.cpp b/code/actions/types/PlaySoundAction.cpp index e0acbbc9153..d22bf05cda1 100644 --- a/code/actions/types/PlaySoundAction.cpp +++ b/code/actions/types/PlaySoundAction.cpp @@ -57,7 +57,7 @@ ActionResult PlaySoundAction::execute(ProgramLocals& locals) const } std::unique_ptr PlaySoundAction::clone() const { - return std::unique_ptr(new PlaySoundAction(*this)); + return std::make_unique(*this); } } // namespace types } // namespace actions diff --git a/code/actions/types/SetDirectionAction.cpp b/code/actions/types/SetDirectionAction.cpp index 03f6616bc28..937ff22037b 100644 --- a/code/actions/types/SetDirectionAction.cpp +++ b/code/actions/types/SetDirectionAction.cpp @@ -32,7 +32,7 @@ ActionResult SetDirectionAction::execute(ProgramLocals& locals) const std::unique_ptr SetDirectionAction::clone() const { - return std::unique_ptr(new SetDirectionAction(*this)); + return std::make_unique(*this); } } // namespace types diff --git a/code/actions/types/SetPositionAction.cpp b/code/actions/types/SetPositionAction.cpp index e57275ace60..b072055fec8 100644 --- a/code/actions/types/SetPositionAction.cpp +++ b/code/actions/types/SetPositionAction.cpp @@ -27,7 +27,7 @@ ActionResult SetPositionAction::execute(ProgramLocals& locals) const std::unique_ptr SetPositionAction::clone() const { - return std::unique_ptr(new SetPositionAction(*this)); + return std::make_unique(*this); } } // namespace types diff --git a/code/actions/types/WaitAction.cpp b/code/actions/types/WaitAction.cpp index f9fca06940f..3b6ba9ee6bd 100644 --- a/code/actions/types/WaitAction.cpp +++ b/code/actions/types/WaitAction.cpp @@ -41,7 +41,7 @@ ActionResult WaitAction::execute(actions::ProgramLocals& locals) const } std::unique_ptr WaitAction::clone() const { - return std::unique_ptr(new WaitAction(*this)); + return std::make_unique(*this); } } // namespace types diff --git a/code/ai/aiturret.cpp b/code/ai/aiturret.cpp index f4d433ea99b..1cf25f6a502 100644 --- a/code/ai/aiturret.cpp +++ b/code/ai/aiturret.cpp @@ -2042,7 +2042,7 @@ bool turret_fire_weapon(int weapon_num, } //spawn particle effect auto particleSource = particle::ParticleManager::get()->createSource(wip->muzzle_effect); - particleSource->setHost(make_unique(&Objects[parent_ship->objnum], turret->system_info->turret_gun_sobj, turret->turret_next_fire_pos, false)); + particleSource->setHost(std::make_unique(&Objects[parent_ship->objnum], turret->system_info->turret_gun_sobj, turret->turret_next_fire_pos, false)); particleSource->setTriggerRadius(objp->radius * radius_mult); particleSource->setTriggerVelocity(vm_vec_mag_quick(&objp->phys_info.vel)); particleSource->finishCreation(); @@ -2163,7 +2163,7 @@ void turret_swarm_fire_from_turret(turret_swarm_info *tsi) if (Weapon_info[tsi->weapon_class].muzzle_effect.isValid()) { //spawn particle effect auto particleSource = particle::ParticleManager::get()->createSource(Weapon_info[tsi->weapon_class].muzzle_effect); - particleSource->setHost(make_unique(&Objects[tsi->parent_objnum], tsi->turret->system_info->turret_gun_sobj, tsi->turret->turret_next_fire_pos - 1, false)); + particleSource->setHost(std::make_unique(&Objects[tsi->parent_objnum], tsi->turret->system_info->turret_gun_sobj, tsi->turret->turret_next_fire_pos - 1, false)); particleSource->setTriggerRadius(Objects[weapon_objnum].radius); particleSource->finishCreation(); } diff --git a/code/asteroid/asteroid.cpp b/code/asteroid/asteroid.cpp index 7dedcde34ba..12c363ed6c0 100644 --- a/code/asteroid/asteroid.cpp +++ b/code/asteroid/asteroid.cpp @@ -2232,7 +2232,7 @@ static void asteroid_parse_section() } return; } - asteroid_list.push_back(asteroid_t); + asteroid_list.push_back(std::move(asteroid_t)); asteroid_p = &asteroid_list[asteroid_list.size() - 1]; } @@ -2330,7 +2330,7 @@ static void asteroid_parse_section() thisType.type_name.c_str(), asteroid_p->name); } else { - asteroid_p->subtypes.push_back(thisType); + asteroid_p->subtypes.push_back(std::move(thisType)); } } } @@ -2619,7 +2619,7 @@ static void verify_asteroid_splits() } // Replace splits with only valid splits - Asteroid_info[i].split_info = splits_t; + Asteroid_info[i].split_info = std::move(splits_t); } } diff --git a/code/cfile/cfilesystem.cpp b/code/cfile/cfilesystem.cpp index c23b5bdf46b..6fc5513cda1 100644 --- a/code/cfile/cfilesystem.cpp +++ b/code/cfile/cfilesystem.cpp @@ -532,7 +532,7 @@ void cf_build_pack_list( cf_root *root ) s_root.roottype = CF_ROOTTYPE_PACK; s_root.cf_type = i; - temp_roots_sort.push_back(s_root); + temp_roots_sort.push_back(std::move(s_root)); } } @@ -1083,7 +1083,7 @@ void cf_search_root_pack(int root_index) file.pathtype = path_type; file.offset = find.offset; - files.push_back(file); + files.push_back(std::move(file)); //mprintf(( "Found pack file '%s'\n", find.filename )); } @@ -1356,7 +1356,7 @@ CFileLocation cf_find_file_location(const char* filespec, int pathtype, uint32_t fclose(fp); res.offset = 0; - res.full_name = longname; + res.full_name = std::move(longname); res.name_ext = filespec; return res; @@ -1549,8 +1549,8 @@ CFileLocationExt cf_find_file_location_ext(const char *filename, const int ext_n fclose(fp); res.offset = 0; - res.full_name = longname; - res.name_ext = filespec_ext; + res.full_name = std::move(longname); + res.name_ext = std::move(filespec_ext); return res; } diff --git a/code/cheats_table/cheats_table.cpp b/code/cheats_table/cheats_table.cpp index 2ef43f7b254..bc74f454851 100644 --- a/code/cheats_table/cheats_table.cpp +++ b/code/cheats_table/cheats_table.cpp @@ -135,7 +135,7 @@ void parse_cheat_table(const char* filename) { } if (shipSpawn) { - std::unique_ptr shipCheat(new SpawnShipCheat(code, msg, requireCheats, shipClass, shipName)); + std::unique_ptr shipCheat(new SpawnShipCheat(code, std::move(msg), requireCheats, std::move(shipClass), std::move(shipName))); if(customCheats.count(code) == 1) { Warning(LOCATION, "A cheat for code '%s' already exists. It will be replaced.", code.c_str()); @@ -144,7 +144,7 @@ void parse_cheat_table(const char* filename) { customCheats.emplace(code, std::move(shipCheat)); } } else { - std::unique_ptr cheat(new CustomCheat(code, msg, requireCheats)); + std::unique_ptr cheat(new CustomCheat(code, std::move(msg), requireCheats)); if(customCheats.count(code) == 1) { Warning(LOCATION, "A cheat for code '%s' already exists. It will be replaced.", code.c_str()); diff --git a/code/cheats_table/cheats_table.h b/code/cheats_table/cheats_table.h index 71f65439f5b..ddd66b54072 100644 --- a/code/cheats_table/cheats_table.h +++ b/code/cheats_table/cheats_table.h @@ -35,7 +35,7 @@ class SpawnShipCheat : public CustomCheat { SCP_string shipName; public: - SpawnShipCheat(SCP_string cheat_code, SCP_string cheat_msg, bool require_cheats_enabled, SCP_string class_name, SCP_string ship_name) : CustomCheat(cheat_code, cheat_msg, require_cheats_enabled), + SpawnShipCheat(SCP_string cheat_code, SCP_string cheat_msg, bool require_cheats_enabled, SCP_string class_name, SCP_string ship_name) : CustomCheat(std::move(cheat_code), std::move(cheat_msg), require_cheats_enabled), shipClassName(std::move(class_name)), shipName(std::move(ship_name)) { } diff --git a/code/cmdline/cmdline.cpp b/code/cmdline/cmdline.cpp index 7dae2532cab..36a90ef9df5 100644 --- a/code/cmdline/cmdline.cpp +++ b/code/cmdline/cmdline.cpp @@ -1322,7 +1322,7 @@ static void handle_unix_modlist(char **modlist, size_t *len) for (char *cur_mod = strtok(*modlist, ","); cur_mod != NULL; cur_mod = strtok(NULL, ",")) { SCP_string path = get_real_mod_path(cur_mod); - mod_paths.push_back(path); + mod_paths.push_back(std::move(path)); } // create new char[] to replace modlist diff --git a/code/cutscene/movie.cpp b/code/cutscene/movie.cpp index f8d7633db8c..49768f46970 100644 --- a/code/cutscene/movie.cpp +++ b/code/cutscene/movie.cpp @@ -279,7 +279,7 @@ bool play(const char* filename, bool via_tech_room = false) { auto paramList = scripting::hook_param_list(scripting::hook_param("Filename", 's', filename), scripting::hook_param("ViaTechRoom", 'b', via_tech_room)); bool skip = scripting::hooks::OnMovieAboutToPlay->isOverride(paramList); - scripting::hooks::OnMovieAboutToPlay->run(paramList); + scripting::hooks::OnMovieAboutToPlay->run(std::move(paramList)); if (skip) return false; } diff --git a/code/debris/debris.cpp b/code/debris/debris.cpp index ea4f0c15517..44f50324006 100644 --- a/code/debris/debris.cpp +++ b/code/debris/debris.cpp @@ -149,7 +149,7 @@ void debris_init() particle::ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(1.f), //Velocity Inherit false, //Velocity Inherit absolute? - make_unique(0.3f, 1.f, true), //Velocity volume + std::make_unique(0.3f, 1.f, true), //Velocity volume ::util::UniformFloatRange(0.f, 10.f), //Velocity volume multiplier particle::ParticleEffect::VelocityScaling::NONE, //Velocity directional scaling std::nullopt, //Orientation-based velocity diff --git a/code/debugconsole/console.cpp b/code/debugconsole/console.cpp index 028302d1cc2..92eb5865b1a 100644 --- a/code/debugconsole/console.cpp +++ b/code/debugconsole/console.cpp @@ -467,7 +467,7 @@ void dc_putc(char c) */ temp_str = line_str->substr(lastwhite); line_str->resize(lastwhite); - dc_buffer.push_back(temp_str); + dc_buffer.push_back(std::move(temp_str)); line_str = &dc_buffer.back(); if ((dc_buffer.size() > DROWS) && (dc_scroll_y < SCROLL_Y_MAX)) { diff --git a/code/gamehelp/gameplayhelp.cpp b/code/gamehelp/gameplayhelp.cpp index 9d23bd27397..bc760023b4c 100644 --- a/code/gamehelp/gameplayhelp.cpp +++ b/code/gamehelp/gameplayhelp.cpp @@ -437,7 +437,7 @@ SCP_vector gameplay_help_init_text() } - complete_help_text.push_back(thisHelp); + complete_help_text.push_back(std::move(thisHelp)); } return complete_help_text; diff --git a/code/globalincs/vmallocator.h b/code/globalincs/vmallocator.h index 148fe19cc9d..fa39a0416ca 100644 --- a/code/globalincs/vmallocator.h +++ b/code/globalincs/vmallocator.h @@ -195,19 +195,11 @@ class SCP_unordered_set : public std::unordered_set -typename std::enable_if::value, std::unique_ptr>::type make_unique(Args&&... args) { - return std::unique_ptr(new T(std::forward(args)...)); -} template typename std::enable_if::value, std::unique_ptr>::type make_unique(std::size_t n) { return std::unique_ptr(new typename std::remove_extent::type[n]()); } -template -typename std::enable_if::value, std::shared_ptr>::type make_shared(Args&&... args) { - return std::shared_ptr(new T(std::forward(args)...)); -} template typename std::enable_if::value, std::shared_ptr>::type make_shared(std::size_t n) { return std::shared_ptr(new typename std::remove_extent::type[n]()); diff --git a/code/graphics/2d.cpp b/code/graphics/2d.cpp index 3ab1e950257..94a316b7a9d 100644 --- a/code/graphics/2d.cpp +++ b/code/graphics/2d.cpp @@ -3081,9 +3081,9 @@ SCP_vector gr_enumerate_displays() display.video_modes.push_back(videoMode); } - data.push_back(display); + data.push_back(std::move(display)); } - + SDL_QuitSubSystem(SDL_INIT_VIDEO); return data; diff --git a/code/graphics/opengl/ShaderProgram.cpp b/code/graphics/opengl/ShaderProgram.cpp index 02b39442b5d..d2930ff119a 100644 --- a/code/graphics/opengl/ShaderProgram.cpp +++ b/code/graphics/opengl/ShaderProgram.cpp @@ -246,7 +246,7 @@ void opengl::ShaderUniforms::setTextureUniform(const SCP_string& name, const int new_bind.name = name; new_bind.value = texture_unit; - _uniforms.push_back(new_bind); + _uniforms.push_back(std::move(new_bind)); _uniform_lookup[name] = _uniforms.size() - 1; } diff --git a/code/graphics/paths/PathRenderer.cpp b/code/graphics/paths/PathRenderer.cpp index 26695ee3ccc..9524aff3602 100644 --- a/code/graphics/paths/PathRenderer.cpp +++ b/code/graphics/paths/PathRenderer.cpp @@ -15,7 +15,7 @@ namespace paths { std::unique_ptr PathRenderer::s_instance; bool PathRenderer::init() { - s_instance = std::unique_ptr(new PathRenderer()); + s_instance = std::make_unique(); return true; } diff --git a/code/graphics/post_processing.cpp b/code/graphics/post_processing.cpp index 8802b625e01..0a28e038baa 100644 --- a/code/graphics/post_processing.cpp +++ b/code/graphics/post_processing.cpp @@ -162,7 +162,7 @@ bool PostProcessingManager::parse_table() // Post_effects index is used for flag checks, so we can't have more than 32 if (m_postEffects.size() < 32) { - m_postEffects.push_back(eff); + m_postEffects.push_back(std::move(eff)); } else if (!warned) { mprintf(("WARNING: post_processing.tbl can only have a max of 32 effects! Ignoring extra...\n")); warned = true; diff --git a/code/hud/hud.cpp b/code/hud/hud.cpp index 2761a3e1545..0c9d31afd0f 100644 --- a/code/hud/hud.cpp +++ b/code/hud/hud.cpp @@ -2538,7 +2538,7 @@ void HudGaugeDamage::render(float /*frametime*/, bool config) by += fl2i(line_h * scale); sy += fl2i(line_h * scale); - info_lines.push_back(info); + info_lines.push_back(std::move(info)); // Remove it from hud_subsys_list if ( best_index < (num-i-1) ) { @@ -2584,7 +2584,7 @@ void HudGaugeDamage::render(float /*frametime*/, bool config) info.value_y = y + fl2i(hull_integ_offsets[1] * scale); // Insert at the top since hull is always first - info_lines.insert(info_lines.begin(), info); + info_lines.insert(info_lines.begin(), std::move(info)); } if (info_lines.empty()) { diff --git a/code/hud/hudartillery.cpp b/code/hud/hudartillery.cpp index b535287f7ed..14370ae4548 100644 --- a/code/hud/hudartillery.cpp +++ b/code/hud/hudartillery.cpp @@ -389,7 +389,7 @@ void ssm_create(object *target, const vec3d *start, size_t ssm_index, const ssm_ snd_play(gamesnd_get_game_sound(Ssm_info[ssm_index].sound_index)); } - Ssm_strikes.push_back(ssm); + Ssm_strikes.push_back(std::move(ssm)); } // delete a finished ssm effect diff --git a/code/hud/hudconfig.cpp b/code/hud/hudconfig.cpp index a770758436b..fbe95b6b8d9 100644 --- a/code/hud/hudconfig.cpp +++ b/code/hud/hudconfig.cpp @@ -445,7 +445,7 @@ void hud_config_get_unique_huds() } } - HC_available_huds.push_back(newPair); + HC_available_huds.push_back(std::move(newPair)); seenHuds.insert(hudName); } } diff --git a/code/hud/hudmessage.cpp b/code/hud/hudmessage.cpp index 4cbaac87c6c..021897e02aa 100644 --- a/code/hud/hudmessage.cpp +++ b/code/hud/hudmessage.cpp @@ -363,7 +363,7 @@ void HudGaugeMessages::scrollMessages() new_active_msg.target_y = new_active_msg.y; } - active_messages.push_back(new_active_msg); + active_messages.push_back(std::move(new_active_msg)); } Scroll_in_progress = false; @@ -864,7 +864,7 @@ void hud_initialize_scrollback_lines() } } else { node_msg.y = height / 3; - Msg_scrollback_lines.push_back(node_msg); + Msg_scrollback_lines.push_back(std::move(node_msg)); } } } diff --git a/code/hud/hudparse.cpp b/code/hud/hudparse.cpp index 643a8d93834..a72f353c496 100644 --- a/code/hud/hudparse.cpp +++ b/code/hud/hudparse.cpp @@ -317,7 +317,7 @@ void parse_hud_gauges_tbl(const char *filename) preset.g = rgb[1]; preset.b = rgb[2]; - HC_colors[i] = preset; + HC_colors[i] = std::move(preset); if (optional_string("+Default")) { HC_default_color = i; } diff --git a/code/hud/hudsquadmsg.cpp b/code/hud/hudsquadmsg.cpp index 1aa33d438d1..e0c67d80cc2 100644 --- a/code/hud/hudsquadmsg.cpp +++ b/code/hud/hudsquadmsg.cpp @@ -1014,7 +1014,7 @@ bool hud_squadmsg_run_order_issued_hook(int command, ship* sendingShip, ship* re } scripting::hooks::OnHudCommOrderIssued->run( scripting::hooks::CommOrderConditions{sendingShip, targetObject, &recipient}, - paramList); + std::move(paramList)); } return isOverride; diff --git a/code/math/curve.cpp b/code/math/curve.cpp index 032499fe671..304259f7e2c 100644 --- a/code/math/curve.cpp +++ b/code/math/curve.cpp @@ -89,7 +89,7 @@ void parse_curve_table(const char* filename) { if (index < 0) { Curve curv = Curve(std::move(name)); curv.ParseData(); - Curves.push_back(curv); + Curves.push_back(std::move(curv)); } else { Curves[index].ParseData(); } diff --git a/code/menuui/credits.cpp b/code/menuui/credits.cpp index 13cd3fcb2ec..160069b98ce 100644 --- a/code/menuui/credits.cpp +++ b/code/menuui/credits.cpp @@ -450,7 +450,7 @@ void credits_parse_table(const char* filename) } } - Credit_text_parts.push_back(credits_text); + Credit_text_parts.push_back(std::move(credits_text)); Credits_parsed = true; } diff --git a/code/menuui/mainhallmenu.cpp b/code/menuui/mainhallmenu.cpp index 01707b5090b..ed50cd57aec 100644 --- a/code/menuui/mainhallmenu.cpp +++ b/code/menuui/mainhallmenu.cpp @@ -453,7 +453,7 @@ DCF(mainhall, "Temporarily sets the player to be on any main hall. Can be used dc_printf("Main hall not currently initialized. Setting player select override main hall to %s%s%s.\n", quote, name.empty() ? "0" : name.c_str(), quote); extern SCP_string Player_select_force_main_hall; - Player_select_force_main_hall = name; + Player_select_force_main_hall = std::move(name); return; } @@ -512,7 +512,7 @@ void main_hall_init(const SCP_string &main_hall_name) Warning(LOCATION, "Tried to load a main hall called '%s', but it does not exist; loading first available main hall.", requested_hall.c_str()); main_hall_get_name(main_hall_to_load, 0); } else { - main_hall_to_load = requested_hall; + main_hall_to_load = std::move(requested_hall); } // if we're switching to a different mainhall, stop the ambient (it will be started again promptly) diff --git a/code/menuui/readyroom.cpp b/code/menuui/readyroom.cpp index 899b427f828..bfecaee0f19 100644 --- a/code/menuui/readyroom.cpp +++ b/code/menuui/readyroom.cpp @@ -506,7 +506,7 @@ int build_standalone_mission_list_do_frame(bool API_Access) api_mission.author = The_mission.author; api_mission.visible = 1; - Sim_Missions.push_back(api_mission); + Sim_Missions.push_back(std::move(api_mission)); } // determine some extra information @@ -578,7 +578,7 @@ int build_campaign_mission_list_do_frame(bool API_Access) api_mission.author = The_mission.author; api_mission.visible = Campaign.missions[Num_campaign_missions_with_info].completed; - Sim_CMissions.push_back(api_mission); + Sim_CMissions.push_back(std::move(api_mission)); } // determine some extra information diff --git a/code/menuui/techmenu.cpp b/code/menuui/techmenu.cpp index bfd140745c2..8642d7079ef 100644 --- a/code/menuui/techmenu.cpp +++ b/code/menuui/techmenu.cpp @@ -1123,7 +1123,7 @@ void parse_intel_table(const char* filename) if (intel_p != nullptr) { error_display(0, "Duplicate entry %s in %s!", intel_t.name, filename); } - Intel_info.push_back(intel_t); + Intel_info.push_back(std::move(intel_t)); intel_p = &Intel_info[Intel_info.size() - 1]; } else { if (intel_p == nullptr) { diff --git a/code/mission/missiongoals.cpp b/code/mission/missiongoals.cpp index 6a1a046cdab..23bc16ab886 100644 --- a/code/mission/missiongoals.cpp +++ b/code/mission/missiongoals.cpp @@ -752,7 +752,7 @@ void mission_goal_status_change( int goal_num, int new_status) isOverride = true; // Override here only prevents displaying the goals and playing the music. Everything // else still runs. } - scripting::hooks::OnMissionGoalStatusChanged->run(paramList); + scripting::hooks::OnMissionGoalStatusChanged->run(std::move(paramList)); } Mission_goals[goal_num].satisfied = new_status; diff --git a/code/mission/missionmessage.cpp b/code/mission/missionmessage.cpp index 9fc2e1569ec..70892eb0de4 100644 --- a/code/mission/missionmessage.cpp +++ b/code/mission/missionmessage.cpp @@ -646,7 +646,7 @@ void message_parse(MessageFormat format) { } Num_messages++; - Messages.push_back(msg); + Messages.push_back(std::move(msg)); } void message_frequency_parse() @@ -705,7 +705,7 @@ void message_moods_parse() stuff_string(buf, F_NAME); if (!message_moods_check_existing(buf)) { - Builtin_moods.push_back(buf); + Builtin_moods.push_back(std::move(buf)); } else { mprintf(("Message mood %s already exists. Skipping!", buf.c_str())); } @@ -2248,7 +2248,7 @@ bool filters_match(MessageFilter& filter, ship* it) { return filter_matches(it->ship_name, filter.ship_name) && filter_matches(hud_get_ship_callsign(it), filter.callsign) && filter_matches(hud_get_ship_class(it), filter.class_name) - && filter_matches(wing_name, filter.wing_name) + && filter_matches(std::move(wing_name), filter.wing_name) && filter_matches(Ship_info[it->ship_info_index].species, filter.species_bitfield) && (Ship_info[it->ship_info_index].class_type < 0 || filter_matches(Ship_info[it->ship_info_index].class_type, filter.type_bitfield)) && filter_matches(it->team, filter.team_bitfield); @@ -2654,7 +2654,7 @@ bool add_message(const char* name, const char* message, int persona_index, int m msg.avi_info.index = -1; msg.wave_info.index = -1; } - Messages.push_back(msg); + Messages.push_back(std::move(msg)); Num_messages++; return true; diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index 3f9c444d6d1..70b4f64a465 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -6427,7 +6427,7 @@ void parse_asteroid_fields(mission *pm) } if (valid){ - Asteroid_field.field_asteroid_type.push_back(ast_name); + Asteroid_field.field_asteroid_type.push_back(std::move(ast_name)); } else { WarningEx(LOCATION, "Mission %s\n Invalid asteroid %s!", pm->name, ast_name.c_str()); } @@ -6671,7 +6671,7 @@ void parse_custom_data(mission* pm) required_string("+String:"); stuff_string(cs.text, F_MULTITEXT); - pm->custom_strings.push_back(cs); + pm->custom_strings.push_back(std::move(cs)); } required_string("$end_custom_strings"); diff --git a/code/missioneditor/missionsave.cpp b/code/missioneditor/missionsave.cpp index e46fb6b590c..505163dee6c 100644 --- a/code/missioneditor/missionsave.cpp +++ b/code/missioneditor/missionsave.cpp @@ -1395,13 +1395,13 @@ void Fred_mission_save::fso_comment_push(const char* ver) return; } - SCP_string before = fso_ver_comment.back(); + const auto &before = fso_ver_comment.back(); int major, minor, build, revis; int in_major, in_minor, in_build, in_revis; int elem1, elem2; - elem1 = scan_fso_version_string(fso_ver_comment.back().c_str(), &major, &minor, &build, &revis); + elem1 = scan_fso_version_string(before.c_str(), &major, &minor, &build, &revis); elem2 = scan_fso_version_string(ver, &in_major, &in_minor, &in_build, &in_revis); // check consistency diff --git a/code/missionui/missionweaponchoice.cpp b/code/missionui/missionweaponchoice.cpp index 151222c9306..6f8f20cf1ca 100644 --- a/code/missionui/missionweaponchoice.cpp +++ b/code/missionui/missionweaponchoice.cpp @@ -4171,7 +4171,7 @@ void wl_apply_current_loadout_to_all_ships_in_current_wing() { SCP_string temp; sprintf(temp, XSTR("%s is unable to carry %s weaponry", 1629), ship_name, wep_display_name); - error_messages.push_back(temp); + error_messages.push_back(std::move(temp)); error_flag = true; continue; @@ -4187,7 +4187,7 @@ void wl_apply_current_loadout_to_all_ships_in_current_wing() sprintf(temp, XSTR("%s is unable to carry %s weaponry in primary bank %d", 1630), ship_name, wep_display_name, cur_bank+1); else sprintf(temp, XSTR("%s is unable to carry %s weaponry in secondary bank %d", 1631), ship_name, wep_display_name, cur_bank+1-MAX_SHIP_PRIMARY_BANKS); - error_messages.push_back(temp); + error_messages.push_back(std::move(temp)); error_flag = true; continue; @@ -4202,7 +4202,7 @@ void wl_apply_current_loadout_to_all_ships_in_current_wing() { SCP_string temp; sprintf(temp, XSTR("Insufficient %s available to arm %s", 1632), Weapon_info[weapon_type_to_add].get_display_name(), ship_name); - error_messages.push_back(temp); + error_messages.push_back(std::move(temp)); error_flag = true; continue; diff --git a/code/missionui/redalert.cpp b/code/missionui/redalert.cpp index 726faa660f1..261d7737ed3 100644 --- a/code/missionui/redalert.cpp +++ b/code/missionui/redalert.cpp @@ -767,8 +767,8 @@ void red_alert_store_ship_status() red_alert_store_weapons(&ras, &shipp->weapons); red_alert_store_subsys_status(&ras, shipp); - Red_alert_ship_status.push_back( ras ); - // niffiwan: trying to track down red alert bug creating HUGE pilot files + Red_alert_ship_status.push_back( std::move(ras) ); + // niffiwan: trying to track down red alert bug creating HUGE pilot files Assert( (Red_alert_ship_status.size() <= MAX_SHIPS) ); if (shipp->wingnum >= 0) { @@ -803,8 +803,8 @@ void red_alert_store_ship_status() red_alert_store_weapons(&ras, NULL); red_alert_store_subsys_status(&ras, NULL); - Red_alert_ship_status.push_back( ras ); - // niffiwan: trying to track down red alert bug creating HUGE pilot files + Red_alert_ship_status.push_back( std::move(ras) ); + // niffiwan: trying to track down red alert bug creating HUGE pilot files Assert( (Red_alert_ship_status.size() <= MAX_SHIPS) ); if (exited.wingnum >= 0) { @@ -829,7 +829,7 @@ void red_alert_store_ship_status() rws.total_destroyed = wingp->total_destroyed; rws.total_vanished = wingp->total_vanished; - Red_alert_wing_status.push_back( rws ); + Red_alert_wing_status.push_back( std::move(rws) ); // see comments from niffiwan above Assert( (Red_alert_wing_status.size() <= MAX_WINGS) ); } diff --git a/code/mod_table/mod_table.cpp b/code/mod_table/mod_table.cpp index 29de69de533..2a7176fd829 100644 --- a/code/mod_table/mod_table.cpp +++ b/code/mod_table/mod_table.cpp @@ -300,7 +300,7 @@ void parse_mod_table(const char *filename) splash.is_default = true; } - Splash_screens.push_back(splash); + Splash_screens.push_back(std::move(splash)); } } diff --git a/code/model/animation/modelanimation.cpp b/code/model/animation/modelanimation.cpp index bbd4f008dac..722e8d2b4bd 100644 --- a/code/model/animation/modelanimation.cpp +++ b/code/model/animation/modelanimation.cpp @@ -202,7 +202,7 @@ namespace animation { auto thisPtr = shared_from_this(); - animEntry.animationList.push_back(thisPtr); + animEntry.animationList.push_back(std::move(thisPtr)); } /* fall-thru */ @@ -686,20 +686,20 @@ namespace animation { for (const auto& submodel : other.m_submodels) { auto newSubmodel = std::shared_ptr(submodel->copy()); newSubmodel->m_submodel = {}; - m_submodels.push_back(newSubmodel); + m_submodels.push_back(std::move(newSubmodel)); } for (const auto& animationTypes : other.m_animationSet) { auto& newAnimations = m_animationSet[animationTypes.first]; for (const auto& oldAnimations : animationTypes.second) { for (const auto& oldAnimation : oldAnimations.second) { - std::shared_ptr newAnimation = std::shared_ptr(new ModelAnimation(*oldAnimation)); + std::shared_ptr newAnimation = std::make_shared(*oldAnimation); newAnimation->m_animation = std::shared_ptr(newAnimation->m_animation->copy()); newAnimation->m_animation->exchangeSubmodelPointers(*this); newAnimation->m_set = this; - newAnimations[oldAnimations.first].push_back(newAnimation); + newAnimations[oldAnimations.first].push_back(std::move(newAnimation)); } } } @@ -710,7 +710,7 @@ namespace animation { } std::shared_ptr ModelAnimationSet::emplace(const std::shared_ptr& animation, const SCP_string& request, const SCP_string& name, ModelAnimationTriggerType type, int subtype, unsigned int uniqueId) { - auto newAnim = std::shared_ptr(new ModelAnimation(*animation)); + auto newAnim = std::make_shared(*animation); newAnim->m_set = this; newAnim->m_animation = std::shared_ptr(animation->m_animation->copy()); newAnim->m_animation->exchangeSubmodelPointers(*this); @@ -1248,11 +1248,11 @@ namespace animation { switch (turretStatus) { case 0: //Submodel - return std::shared_ptr(new ModelAnimationSubmodel(name)); + return std::make_shared(name); case 1: //Turret Base - return std::shared_ptr(new ModelAnimationSubmodelTurret(name, false, "")); + return std::make_shared(name, false, ""); case 2: //Turret Arm - return std::shared_ptr(new ModelAnimationSubmodelTurret(name, true, "")); + return std::make_shared(name, true, ""); default: //Not specified return nullptr; } @@ -1285,7 +1285,7 @@ namespace animation { return; } - auto animation = std::shared_ptr(new ModelAnimation(type == ModelAnimationTriggerType::Initial)); + auto animation = std::make_shared(type == ModelAnimationTriggerType::Initial); int subtype = ModelAnimationSet::SUBTYPE_DEFAULT; SCP_string name; @@ -1490,7 +1490,7 @@ namespace animation { curve = Curves[curve_id]; } - driver = [remap_driver_source, curve](ModelAnimation &, ModelAnimation::instance_data &instance, polymodel_instance *pmi, float) { + driver = [remap_driver_source = std::move(remap_driver_source), curve](ModelAnimation &, ModelAnimation::instance_data &instance, polymodel_instance *pmi, float) { float oldFrametime = instance.time; instance.time = curve ? curve->GetValue(remap_driver_source(pmi)) : remap_driver_source(pmi); CLAMP(instance.time, 0.0f, instance.duration); @@ -1515,7 +1515,7 @@ namespace animation { curve = Curves[curve_id]; } - propertyDrivers.emplace_back([driver_source, curve, target](ModelAnimation &, ModelAnimation::instance_data &instance, polymodel_instance *pmi) { + propertyDrivers.emplace_back([driver_source = std::move(driver_source), curve, target](ModelAnimation &, ModelAnimation::instance_data &instance, polymodel_instance *pmi) { float& property = instance.*(target.target); property = curve ? curve->GetValue(driver_source(pmi)) : driver_source(pmi); if(target.clamp) { @@ -1541,7 +1541,7 @@ namespace animation { curve = Curves[curve_id]; } - startupDrivers.emplace_back([driver_source, curve, target](ModelAnimation &, ModelAnimation::instance_data &instance, polymodel_instance *pmi) { + startupDrivers.emplace_back([driver_source = std::move(driver_source), curve, target](ModelAnimation &, ModelAnimation::instance_data &instance, polymodel_instance *pmi) { float& property = instance.*(target.target); property = curve ? curve->GetValue(driver_source(pmi)) : driver_source(pmi); if(target.clamp) { @@ -1676,7 +1676,7 @@ namespace animation { if (optional_string("+time:")) skip_token(); - auto anim = std::shared_ptr(new ModelAnimation(true)); + auto anim = std::make_shared(true); char namelower[MAX_NAME_LEN]; strncpy(namelower, sp->subobj_name, MAX_NAME_LEN); @@ -1685,12 +1685,12 @@ namespace animation { //sadly, we also need to check for engine and radar, since these take precedent (as in, an engineturret is an engine before a turret type) if (!strstr(namelower, "engine") && !strstr(namelower, "radar") && strstr(namelower, "turret")) { auto subsysBase = sip->animations.getSubmodel(sp->subobj_name, sip->name, false); - auto rotBase = std::shared_ptr(new ModelAnimationSegmentSetAngle(std::move(subsysBase), angle.h)); + auto rotBase = std::make_shared(std::move(subsysBase), angle.h); auto subsysBarrel = sip->animations.getSubmodel(sp->subobj_name, sip->name, true); - auto rotBarrel = std::shared_ptr(new ModelAnimationSegmentSetAngle(std::move(subsysBarrel), angle.p)); + auto rotBarrel = std::make_shared(std::move(subsysBarrel), angle.p); - auto rot = std::shared_ptr(new ModelAnimationSegmentParallel()); + auto rot = std::make_shared(); rot->addSegment(std::move(rotBase)); rot->addSegment(std::move(rotBarrel)); @@ -1707,7 +1707,7 @@ namespace animation { sip->animations.emplace(anim, name, name, ModelAnimationTriggerType::Initial, ModelAnimationSet::SUBTYPE_DEFAULT, ModelAnimationParseHelper::getUniqueAnimationID(name + Animation_types.at(ModelAnimationTriggerType::Initial).first + std::to_string(subtype), 'b', sip->name)); } else { - auto anim = std::shared_ptr(new ModelAnimation()); + auto anim = std::make_shared(); auto subsys = sip->animations.getSubmodel(sp->subobj_name); if (type == ModelAnimationTriggerType::TurretFired) { @@ -1715,12 +1715,12 @@ namespace animation { anim->m_flags += Animation_Flags::Reset_at_completion; } - auto mainSegment = std::shared_ptr(new ModelAnimationSegmentSerial()); + auto mainSegment = std::make_shared(); if (optional_string("+delay:")) { int delayByMs; stuff_int(&delayByMs); - auto delay = std::shared_ptr(new ModelAnimationSegmentWait(((float)delayByMs) * 0.001f)); + auto delay = std::make_shared(((float)delayByMs) * 0.001f); mainSegment->addSegment(delay); } @@ -1785,14 +1785,14 @@ namespace animation { required_string("+Radius:"); stuff_float(&snd_rad); - auto sound = std::shared_ptr(new ModelAnimationSegmentSoundDuring(rotation, start_sound, end_sound, loop_sound, true)); + auto sound = std::make_shared(rotation, start_sound, end_sound, loop_sound, true); mainSegment->addSegment(sound); } else mainSegment->addSegment(rotation); if (delayByMsReverse != -1) { - auto delay = std::shared_ptr(new ModelAnimationSegmentWait(((float)delayByMsReverse) * 0.001f)); + auto delay = std::make_shared(((float)delayByMsReverse) * 0.001f); mainSegment->addSegment(delay); } diff --git a/code/model/animation/modelanimation_moveables.cpp b/code/model/animation/modelanimation_moveables.cpp index dd34c8f151e..7434eb0bb1a 100644 --- a/code/model/animation/modelanimation_moveables.cpp +++ b/code/model/animation/modelanimation_moveables.cpp @@ -32,9 +32,9 @@ namespace animation { void ModelAnimationMoveableOrientation::initialize(ModelAnimationSet* parentSet, polymodel_instance* pmi) { auto& anim = m_instances[pmi->id].animation; - anim = std::shared_ptr(new ModelAnimation(false, false, true, parentSet)); + anim = std::make_shared(false, false, true, parentSet); - anim->setAnimation(std::shared_ptr(new ModelAnimationSegmentSetOrientation(parentSet->getSubmodel(m_submodel), m_defaultPosOrient, ModelAnimationCoordinateRelation::RELATIVE_COORDS))); + anim->setAnimation(std::make_shared(parentSet->getSubmodel(m_submodel), m_defaultPosOrient, ModelAnimationCoordinateRelation::RELATIVE_COORDS)); anim->start(pmi,ModelAnimationDirection::FWD); } @@ -48,7 +48,7 @@ namespace animation { if(submodel == nullptr) error_display(1, "Could not create moveable! Moveable Orientation has no target submodel!"); - return std::shared_ptr(new ModelAnimationMoveableOrientation(submodel, angle)); + return std::make_shared(std::move(submodel), angle); } @@ -92,16 +92,16 @@ namespace animation { void ModelAnimationMoveableRotation::initialize(ModelAnimationSet* parentSet, polymodel_instance* pmi) { auto& anim = m_instances[pmi->id].animation; - anim = std::shared_ptr(new ModelAnimation(false, false, true, parentSet)); + anim = std::make_shared(false, false, true, parentSet); matrix orient; vm_angles_2_matrix(&orient, &m_defaultPosOrient); auto submodel = parentSet->getSubmodel(m_submodel); - auto sequence = std::shared_ptr(new ModelAnimationSegmentSerial()); - sequence->addSegment(std::shared_ptr(new ModelAnimationSegmentSetOrientation(submodel, orient, ModelAnimationCoordinateRelation::RELATIVE_COORDS))); - sequence->addSegment(std::shared_ptr(new ModelAnimationSegmentRotation(submodel, m_defaultPosOrient, m_velocity, std::nullopt, m_acceleration, ModelAnimationCoordinateRelation::ABSOLUTE_COORDS))); + auto sequence = std::make_shared(); + sequence->addSegment(std::make_shared(submodel, orient, ModelAnimationCoordinateRelation::RELATIVE_COORDS)); + sequence->addSegment(std::make_shared(std::move(submodel), m_defaultPosOrient, m_velocity, std::nullopt, m_acceleration, ModelAnimationCoordinateRelation::ABSOLUTE_COORDS)); anim->setAnimation(sequence); @@ -128,7 +128,7 @@ namespace animation { if(submodel == nullptr) error_display(1, "Could not create moveable! Moveable Rotation has no target submodel!"); - return std::shared_ptr(new ModelAnimationMoveableRotation(submodel, angle, velocity, acceleration)); + return std::make_shared(std::move(submodel), angle, velocity, acceleration); } @@ -172,13 +172,13 @@ namespace animation { void ModelAnimationMoveableTranslation::initialize(ModelAnimationSet* parentSet, polymodel_instance* pmi) { auto& anim = m_instances[pmi->id].animation; - anim = std::shared_ptr(new ModelAnimation(false, false, true, parentSet)); + anim = std::make_shared(false, false, true, parentSet); auto submodel = parentSet->getSubmodel(m_submodel); - auto sequence = std::shared_ptr(new ModelAnimationSegmentSerial()); - sequence->addSegment(std::shared_ptr(new ModelAnimationSegmentSetOffset(submodel, m_defaultOffset, ModelAnimationCoordinateRelation::RELATIVE_COORDS))); - sequence->addSegment(std::shared_ptr(new ModelAnimationSegmentTranslation(std::move(submodel), m_defaultOffset, m_velocity, std::nullopt, m_acceleration, ModelAnimationSegmentTranslation::CoordinateSystem::COORDS_PARENT, ModelAnimationCoordinateRelation::ABSOLUTE_COORDS))); + auto sequence = std::make_shared(); + sequence->addSegment(std::make_shared(submodel, m_defaultOffset, ModelAnimationCoordinateRelation::RELATIVE_COORDS)); + sequence->addSegment(std::make_shared(std::move(submodel), m_defaultOffset, m_velocity, std::nullopt, m_acceleration, ModelAnimationSegmentTranslation::CoordinateSystem::COORDS_PARENT, ModelAnimationCoordinateRelation::ABSOLUTE_COORDS)); anim->setAnimation(sequence); @@ -205,7 +205,7 @@ namespace animation { if(submodel == nullptr) error_display(1, "Could not create moveable! Moveable Translation has no target submodel!"); - return std::shared_ptr(new ModelAnimationMoveableTranslation(submodel, angle, velocity, acceleration)); + return std::make_shared(std::move(submodel), angle, velocity, acceleration); } @@ -258,13 +258,13 @@ namespace animation { void ModelAnimationMoveableAxisRotation::initialize(ModelAnimationSet* parentSet, polymodel_instance* pmi) { auto& anim = m_instances[pmi->id].animation; - anim = std::shared_ptr(new ModelAnimation(false, false, true, parentSet)); + anim = std::make_shared(false, false, true, parentSet); auto submodel = parentSet->getSubmodel(m_submodel); - auto sequence = std::shared_ptr(new ModelAnimationSegmentSerial()); - sequence->addSegment(std::shared_ptr(new ModelAnimationSegmentSetOrientation(submodel, vmd_identity_matrix, ModelAnimationCoordinateRelation::RELATIVE_COORDS))); - sequence->addSegment(std::shared_ptr(new ModelAnimationSegmentAxisRotation(submodel, 0.0f, m_velocity, std::nullopt, m_acceleration, m_axis))); + auto sequence = std::make_shared(); + sequence->addSegment(std::make_shared(submodel, vmd_identity_matrix, ModelAnimationCoordinateRelation::RELATIVE_COORDS)); + sequence->addSegment(std::make_shared(std::move(submodel), 0.0f, m_velocity, std::nullopt, m_acceleration, m_axis)); anim->setAnimation(sequence); @@ -292,7 +292,7 @@ namespace animation { if(submodel == nullptr) error_display(1, "Could not create moveable! Moveable Axis Rotation has no target submodel!"); - return std::shared_ptr(new ModelAnimationMoveableAxisRotation(submodel, velocity, acceleration, axis)); + return std::make_shared(std::move(submodel), velocity, acceleration, axis); } @@ -353,17 +353,17 @@ namespace animation { void ModelAnimationMoveableIK::initialize(ModelAnimationSet* parentSet, polymodel_instance* pmi) { auto& anim = m_instances[pmi->id].animation; - anim = std::shared_ptr(new ModelAnimation(false, false, true, parentSet)); + anim = std::make_shared(false, false, true, parentSet); - auto sequence = std::shared_ptr(new ModelAnimationSegmentSerial()); - auto parallelSetOrient = std::shared_ptr(new ModelAnimationSegmentParallel()); + auto sequence = std::make_shared(); + auto parallelSetOrient = std::make_shared(); for(const auto& link : m_chain) { auto submodel = parentSet->getSubmodel(link.submodel); - parallelSetOrient->addSegment(std::shared_ptr(new ModelAnimationSegmentSetOrientation(submodel, vmd_identity_matrix, ModelAnimationCoordinateRelation::RELATIVE_COORDS))); + parallelSetOrient->addSegment(std::make_shared(submodel, vmd_identity_matrix, ModelAnimationCoordinateRelation::RELATIVE_COORDS)); } sequence->addSegment(parallelSetOrient); - auto parallelIK = std::shared_ptr(new ModelAnimationSegmentParallel()); + auto parallelIK = std::make_shared(); vec3d startPos = ZERO_VECTOR; for(size_t i = 1; i < m_chain.size(); ++i) { @@ -413,7 +413,7 @@ namespace animation { required_string("Window"); required_string("+Window Size:"); stuff_angles_deg_phb(&window); - constraint = std::shared_ptr(new ik_constraint_window(window)); + constraint = std::make_shared(window); break; } case 1: { //Hinge @@ -422,7 +422,7 @@ namespace animation { required_string("+Axis:"); stuff_vec3d(&axis); vm_vec_normalize(&axis); - constraint = std::shared_ptr(new ik_constraint_hinge(axis)); + constraint = std::make_shared(axis); break; } default: @@ -430,11 +430,11 @@ namespace animation { break; } } else - constraint = std::shared_ptr(new ik_constraint()); + constraint = std::make_shared(); - chain.push_back({std::move(submodel), constraint, acceleration}); + chain.push_back({std::move(submodel), std::move(constraint), acceleration}); } - return std::shared_ptr(new ModelAnimationMoveableIK(chain, time)); + return std::make_shared(std::move(chain), time); } } diff --git a/code/model/animation/modelanimation_segments.cpp b/code/model/animation/modelanimation_segments.cpp index f2a48ed634a..b208c79bed1 100644 --- a/code/model/animation/modelanimation_segments.cpp +++ b/code/model/animation/modelanimation_segments.cpp @@ -79,7 +79,7 @@ namespace animation { auto submodelOverride = ModelAnimationParseHelper::parseSubmodel(); ignore_white_space(); - auto segment = std::shared_ptr(new ModelAnimationSegmentSerial()); + auto segment = std::make_shared(); while (!optional_string("+End Segment")) { if (submodelOverride) @@ -153,7 +153,7 @@ namespace animation { auto submodelOverride = ModelAnimationParseHelper::parseSubmodel(); ignore_white_space(); - auto segment = std::shared_ptr(new ModelAnimationSegmentParallel()); + auto segment = std::make_shared(); while (!optional_string("+End Segment")) { if (submodelOverride) @@ -181,7 +181,7 @@ namespace animation { required_string("+Time:"); float time = 0.0f; stuff_float(&time); - auto segment = std::shared_ptr(new ModelAnimationSegmentWait(time)); + auto segment = std::make_shared(time); return segment; } @@ -253,7 +253,7 @@ namespace animation { error_display(1, "Set Orientation has no target submodel!"); } - auto segment = std::shared_ptr(new ModelAnimationSegmentSetOrientation(submodel, angle, relationType)); + auto segment = std::make_shared(std::move(submodel), angle, relationType); return segment; } @@ -308,7 +308,7 @@ namespace animation { error_display(1, "Set Offset has no target submodel!"); } - auto segment = std::shared_ptr(new ModelAnimationSegmentSetOffset(submodel, target, relationType)); + auto segment = std::make_shared(std::move(submodel), target, relationType); return segment; } @@ -382,7 +382,7 @@ namespace animation { error_display(1, "Set Angle has no target submodel!"); } - auto segment = std::shared_ptr(new ModelAnimationSegmentSetAngle(submodel, fl_radians(angle))); + auto segment = std::make_shared(std::move(submodel), fl_radians(angle)); return segment; } @@ -667,7 +667,7 @@ namespace animation { error_display(1, "Rotation has no target submodel!"); } - auto segment = std::shared_ptr(new ModelAnimationSegmentRotation(submodel, angle, velocity, time, acceleration, relationType)); + auto segment = std::make_shared(std::move(submodel), angle, velocity, time, acceleration, relationType); return segment; } @@ -916,7 +916,7 @@ namespace animation { error_display(1, "Rotation has no target submodel!"); } - auto segment = std::shared_ptr(new ModelAnimationSegmentAxisRotation(submodel, angle, velocity, time, acceleration, axis)); + auto segment = std::make_shared(std::move(submodel), angle, velocity, time, acceleration, axis); return segment; } @@ -1231,7 +1231,7 @@ namespace animation { error_display(1, "Translation has no target submodel!"); } - auto segment = std::shared_ptr(new ModelAnimationSegmentTranslation(submodel, offset, velocity, time, acceleration, coordSystem, relationType)); + auto segment = std::make_shared(std::move(submodel), offset, velocity, time, acceleration, coordSystem, relationType); return segment; } @@ -1381,7 +1381,7 @@ namespace animation { bool flipIfReversed = optional_string("+Flip When Reversed"); bool abortSoundIfRunning = !optional_string("+Don't Interrupt Playing Sounds"); - auto segment = std::shared_ptr(new ModelAnimationSegmentSoundDuring(data->parseSegment(), start_sound, end_sound, loop_sound, flipIfReversed, abortSoundIfRunning, snd_rad, submodel, position)); + auto segment = std::make_shared(data->parseSegment(), start_sound, end_sound, loop_sound, flipIfReversed, abortSoundIfRunning, snd_rad, std::move(submodel), std::move(position)); return segment; } @@ -1504,7 +1504,7 @@ namespace animation { }; void ModelAnimationSegmentIK::recalculate(ModelAnimationSubmodelBuffer& base, ModelAnimationSubmodelBuffer& currentAnimDelta, polymodel_instance* pmi) { - auto ik = std::unique_ptr(new ik_solver_fabrik()); + auto ik = std::make_unique(); polymodel* pm = model_get(pmi->model_num); bsp_info* lastSubmodel = nullptr; @@ -1568,8 +1568,8 @@ namespace animation { float time; stuff_float(&time); - auto segment = std::shared_ptr(new ModelAnimationSegmentIK(targetPosition, targetRotation)); - auto parallel = std::shared_ptr(new ModelAnimationSegmentParallel()); + auto segment = std::make_shared(targetPosition, targetRotation); + auto parallel = std::make_shared(); segment->m_segment = parallel; while(optional_string("$Chain Link:")){ @@ -1597,7 +1597,7 @@ namespace animation { required_string("Window"); required_string("+Window Size:"); stuff_angles_deg_phb(&window); - constraint = std::shared_ptr(new ik_constraint_window(window)); + constraint = std::make_shared(window); break; } case 1: { //Hinge @@ -1606,7 +1606,7 @@ namespace animation { required_string("+Axis:"); stuff_vec3d(&axis); vm_vec_normalize(&axis); - constraint = std::shared_ptr(new ik_constraint_hinge(axis)); + constraint = std::make_shared(axis); break; } default: @@ -1619,7 +1619,7 @@ namespace animation { auto rotation = std::make_shared(submodel, std::optional({0,0,0}), std::optional(), time, acceleration, ModelAnimationCoordinateRelation::ABSOLUTE_COORDS); parallel->addSegment(rotation); - segment->m_chain.push_back({submodel, constraint, rotation}); + segment->m_chain.push_back({std::move(submodel), std::move(constraint), std::move(rotation)}); } return segment; diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index a607ebc8b6d..aa465153070 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -3690,7 +3690,7 @@ void model_set_bay_path_nums(polymodel *pm) */ // malloc out storage for the path information - pm->ship_bay = make_shared(); + pm->ship_bay = std::make_shared(); pm->ship_bay->num_paths = 0; // TODO: determine if zeroing out here is affecting any earlier initializations @@ -5339,7 +5339,7 @@ int model_create_bsp_collision_tree() bsp_collision_tree tree{}; tree.used = true; - Bsp_collision_tree_list.push_back(tree); + Bsp_collision_tree_list.push_back(std::move(tree)); return (int)(Bsp_collision_tree_list.size() - 1); } diff --git a/code/model/modelrender.cpp b/code/model/modelrender.cpp index 651f46a2242..b43a73ce7bf 100644 --- a/code/model/modelrender.cpp +++ b/code/model/modelrender.cpp @@ -226,7 +226,7 @@ void model_render_params::set_replacement_textures(std::shared_ptr& replacement_textures) { - auto textures = make_shared(); + auto textures = std::make_shared(); polymodel* pm = model_get(modelnum); @@ -3129,7 +3129,7 @@ void modelinstance_replace_active_texture(polymodel_instance* pmi, const char* o texture = bm_load_either(new_name); if (pmi->texture_replace == nullptr) { - pmi->texture_replace = make_shared(); + pmi->texture_replace = std::make_shared(); } (*pmi->texture_replace)[final_index] = texture; diff --git a/code/model/modelreplace.cpp b/code/model/modelreplace.cpp index acbb221a363..f929b997337 100644 --- a/code/model/modelreplace.cpp +++ b/code/model/modelreplace.cpp @@ -12,17 +12,17 @@ static SCP_unordered_map, SCP_string_lcase_hash, SCP_string_lcase_equal_to> virtual_pofs; static SCP_unordered_map()>> virtual_pof_operations = { - {"$Add Subobject:", &make_unique }, - {"$Add Turret:", &make_unique }, - {"$Add Engine:", &make_unique }, - {"$Add Glowpoint:", &make_unique }, - {"$Add Weapon Bank:", &make_unique }, - {"$Add Dock Point:", &make_unique }, - {"$Add Path:", &make_unique }, - {"$Rename Subobjects:", &make_unique }, - {"$Set Subsystem Data:", &make_unique }, - {"$Set Subobject Data:", &make_unique }, - {"$Set Header Data:", &make_unique } + {"$Add Subobject:", &std::make_unique }, + {"$Add Turret:", &std::make_unique }, + {"$Add Engine:", &std::make_unique }, + {"$Add Glowpoint:", &std::make_unique }, + {"$Add Weapon Bank:", &std::make_unique }, + {"$Add Dock Point:", &std::make_unique }, + {"$Add Path:", &std::make_unique }, + {"$Rename Subobjects:", &std::make_unique }, + {"$Set Subsystem Data:", &std::make_unique }, + {"$Set Subobject Data:", &std::make_unique }, + {"$Set Header Data:", &std::make_unique } }; /* @@ -86,7 +86,7 @@ static class VirtualPOFBuildCache { //Don't load from cache if it's a virtual pof (always reload these) or we don't have it cached if ((vp_it != virtual_pofs.end() && (int)vp_it->second.size() > depth[pof_name]) || it == cache.end()) { - auto pmh = ::make_shared(pof_name, depth); + auto pmh = std::make_shared(pof_name, depth); if(pmh->needs_emplace) cache.emplace(pof_name, pmh); return pmh; @@ -250,7 +250,7 @@ int reallocate_and_copy_array(std::shared_ptr& array, int& size, size_t to_ //Generates one function for replacing data in a type, which is a map entry of which the key may be replaced. Takes an rvalue reference, used for making a copy and modifying the temporary to then assign it somewhere #define CHANGE_HELPER_MAP_KEY(name, intype, argtype) template static typename std::enable_if>::value, intype>::type name(intype&& pass, map_t replace){ \ const auto it = replace.find(pass.first); \ - intype input = { (it == replace.end() ? pass.first : it->second), pass.second }; + intype input = { (it == replace.end() ? pass.first : it->second), std::move(pass.second) }; #define CHANGE_HELPER_MAP_KEY_END return input; } //Generates two functions for replacing data in a type. One that takes an rvalue reference, used for making a copy and modifying the temporary to then assign it somewhere, and one which takes an lvalue reference for modifying in-place @@ -340,7 +340,7 @@ VirtualPOFOperationAddSubmodel::VirtualPOFOperationAddSubmodel() { } if (optional_string("$Rename Subobjects:")) { - rename = make_unique(); + rename = std::make_unique(); } required_string("+Destination Subobject:"); diff --git a/code/network/multi_fstracker.cpp b/code/network/multi_fstracker.cpp index 407bb61a333..7e09ac2df95 100644 --- a/code/network/multi_fstracker.cpp +++ b/code/network/multi_fstracker.cpp @@ -1290,7 +1290,7 @@ bool multi_fs_tracker_validate_mission_list(SCP_vector &file_ cf_chksum_long(entry.filename, &item.crc); item.name = entry.filename; - vdr.files.push_back(item); + vdr.files.push_back(std::move(item)); vdr.num_files++; packet_size += len; @@ -1375,7 +1375,7 @@ static int validate_table_list(const SCP_vector &table_list, int &ga cf_chksum_long(tbl.c_str(), &item.crc); item.name = tbl; - vdr.files.push_back(item); + vdr.files.push_back(std::move(item)); vdr.num_files++; packet_size += len; diff --git a/code/network/multi_obj.cpp b/code/network/multi_obj.cpp index eadd6b0e156..760b31af85b 100644 --- a/code/network/multi_obj.cpp +++ b/code/network/multi_obj.cpp @@ -2726,8 +2726,8 @@ void multi_init_oo_and_ship_tracker() temp_sent_to_player.subsystem_z.reserve(MAX_MODEL_SUBSYSTEMS); temp_sent_to_player.subsystem_z.push_back(0.0f); - temp_netplayer_records.last_sent.push_back(temp_sent_to_player); - Oo_info.frame_info.push_back(temp_position_records); + temp_netplayer_records.last_sent.push_back(std::move(temp_sent_to_player)); + Oo_info.frame_info.push_back(std::move(temp_position_records)); for (int i = 0; i < MAX_PLAYERS; i++) { Oo_info.player_frame_info.push_back(temp_netplayer_records); diff --git a/code/network/multi_pxo.cpp b/code/network/multi_pxo.cpp index 398c5db88da..6cbc8326c60 100644 --- a/code/network/multi_pxo.cpp +++ b/code/network/multi_pxo.cpp @@ -2747,7 +2747,7 @@ void multi_pxo_clear_players() void multi_pxo_add_player(const char *name) { SCP_string new_player = name; - Multi_pxo_players.push_back(new_player); + Multi_pxo_players.push_back(std::move(new_player)); } /** diff --git a/code/network/multimsgs.cpp b/code/network/multimsgs.cpp index 182c94d4d2d..6d1ba57db17 100644 --- a/code/network/multimsgs.cpp +++ b/code/network/multimsgs.cpp @@ -8783,7 +8783,7 @@ void process_flak_fired_packet(ubyte *data, header *hinfo) //spawn particle effect auto particleSource = particle::ParticleManager::get()->createSource(wip.muzzle_effect); //This could potentially be attached to the ship, but might look weird if the spawn position of the weapon is ever interpolated away from the ship's barrel. - particleSource->setHost(make_unique(pos, orient, objp->phys_info.vel)); + particleSource->setHost(std::make_unique(pos, orient, objp->phys_info.vel)); particleSource->setTriggerRadius(wp_obj.radius * radius_mult); particleSource->setTriggerVelocity(vm_vec_mag_quick(&wp_obj.phys_info.vel)); particleSource->finishCreation(); diff --git a/code/network/psnet2.cpp b/code/network/psnet2.cpp index f209051be3e..3dee7742656 100644 --- a/code/network/psnet2.cpp +++ b/code/network/psnet2.cpp @@ -1085,13 +1085,13 @@ static bool psnet_explode_ip_string(const char *ip_string, SCP_string &host, SCP // if it has no dots then assume it's just IPv6 if (dot == SCP_string::npos) { - host = ip; + host = std::move(ip); return true; } // if colon before dots then it's likely mapped IPv4 if ( (colon != SCP_string::npos) && (dot != SCP_string::npos) && (colon < dot) ) { - host = ip; + host = std::move(ip); return true; } diff --git a/code/object/collideshipweapon.cpp b/code/object/collideshipweapon.cpp index 25b33f50c67..5d31296c841 100644 --- a/code/object/collideshipweapon.cpp +++ b/code/object/collideshipweapon.cpp @@ -702,7 +702,7 @@ static std::tuple prop_weapon_check_coll bool recheck = false; // most of this data is irrevelant for props but let's match it to make it easy ship_weapon_collision_data cd{mc, -1, postproc, -1, -1, ZERO_VECTOR, false, false}; - return {postproc, recheck, cd}; + return {postproc, recheck, std::move(cd)}; } } @@ -713,7 +713,7 @@ static std::tuple prop_weapon_check_coll // most of this data is irrevelant for props but let's match it to make it easy ship_weapon_collision_data cd{(valid_hit_occurred ? mc : mc_info{}), -1, postproc, -1, -1, ZERO_VECTOR, false, false}; - return{postproc, recheck, cd}; + return{postproc, recheck, std::move(cd)}; } static void prop_weapon_process_collision(obj_pair* pair, const ship_weapon_collision_data& cd) diff --git a/code/object/object.cpp b/code/object/object.cpp index aabd5dd4f43..06bbf7cb240 100644 --- a/code/object/object.cpp +++ b/code/object/object.cpp @@ -1903,7 +1903,7 @@ void obj_queue_render(object* obj, model_draw_list* scene) // Always execute the hook content bool skip_render = scripting::hooks::OnObjectRender->isOverride(scripting::hooks::ObjectDrawConditions{ obj }, param_list); - scripting::hooks::OnObjectRender->run(scripting::hooks::ObjectDrawConditions{ obj }, param_list); + scripting::hooks::OnObjectRender->run(scripting::hooks::ObjectDrawConditions{ obj }, std::move(param_list)); // Clear the render scene context scripting::api::Current_scene = nullptr; diff --git a/code/options/Option.h b/code/options/Option.h index 44032a80f1a..d6de2845599 100644 --- a/code/options/Option.h +++ b/code/options/Option.h @@ -532,8 +532,8 @@ class OptionBuilder { display_mapping.emplace(p.first, p.second); } - _instance.setValueEnumerator(VectorEnumerator(values)); - _instance.setDisplayFunc(MapValueDisplay(display_mapping)); + _instance.setValueEnumerator(VectorEnumerator(std::move(values))); + _instance.setDisplayFunc(MapValueDisplay(std::move(display_mapping))); return *this; } //The string to display for the option values, usually a function that returns a string @@ -608,7 +608,7 @@ class OptionBuilder { _instance.setPreset(val.first, json_dump_string_new(_instance.getSerializer()(val.second), JSON_COMPACT | JSON_ENSURE_ASCII | JSON_ENCODE_ANY)); } - auto opt_ptr = make_shared>(_instance); + auto opt_ptr = std::make_shared>(_instance); if (std::holds_alternative>(_title)) { const auto& xstr_info = std::get>(_title); diff --git a/code/options/OptionsManager.cpp b/code/options/OptionsManager.cpp index 1510ba58571..b18bebebb10 100644 --- a/code/options/OptionsManager.cpp +++ b/code/options/OptionsManager.cpp @@ -265,7 +265,7 @@ void OptionsManager::set_ingame_binary_option(SCP_string key, bool value) return; } - const OptionBase* thisOpt = getOptionByKey(key); + const OptionBase* thisOpt = getOptionByKey(std::move(key)); if (thisOpt != nullptr) { auto val = thisOpt->getCurrentValueDescription(); SCP_string newVal = value ? "true" : "false"; // OptionsManager stores values as serialized strings @@ -281,7 +281,7 @@ void OptionsManager::set_ingame_multi_option(SCP_string key, int value) return; } - const OptionBase* thisOpt = getOptionByKey(key); + const OptionBase* thisOpt = getOptionByKey(std::move(key)); if (thisOpt != nullptr) { auto values = thisOpt->getValidValues(); thisOpt->setValueDescription(values[value]); @@ -296,7 +296,7 @@ void OptionsManager::set_ingame_range_option(SCP_string key, int value) return; } - const OptionBase* thisOpt = getOptionByKey(key); + const OptionBase* thisOpt = getOptionByKey(std::move(key)); if (thisOpt != nullptr) { SCP_string newVal = std::to_string(value); // OptionsManager stores values as serialized strings thisOpt->setValueDescription({newVal.c_str(), newVal.c_str()}); @@ -311,7 +311,7 @@ void OptionsManager::set_ingame_range_option(SCP_string key, float value) return; } - const OptionBase* thisOpt = getOptionByKey(key); + const OptionBase* thisOpt = getOptionByKey(std::move(key)); if (thisOpt != nullptr) { SCP_string newVal = std::to_string(value); // OptionsManager stores values as serialized strings thisOpt->setValueDescription({newVal.c_str(), newVal.c_str()}); diff --git a/code/parse/parselo.cpp b/code/parse/parselo.cpp index a89fef40417..c4076577b9f 100644 --- a/code/parse/parselo.cpp +++ b/code/parse/parselo.cpp @@ -3649,7 +3649,7 @@ void pause_parse() Mark.Warning_count = Warning_count; Mark.Error_count = Error_count; - Bookmarks.push_back(Mark); + Bookmarks.push_back(std::move(Mark)); } // unpause parsing to continue with previously parsing file diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 2a8dc37e90a..b54df485689 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -28106,7 +28106,7 @@ void add_to_event_log_buffer(int node, int op_num, int result) } } - Current_event_log_buffer->push_back(tmp); + Current_event_log_buffer->push_back(std::move(tmp)); } /** diff --git a/code/parse/sexp/LuaAISEXP.cpp b/code/parse/sexp/LuaAISEXP.cpp index b7436fdbfb0..55181774fe6 100644 --- a/code/parse/sexp/LuaAISEXP.cpp +++ b/code/parse/sexp/LuaAISEXP.cpp @@ -134,7 +134,7 @@ void LuaAISEXP::parseTable() { if (optional_string("$Player Order:")) { - playerOrder = std::unique_ptr(new player_order_lua()); + playerOrder = std::make_unique(); auto &order = *playerOrder; if (_arg_type != OPF_SHIP && _arg_type != -1) { error_display(1, "Player orders must have either no target or a ship-type target parameter!"); @@ -265,10 +265,10 @@ void LuaAISEXP::parseTable() { } if (variable_arg_part) { - _varargs_type_pattern.push_back(type); + _varargs_type_pattern.push_back(std::move(type)); } else { - _argument_types.push_back(type); + _argument_types.push_back(std::move(type)); } if (optional_string("$Repeat")) { diff --git a/code/parse/sexp/LuaSEXP.cpp b/code/parse/sexp/LuaSEXP.cpp index 17ecb2dcd8e..0f8af3ee049 100644 --- a/code/parse/sexp/LuaSEXP.cpp +++ b/code/parse/sexp/LuaSEXP.cpp @@ -756,7 +756,7 @@ void LuaSEXP::parseTable() { if (skip) continue; - thisList.list.push_back(item); + thisList.list.push_back(std::move(item)); } if (thisList.list.size() == 0) { @@ -810,7 +810,7 @@ void LuaSEXP::parseTable() { dyn_param.operator_name = _name; dyn_param.parameter_map.push_back(param_map); - Dynamic_parameters.push_back(dyn_param); + Dynamic_parameters.push_back(std::move(dyn_param)); } } else if (parent_param_index >= 0) @@ -832,14 +832,14 @@ void LuaSEXP::parseTable() { if (optional_string("+Suffix:")) { SCP_string suffix; stuff_string(suffix, F_NAME); - Dynamic_enum_suffixes.push_back({_name, param_index, suffix}); + Dynamic_enum_suffixes.push_back({_name, param_index, std::move(suffix)}); } } if (variable_arg_part) { - _varargs_type_pattern.push_back(type); + _varargs_type_pattern.push_back(std::move(type)); } else { - _argument_types.push_back(type); + _argument_types.push_back(std::move(type)); } if (optional_string("$Repeat")) { diff --git a/code/parse/sexp/sexp_lookup.cpp b/code/parse/sexp/sexp_lookup.cpp index 60d9d7d585c..2f046b1f65f 100644 --- a/code/parse/sexp/sexp_lookup.cpp +++ b/code/parse/sexp/sexp_lookup.cpp @@ -88,7 +88,7 @@ void parse_sexp_table(const char* filename) { if (skip) continue; - thisList.list.push_back(item); + thisList.list.push_back(std::move(item)); } if (thisList.list.size() > 0) { diff --git a/code/particle/ParticleParse.cpp b/code/particle/ParticleParse.cpp index 1043434b8ae..243e90fb0c4 100644 --- a/code/particle/ParticleParse.cpp +++ b/code/particle/ParticleParse.cpp @@ -587,7 +587,7 @@ namespace particle { case ParticleEffectLegacyType::Sphere: { parseParticleProperties(effect); - effect.m_velocityVolume = make_shared(1.f, 1.f, 1.f); + effect.m_velocityVolume = std::make_shared(1.f, 1.f, 1.f); parseVelocityVolumeScale(effect); parseParticleNumber(effect); @@ -657,7 +657,7 @@ namespace particle { } } - effect.m_spawnVolume = make_shared(bias, stretch, radius); + effect.m_spawnVolume = std::make_shared(bias, stretch, radius); parseVelocityInherit(effect); parseTiming(effect); diff --git a/code/pilotfile/csg.cpp b/code/pilotfile/csg.cpp index 6b0f20933cc..a3bb059bc7f 100644 --- a/code/pilotfile/csg.cpp +++ b/code/pilotfile/csg.cpp @@ -895,7 +895,7 @@ void pilotfile::csg_read_redalert() // this is quite likely a *bad* thing if it doesn't happen if (ras.ship_class >= RED_ALERT_LOWEST_VALID_SHIP_CLASS) { - Red_alert_ship_status.push_back( ras ); + Red_alert_ship_status.push_back( std::move(ras) ); } } } @@ -924,7 +924,7 @@ void pilotfile::csg_read_redalert() rws.total_destroyed = cfread_int(cfp); rws.total_vanished = cfread_int(cfp); - Red_alert_wing_status.push_back(rws); + Red_alert_wing_status.push_back(std::move(rws)); } } diff --git a/code/pilotfile/csg_convert.cpp b/code/pilotfile/csg_convert.cpp index 14db23804a4..58a2ae8003a 100644 --- a/code/pilotfile/csg_convert.cpp +++ b/code/pilotfile/csg_convert.cpp @@ -477,7 +477,7 @@ void pilotfile_convert::csg_import_red_alert() } // add to list - csg->wingman_status.push_back( ras ); + csg->wingman_status.push_back( std::move(ras) ); } } diff --git a/code/pilotfile/plr.cpp b/code/pilotfile/plr.cpp index 2cfef118409..28a5b486689 100644 --- a/code/pilotfile/plr.cpp +++ b/code/pilotfile/plr.cpp @@ -64,7 +64,7 @@ void read_multi_stats(pilot::FileHandler* handler, scoring_special_t* scoring) { ilist.index = ship_info_lookup(ilist.name.c_str()); ilist.val = handler->readInt("val"); - scoring->ship_kills.push_back(ilist); + scoring->ship_kills.push_back(std::move(ilist)); } handler->endArrayRead(); @@ -78,7 +78,7 @@ void read_multi_stats(pilot::FileHandler* handler, scoring_special_t* scoring) { ilist.index = medals_info_lookup(ilist.name.c_str()); ilist.val = handler->readInt("val"); - scoring->medals_earned.push_back(ilist); + scoring->medals_earned.push_back(std::move(ilist)); } handler->endArrayRead(); } diff --git a/code/prop/prop.cpp b/code/prop/prop.cpp index ca5a6b3da93..2728e2ac07f 100644 --- a/code/prop/prop.cpp +++ b/code/prop/prop.cpp @@ -133,7 +133,7 @@ void parse_prop_table(const char* filename) if (existing != Prop_categories.end()) { *existing = pc; // Replace } else { - Prop_categories.push_back(pc); // Add new + Prop_categories.push_back(std::move(pc)); // Add new } } } @@ -346,7 +346,7 @@ void parse_prop_table(const char* filename) required_string("+String:"); stuff_string(cs.text, F_MULTITEXT); - pip->custom_strings.push_back(cs); + pip->custom_strings.push_back(std::move(cs)); } required_string("$end_custom_strings"); @@ -378,7 +378,7 @@ void post_process_props() prop_category pc; pc.name = UnknownCategory; gr_init_color(&pc.list_color, 128, 128, 128); - Prop_categories.push_back(pc); + Prop_categories.push_back(std::move(pc)); } // Sort props by category order from Prop_categories, preserving internal order diff --git a/code/scpui/RocketLuaSystemInterface.cpp b/code/scpui/RocketLuaSystemInterface.cpp index 73e08c612c2..8c12b665e3b 100644 --- a/code/scpui/RocketLuaSystemInterface.cpp +++ b/code/scpui/RocketLuaSystemInterface.cpp @@ -66,7 +66,7 @@ void RocketLuaSystemInterface::PrepareFunction(lua_State* L, int funcIdx, Rocket static_cast(context->GetOwnerDocument()->GetUserValue(LuaEnvironmentIdentifier)); if (envTable == nullptr) { - auto reference = std::unique_ptr(new LuaTableReference(createEnvironment(L))); + auto reference = std::make_unique(createEnvironment(L)); envTable = reference.get(); context->GetOwnerDocument()->PutUserValue(LuaEnvironmentIdentifier, std::move(reference)); diff --git a/code/scpui/rocket_ui.cpp b/code/scpui/rocket_ui.cpp index f9c26b70e30..f39a8b23220 100644 --- a/code/scpui/rocket_ui.cpp +++ b/code/scpui/rocket_ui.cpp @@ -678,7 +678,7 @@ void reloadContext(Rocket::Core::Context* context) status.visible = doc->IsVisible(); status.focus = doc->GetFocusLeafNode() != nullptr; - documents.push_back(status); + documents.push_back(std::move(status)); } Factory::ClearStyleSheetCache(); diff --git a/code/scripting/ade.cpp b/code/scripting/ade.cpp index b4adbca0e13..74e94fc81e1 100644 --- a/code/scripting/ade.cpp +++ b/code/scripting/ade.cpp @@ -582,7 +582,7 @@ std::unique_ptr ade_table_entry::ToDocumentationElement( overloadArgList.simple.assign(overload); } - obj->overloads.push_back(overloadArgList); + obj->overloads.push_back(std::move(overloadArgList)); } if (ReturnDescription != nullptr) { diff --git a/code/scripting/api/libs/async.cpp b/code/scripting/api/libs/async.cpp index e7bf471dfa6..456a581e27c 100644 --- a/code/scripting/api/libs/async.cpp +++ b/code/scripting/api/libs/async.cpp @@ -332,7 +332,7 @@ ADE_FUNC(yield, explicit yield_resolve_context(executor::Executor* executor) : m_exec(executor) {} void setResolver(Resolver resolver) override { - m_exec->post([resolver]() { + m_exec->post([resolver = std::move(resolver)]() { resolver(false, luacpp::LuaValueList()); return executor::Executor::CallbackResult::Done; }); diff --git a/code/scripting/api/libs/mission.cpp b/code/scripting/api/libs/mission.cpp index 395c254fda6..d43f8c6644e 100644 --- a/code/scripting/api/libs/mission.cpp +++ b/code/scripting/api/libs/mission.cpp @@ -2893,7 +2893,7 @@ ADE_FUNC(addLuaEnum, dynamic_sexp_enum_list this_list; this_list.name = enum_name; - Dynamic_enums.push_back(this_list); + Dynamic_enums.push_back(std::move(this_list)); idx = get_dynamic_enum_position(enum_name); @@ -3156,7 +3156,7 @@ ADE_FUNC(waitAsync, { // Keep checking the time until the timestamp is elapsed auto self = shared_from_this(); - auto cb = [this, self, resolver]( + auto cb = [this, self, resolver = std::move(resolver)]( executor::IExecutionContext::State contextState) { if (contextState == executor::IExecutionContext::State::Invalid) { mprintf(("waitAsync: Context is invalid, possibly due to a game state change (current state is %s). Aborting asynchronous context %d.\n", GS_state_text[gameseq_get_state()], m_unique_id)); diff --git a/code/scripting/api/libs/options.cpp b/code/scripting/api/libs/options.cpp index ed242d5ab62..cdf60049c0d 100644 --- a/code/scripting/api/libs/options.cpp +++ b/code/scripting/api/libs/options.cpp @@ -134,7 +134,7 @@ ADE_FUNC(writeIPAddressTable, l_Options, "table", "Saves the table to the multip // then carry on try { SCP_string ip = item.second.getValue(); - list.push_back(ip); + list.push_back(std::move(ip)); } catch (const luacpp::LuaException& /*e*/) { // We were likely fed a userdata that was not an string. // Since we can't actually tell whether that's the case before we try to get the value, and the diff --git a/code/scripting/api/objs/executor.cpp b/code/scripting/api/objs/executor.cpp index 3040b41ab57..8e26d974508 100644 --- a/code/scripting/api/objs/executor.cpp +++ b/code/scripting/api/objs/executor.cpp @@ -40,7 +40,7 @@ ADE_FUNC(schedule, } // Post the function onto the executor with a wrapper to convert the Lua value to the proper enum - executor->getExecutor()->post([L, func]() { + executor->getExecutor()->post([L, func = std::move(func)]() { const auto ret = func(L); if (ret.empty()) { diff --git a/code/scripting/api/objs/modelinstance.cpp b/code/scripting/api/objs/modelinstance.cpp index 49fa843a0a3..48a7fa49e5b 100644 --- a/code/scripting/api/objs/modelinstance.cpp +++ b/code/scripting/api/objs/modelinstance.cpp @@ -83,7 +83,7 @@ ADE_INDEXER(l_ModelInstanceTextures, "number/string IndexOrTextureFilename", "Ar if (ADE_SETTING_VAR) { if (pmi->texture_replace == nullptr) { - pmi->texture_replace = make_shared(); + pmi->texture_replace = std::make_shared(); } if (tdx != nullptr) { diff --git a/code/scripting/api/objs/promise.cpp b/code/scripting/api/objs/promise.cpp index 24a46229805..bfd03c1e67d 100644 --- a/code/scripting/api/objs/promise.cpp +++ b/code/scripting/api/objs/promise.cpp @@ -38,7 +38,7 @@ ADE_FUNC(continueWith, return ade_set_args(L, "o", - l_Promise.Set(promise->then([L, thenFunc](const luacpp::LuaValueList& val) { return thenFunc(L, val); }))); + l_Promise.Set(promise->then([L, thenFunc = std::move(thenFunc)](const luacpp::LuaValueList& val) { return thenFunc(L, val); }))); } ADE_FUNC(catch, @@ -64,7 +64,7 @@ ADE_FUNC(catch, return ADE_RETURN_NIL; } - return ade_set_args(L, "o", l_Promise.Set(promise->catchError([L, thenFunc](const luacpp::LuaValueList& val) { + return ade_set_args(L, "o", l_Promise.Set(promise->catchError([L, thenFunc = std::move(thenFunc)](const luacpp::LuaValueList& val) { return thenFunc(L, val); }))); } diff --git a/code/scripting/api/objs/rpc.cpp b/code/scripting/api/objs/rpc.cpp index b4ee1452524..69f5abbd598 100644 --- a/code/scripting/api/objs/rpc.cpp +++ b/code/scripting/api/objs/rpc.cpp @@ -132,7 +132,7 @@ ADE_FUNC(waitRPC, { // Keep checking the time until the timestamp is elapsed auto self = shared_from_this(); - auto cb = [this, self, resolver]( + auto cb = [this, self, resolver = std::move(resolver)]( executor::IExecutionContext::State contextState) { if (contextState == executor::IExecutionContext::State::Invalid) { mprintf(("waitRPC: Context is invalid, possibly due to a game state change (current state is %s). Aborting asynchronous context %d.\n", GS_state_text[gameseq_get_state()], m_unique_id)); diff --git a/code/scripting/doc_luastub.cpp b/code/scripting/doc_luastub.cpp index b74bc1d7852..d490abeeb3a 100644 --- a/code/scripting/doc_luastub.cpp +++ b/code/scripting/doc_luastub.cpp @@ -97,7 +97,7 @@ SCP_string create_function_alias(const ade_type_info& type_info, SCP_string alia aliasName += "_" + std::to_string(dupCount++); } - Aliases.emplace_back(aliasName, alias); + Aliases.emplace_back(aliasName, std::move(alias)); return aliasName; } diff --git a/code/scripting/hook_conditions.cpp b/code/scripting/hook_conditions.cpp index 5035b1d2a51..1b62727430b 100644 --- a/code/scripting/hook_conditions.cpp +++ b/code/scripting/hook_conditions.cpp @@ -16,7 +16,7 @@ #define HOOK_CONDITIONS_END return build; \ }(); #define HOOK_CONDITION(conditionsClassName, conditionParseName, documentation, argument, argumentParse, argumentValid) \ - build.emplace(conditionParseName, ::make_unique().argument), decltype(argumentParse(std::declval()))>> \ (documentation, &conditionsClassName::argument, argumentParse, argumentValid)) @@ -36,7 +36,7 @@ class ParseableConditionImpl : public ParseableCondition { template friend class EvaluatableConditionImpl; public: std::unique_ptr parse(const SCP_string& input) const override { - return ::make_unique>(*this, input); + return std::make_unique>(*this, input); } ParseableConditionImpl(SCP_string documentation_, const operating_t conditions_t::* object_, std::function cache_, std::function evaluate_) : diff --git a/code/scripting/hook_conditions.h b/code/scripting/hook_conditions.h index abbd7101ad6..6f4e71dbc39 100644 --- a/code/scripting/hook_conditions.h +++ b/code/scripting/hook_conditions.h @@ -27,7 +27,7 @@ class ParseableCondition { SCP_string documentation; virtual std::unique_ptr parse(const SCP_string& /*input*/) const { - return make_unique(); + return std::make_unique(); }; ParseableCondition() : documentation("Invalid Condition. Will never evaluate.") { } diff --git a/code/scripting/lua.cpp b/code/scripting/lua.cpp index db5e5cfc6db..8d19b38b4ac 100644 --- a/code/scripting/lua.cpp +++ b/code/scripting/lua.cpp @@ -314,7 +314,7 @@ void script_state::OutputLuaDocumentation(ScriptingDocumentation& doc, e.name = Enumerations[i].name; e.value = Enumerations[i].def; - doc.enumerations.push_back(e); + doc.enumerations.push_back(std::move(e)); } auto& optionsList = options::OptionsManager::instance()->getOptions(); @@ -326,7 +326,7 @@ void script_state::OutputLuaDocumentation(ScriptingDocumentation& doc, o.description = thisOpt->getDescription(); o.key = thisOpt->getConfigKey(); - doc.options.push_back(o); + doc.options.push_back(std::move(o)); } } diff --git a/code/scripting/lua/LuaThread.cpp b/code/scripting/lua/LuaThread.cpp index d8ae8adb5e9..48eb14b7979 100644 --- a/code/scripting/lua/LuaThread.cpp +++ b/code/scripting/lua/LuaThread.cpp @@ -19,9 +19,9 @@ LuaThread LuaThread::create(lua_State* L, const LuaFunction& func) //Usually we'd want to do this when the childs references are GC'd, but creating tables on child threads from C causes tests to fail for some reason. auto threadRef = std::weak_ptr(thread.getReference()); //These must be pointers to weak pointers, as we cannot know the weak pointers before creating the lambda. - auto delFuncRef = make_shared>(); - auto delTabRef = make_shared>(); - auto delUserdataRef = make_shared>(); + auto delFuncRef = std::make_shared>(); + auto delTabRef = std::make_shared>(); + auto delUserdataRef = std::make_shared>(); int stack = lua_gettop(L); diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 94e953b2245..a706bd8e663 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -2623,7 +2623,7 @@ particle::ParticleEffectHandle create_ship_legacy_particle_effect(LegacyShipPart break; } - auto velocity_volume = make_shared(normal_variance, 1.f, true); + auto velocity_volume = std::make_shared(normal_variance, 1.f, true); if (variance_curve) { velocity_volume->m_modular_curves.add_curve("Host Radius", particle::LegacyAACuboidVolume::VolumeModularCurveOutput::VARIANCE, *variance_curve); } @@ -5080,7 +5080,7 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool particle::ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(1.f), //Velocity Inherit true, //Velocity Inherit absolute? - make_unique(variance, 1.f, true), //Velocity volume + std::make_unique(variance, 1.f, true), //Velocity volume ::util::UniformFloatRange(0.75f, 1.25f), //Velocity volume multiplier particle::ParticleEffect::VelocityScaling::NONE, //Velocity directional scaling std::nullopt, //Orientation-based velocity @@ -6939,7 +6939,7 @@ void ship_add_exited_ship( ship *sp, Ship::Exit_Flags reason ) if (ship_it != Ship_registry_map.end()) Ship_registry[ship_it->second].exited_index = static_cast(Ships_exited.size()); - Ships_exited.push_back(entry); + Ships_exited.push_back(std::move(entry)); } /** @@ -7434,7 +7434,7 @@ void ship::apply_replacement_textures(const SCP_vector &replace polymodel_instance* pmi = model_get_instance(model_instance_num); - pmi->texture_replace = make_shared(); + pmi->texture_replace = std::make_shared(); auto pm = model_get(Ship_info[ship_info_index].model_num); @@ -8675,7 +8675,7 @@ void ship_init_cockpit_displays(ship *shipp) } // ship's cockpit texture replacements haven't been setup yet, so do it. - Player_cockpit_textures = make_shared(); + Player_cockpit_textures = std::make_shared(); for ( auto& display : sip->displays ) { ship_add_cockpit_display(&display, cockpit_model_num); @@ -11864,7 +11864,7 @@ static void ship_model_change(int n, int ship_type) if ( !sip->replacement_textures.empty() ) { // clear and reset replacement textures because the new positions may be different - pmi->texture_replace = make_shared(); + pmi->texture_replace = std::make_shared(); auto& texture_replace_deref = *pmi->texture_replace; // now fill them in according to texture name @@ -21118,7 +21118,7 @@ void parse_armor_type() parse_string_flag_list(tat.flags, Armor_flags, Num_armor_flags); //Add it to global armor types - Armor_types.push_back(tat); + Armor_types.push_back(std::move(tat)); } void armor_parse_table(const char *filename) diff --git a/code/ship/shipfx.cpp b/code/ship/shipfx.cpp index d9465e76853..ef5b30531a0 100644 --- a/code/ship/shipfx.cpp +++ b/code/ship/shipfx.cpp @@ -542,7 +542,7 @@ void shipfx_warpin_start( object *objp ) auto params = scripting::hook_param_list(scripting::hook_param("Self", 'o', objp)); auto conditions = scripting::hooks::ShipSourceConditions{ shipp }; if (OnWarpInHook->isOverride(conditions, params)) { - OnWarpInHook->run(conditions, params); + OnWarpInHook->run(conditions, std::move(params)); return; } } @@ -562,7 +562,7 @@ void shipfx_warpin_start( object *objp ) { auto params = scripting::hook_param_list(scripting::hook_param("Self", 'o', objp)); auto conditions = scripting::hooks::ShipSourceConditions{ shipp }; - OnWarpInHook->run(conditions, params); + OnWarpInHook->run(conditions, std::move(params)); } } @@ -705,7 +705,7 @@ void shipfx_warpout_start( object *objp ) auto params = scripting::hook_param_list(scripting::hook_param("Self", 'o', objp)); auto conditions = scripting::hooks::ShipSourceConditions{ shipp }; if (OnWarpOutHook->isOverride(conditions, params)) { - OnWarpOutHook->run(conditions, params); + OnWarpOutHook->run(conditions, std::move(params)); return; } } @@ -747,7 +747,7 @@ void shipfx_warpout_start( object *objp ) if (OnWarpOutHook->isActive()) { auto params = scripting::hook_param_list(scripting::hook_param("Self", 'o', objp)); auto conditions = scripting::hooks::ShipSourceConditions{ shipp }; - OnWarpOutHook->run(conditions, params); + OnWarpOutHook->run(conditions, std::move(params)); } } @@ -1097,7 +1097,7 @@ void shipfx_flash_create(object *objp, int model_num, vec3d *gun_pos, vec3d *gun // spawn particle effect auto particleSource = particle::ParticleManager::get()->createSource(Weapon_info[weapon_info_index].muzzle_effect); //This should probably end up attached to the subobject, not the object, but it's not that much of a problem since primaries / secondaries rarely move. - particleSource->setHost(make_unique(objp, *gun_pos, gunOrient, true)); + particleSource->setHost(std::make_unique(objp, *gun_pos, gunOrient, true)); auto *weapon_objp = &Objects[weapon_objnum]; auto *wp = &Weapons[weapon_objp->instance]; diff --git a/code/ship/shiphit.cpp b/code/ship/shiphit.cpp index 754f9881cad..541a3b9c272 100755 --- a/code/ship/shiphit.cpp +++ b/code/ship/shiphit.cpp @@ -181,7 +181,7 @@ void do_subobj_destroyed_stuff( ship *ship_p, ship_subsys *subsys, const vec3d* vm_vec_normalize(&normalized_center_to_subsys); // spawn particle effect auto source = particle::ParticleManager::get()->createSource(death_effect); - source->setHost(make_unique(ship_objp, subsys_local_pos, vmd_identity_matrix)); + source->setHost(std::make_unique(ship_objp, subsys_local_pos, vmd_identity_matrix)); source->setTriggerRadius(psub->radius); source->setNormal(normalized_center_to_subsys); source->finishCreation(); @@ -1917,7 +1917,7 @@ void ship_hit_kill(object *ship_objp, object *other_obj, const vec3d *hitpos, fl hitpos != nullptr)); if (scripting::hooks::OnShipDeath->isOverride(scripting::hooks::ShipDeathConditions{ sp }, onDeathParamList)) { - scripting::hooks::OnShipDeath->run(scripting::hooks::ShipDeathConditions{ sp }, onDeathParamList); + scripting::hooks::OnShipDeath->run(scripting::hooks::ShipDeathConditions{ sp }, std::move(onDeathParamList)); return; } } diff --git a/code/sound/ds.cpp b/code/sound/ds.cpp index 526dc686856..173138b567f 100644 --- a/code/sound/ds.cpp +++ b/code/sound/ds.cpp @@ -1751,7 +1751,7 @@ int ds_eax_get_prop(EFXREVERBPROPERTIES **props, const char *name, const char *t n_prop.iDecayHFLimit = AL_TRUE; } - EFX_presets.push_back( n_prop ); + EFX_presets.push_back( std::move(n_prop) ); *props = &EFX_presets[id]; } diff --git a/code/sound/openal.cpp b/code/sound/openal.cpp index 6d91751d881..c40565fe0c4 100644 --- a/code/sound/openal.cpp +++ b/code/sound/openal.cpp @@ -300,7 +300,7 @@ static void find_playback_device(OpenALInformation* info) new_device.type = OAL_DEVICE_DEFAULT; } - PlaybackDevices.push_back( new_device ); + PlaybackDevices.push_back( std::move(new_device) ); } if ( PlaybackDevices.empty() ) { @@ -412,7 +412,7 @@ static void find_capture_device(OpenALInformation* info) new_device.type = OAL_DEVICE_DEFAULT; } - CaptureDevices.push_back( new_device ); + CaptureDevices.push_back( std::move(new_device) ); } if ( CaptureDevices.empty() ) { diff --git a/code/species_defs/species_defs.cpp b/code/species_defs/species_defs.cpp index 0681f047cc2..90ea9e33a88 100644 --- a/code/species_defs/species_defs.cpp +++ b/code/species_defs/species_defs.cpp @@ -366,7 +366,7 @@ void parse_species_tbl(const char *filename) // don't add new entry if this is just a modified one if (!no_create) - Species_info.push_back(new_species); + Species_info.push_back(std::move(new_species)); } required_string("#END"); diff --git a/code/starfield/starfield.cpp b/code/starfield/starfield.cpp index a32ab336000..c1318027330 100644 --- a/code/starfield/starfield.cpp +++ b/code/starfield/starfield.cpp @@ -455,7 +455,7 @@ void parse_startbl(const char *filename) } } else { - Starfield_bitmaps.push_back(sbm); + Starfield_bitmaps.push_back(std::move(sbm)); } } @@ -564,7 +564,7 @@ void parse_startbl(const char *filename) Warning(LOCATION, "Sun bitmap '%s' listed more than once!! Only using the first entry!", sbm.filename); } else { - Sun_bitmaps.push_back(sbm); + Sun_bitmaps.push_back(std::move(sbm)); } } @@ -594,7 +594,7 @@ void parse_startbl(const char *filename) } } if (count == MAX_MOTION_DEBRIS_BITMAPS) { - Motion_debris_info.push_back(this_debris); + Motion_debris_info.push_back(std::move(this_debris)); } else { error_display(0, "Not enough bitmaps defined for motion debris '%s'. Skipping!\n", this_debris.name.c_str()); } @@ -624,7 +624,7 @@ void parse_startbl(const char *filename) } } if (count == MAX_MOTION_DEBRIS_BITMAPS) { - Motion_debris_info.push_back(this_debris); + Motion_debris_info.push_back(std::move(this_debris)); } else { error_display(0, "Not enough bitmaps defined for motion debris '%s'. Skipping!\n", this_debris.name.c_str()); } @@ -654,7 +654,7 @@ void parse_startbl(const char *filename) this_debris.bitmaps[i].name[0] = '\0'; } - Motion_debris_info.push_back(this_debris); + Motion_debris_info.push_back(std::move(this_debris)); check = static_cast(Motion_debris_info.size()) - 1; } diff --git a/code/starfield/supernova.cpp b/code/starfield/supernova.cpp index e822e7b9ba6..87a25508d58 100644 --- a/code/starfield/supernova.cpp +++ b/code/starfield/supernova.cpp @@ -54,7 +54,7 @@ static particle::ParticleEffectHandle supernova_init_particle() { particle::ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(1.f), //Velocity Inherit false, //Velocity Inherit absolute? - make_unique(0.75f, 1.f, true), //Velocity volume + std::make_unique(0.75f, 1.f, true), //Velocity volume ::util::UniformFloatRange(25.f, 50.f), //Velocity volume multiplier particle::ParticleEffect::VelocityScaling::NONE, //Velocity directional scaling std::nullopt, //Orientation-based velocity @@ -174,11 +174,11 @@ void supernova_do_particles() vm_vec_add2(&b, &Player_obj->pos); auto source = particle::ParticleManager::get()->createSource(Supernova_particle_effect); - source->setHost(make_unique(a, orient, vel)); + source->setHost(std::make_unique(a, orient, vel)); source->finishCreation(); auto source2 = particle::ParticleManager::get()->createSource(Supernova_particle_effect); - source2->setHost(make_unique(b, orient, vel)); + source2->setHost(std::make_unique(b, orient, vel)); source2->finishCreation(); } } diff --git a/code/stats/medals.cpp b/code/stats/medals.cpp index d05987323ff..69b33353b8f 100644 --- a/code/stats/medals.cpp +++ b/code/stats/medals.cpp @@ -315,9 +315,9 @@ void parse_medals_table(const char* filename) continue; } - Medals.push_back(medal_t); + Medals.push_back(std::move(medal_t)); medal_p = &Medals[Medals.size() - 1]; - Medal_display_info.push_back(display_t); + Medal_display_info.push_back(std::move(display_t)); display_p = &Medal_display_info[Medal_display_info.size() - 1]; } diff --git a/code/stats/scoring.cpp b/code/stats/scoring.cpp index 6b8542da3d6..83576ad57e9 100644 --- a/code/stats/scoring.cpp +++ b/code/stats/scoring.cpp @@ -145,7 +145,7 @@ void parse_rank_table(const char* filename) } continue; } - Ranks.push_back(rank_t); + Ranks.push_back(std::move(rank_t)); rank_p = &Ranks.back(); } diff --git a/code/tracing/FrameProfiler.cpp b/code/tracing/FrameProfiler.cpp index 5368d175ed0..240e3d8c974 100644 --- a/code/tracing/FrameProfiler.cpp +++ b/code/tracing/FrameProfiler.cpp @@ -53,7 +53,7 @@ void process_begin(SCP_vector& samples, const trace_event& evt) new_sample.parent = parent; new_sample.num_parents = (parent >= 0) ? 1 : 0; - samples.push_back(new_sample); + samples.push_back(std::move(new_sample)); } void process_end(SCP_vector& samples, const trace_event& evt) { @@ -227,7 +227,7 @@ void FrameProfiler::store_profile_in_history(SCP_string& name, new_history.valid = true; new_history.avg_micro_sec = new_history.min_micro_sec = new_history.max_micro_sec = time; - history.push_back(new_history); + history.push_back(std::move(new_history)); } void FrameProfiler::dump_output(SCP_stringstream& out, uint64_t /*start_profile_time*/, diff --git a/code/weapon/beam.cpp b/code/weapon/beam.cpp index d6fdbc1b748..55c25bbb200 100644 --- a/code/weapon/beam.cpp +++ b/code/weapon/beam.cpp @@ -293,7 +293,7 @@ static void beam_set_state(weapon_info* wip, beam* bm, WeaponState state) if ((map_entry != wip->state_effects.end()) && map_entry->second.isValid()) { auto source = particle::ParticleManager::get()->createSource(map_entry->second); - source->setHost(make_unique(&Objects[bm->objnum])); + source->setHost(std::make_unique(&Objects[bm->objnum])); source->finishCreation(); } } diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index 839bf7966d5..b04384f4dea 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -2363,7 +2363,7 @@ int parse_weapon(int subtype, bool replace, const char *filename) ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(1.f), //Velocity Inherit false, //Velocity Inherit absolute? - make_unique(variance, 1.f, true), //Velocity volume + std::make_unique(variance, 1.f, true), //Velocity volume ::util::UniformFloatRange(MIN(0.5f * velocity, 2.0f * velocity), MAX(0.5f * velocity, 2.0f * velocity)), //Velocity volume multiplier ParticleEffect::VelocityScaling::NONE, //Velocity directional scaling std::nullopt, //Orientation-based velocity @@ -2395,7 +2395,7 @@ int parse_weapon(int subtype, bool replace, const char *filename) ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(1.f), //Velocity Inherit false, //Velocity Inherit absolute? - make_unique(variance, 1.f, true), //Velocity volume + std::make_unique(variance, 1.f, true), //Velocity volume ::util::UniformFloatRange(MIN(0.5f * back_velocity, 2.0f * back_velocity), MAX(0.5f * back_velocity, 2.0f * back_velocity)), //Velocity volume multiplier ParticleEffect::VelocityScaling::NONE, //Velocity directional scaling std::nullopt, //Orientation-based velocity @@ -3268,7 +3268,7 @@ int parse_weapon(int subtype, bool replace, const char *filename) ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(0.f), //Velocity Inherit false, //Velocity Inherit absolute? - make_unique(1.f, 1.f, 1.f), //Velocity volume + std::make_unique(1.f, 1.f, 1.f), //Velocity volume ::util::UniformFloatRange(baseVelocity * variance), //Velocity volume multiplier ParticleEffect::VelocityScaling::NONE, //Velocity directional scaling ::util::UniformFloatRange(MIN(baseVelocity, 2.0f * baseVelocity), MAX(baseVelocity, 2.0f * baseVelocity)), //Orientation-based velocity @@ -3299,7 +3299,7 @@ int parse_weapon(int subtype, bool replace, const char *filename) ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(0.f), //Velocity Inherit false, //Velocity Inherit absolute? - make_unique(1.f, 1.f, 1.f), //Velocity volume + std::make_unique(1.f, 1.f, 1.f), //Velocity volume ::util::UniformFloatRange(backVelocity * variance), //Velocity volume multiplier ParticleEffect::VelocityScaling::NONE, //Velocity directional scaling ::util::UniformFloatRange(MIN(backVelocity, 2.0f * backVelocity), MAX(backVelocity, 2.0f * backVelocity)), //Orientation-based velocity @@ -6130,7 +6130,7 @@ static void weapon_set_state(weapon_info* wip, weapon* wp, WeaponState state) if ((map_entry != wip->state_effects.end()) && map_entry->second.isValid()) { auto source = particle::ParticleManager::get()->createSource(map_entry->second); - source->setHost(make_unique(&Objects[wp->objnum], vmd_zero_vector)); + source->setHost(std::make_unique(&Objects[wp->objnum], vmd_zero_vector)); source->finishCreation(); } @@ -8809,7 +8809,7 @@ void shield_impact_explosion(const vec3d& hitpos, const vec3d& hitdir, const obj vm_vec_unrotate(&hitdir_global, &hitdir, &objp->orient); auto particleSource = particle::ParticleManager::get()->createSource(handle); - particleSource->setHost(make_unique(objp, hitpos, localorient)); + particleSource->setHost(std::make_unique(objp, hitpos, localorient)); particleSource->setNormal(hitdir_global); particleSource->setTriggerRadius(radius); particleSource->setTriggerVelocity(vm_vec_mag_quick(&weapon_objp->phys_info.vel)); diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index d3c937f6d1e..b55e77f5a76 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -4123,7 +4123,7 @@ void game_do_full_frame(DEBUG_TIMER_SIG const vec3d* offset = nullptr, const mat if (fov_override) g3_set_fov(*fov_override); - scripting::hooks::OnHudDraw->run(scripting::hooks::ObjectDrawConditions{ Viewer_obj }, scripting_param_list); + scripting::hooks::OnHudDraw->run(scripting::hooks::ObjectDrawConditions{ Viewer_obj }, std::move(scripting_param_list)); } } @@ -5783,7 +5783,7 @@ void game_enter_state( int old_state, int new_state ) if(scripting::hooks::OnStateStart->isActive()) { if (scripting::hooks::OnStateStart->isOverride(script_param_list)) { - scripting::hooks::OnStateStart->run(script_param_list); + scripting::hooks::OnStateStart->run(std::move(script_param_list)); return; } } From fdef3eaa388182e6074819eb7b2284b055d0738b Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Thu, 7 May 2026 07:03:27 -0500 Subject: [PATCH 23/65] Share texture type names from model.h (#7395) * share texture type names from model.h * signed unsigned mismatch * clang * stop yur whining --- code/model/model.h | 20 +++++ code/model/modelread.cpp | 20 ++--- .../ShipTextureReplacementDialogModel.cpp | 81 ++++++++----------- 3 files changed, 64 insertions(+), 57 deletions(-) diff --git a/code/model/model.h b/code/model/model.h index e9bff59a7f6..3cff8481624 100644 --- a/code/model/model.h +++ b/code/model/model.h @@ -165,6 +165,26 @@ struct submodel_instance //Used by scripting - if you change this, do a search //to update switch() statement in lua.cpp +inline const SCP_map MODEL_TEXTURE_SUFFIXES = { + { TM_GLOW_TYPE, "-glow" }, + { TM_SPECULAR_TYPE, "-shine" }, + { TM_NORMAL_TYPE, "-normal" }, + { TM_HEIGHT_TYPE, "-height" }, + { TM_MISC_TYPE, "-misc" }, + { TM_SPEC_GLOSS_TYPE, "-reflect" }, + { TM_AMBIENT_TYPE, "-ao" } +}; + +inline const SCP_string MODEL_TEXTURE_SUFFIX_TRANS = "-trans"; // -trans is a special case as other suffixes can be appended to it + +inline const SCP_string& model_texture_longest_suffix() { + return std::max_element(MODEL_TEXTURE_SUFFIXES.begin(), + MODEL_TEXTURE_SUFFIXES.end(), + [](const std::pair& left, const std::pair& right) { + return left.second.size() < right.second.size(); + })->second; +} + #define MAX_REPLACEMENT_TEXTURES MAX_MODEL_TEXTURES * TM_NUM_TYPES // Goober5000 - since we need something < 0 diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index aa465153070..44015e877d0 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -2584,10 +2584,10 @@ modelread_status read_model_file_no_subsys(polymodel * pm, const char* filename, { char tmp_name[127]; cfread_string_len(tmp_name,127,fp); - constexpr int max_buffer_size = MAX_FILENAME_LEN - 8; // leave room for the longest suffix, "-reflect" + const auto max_buffer_size = static_cast(MAX_FILENAME_LEN) - model_texture_longest_suffix().size(); if (strlen(tmp_name) >= max_buffer_size) { - Warning(LOCATION, "Model '%s', texture '%s' filename is too long! Truncating to %d characters.", pm->filename, tmp_name, max_buffer_size - 1); + Warning(LOCATION, "Model '%s', texture '%s' filename is too long! Truncating to %d characters.", pm->filename, tmp_name, static_cast(max_buffer_size - 1)); tmp_name[max_buffer_size - 1] = '\0'; } model_load_texture(pm, i, tmp_name); @@ -3098,7 +3098,7 @@ void model_load_texture(polymodel *pm, int i, const char *file) else { // check if we should be transparent, include "-trans" but make sure to skip anything that might be "-transport" - if ( (strstr(tmp_name, "-trans") && !strstr(tmp_name, "-transpo")) || strstr(tmp_name, "shockwave") || !strcmp(tmp_name, "nameplate") ) { + if ((strstr(tmp_name, MODEL_TEXTURE_SUFFIX_TRANS.c_str()) && !strstr(tmp_name, "-transpo")) || strstr(tmp_name, "shockwave") || !strcmp(tmp_name, "nameplate")) { tmap->is_transparent = true; } @@ -3123,7 +3123,7 @@ void model_load_texture(polymodel *pm, int i, const char *file) else { strcpy_s(tmp_name, file); - strcat_s(tmp_name, "-glow" ); + strcat_s(tmp_name, MODEL_TEXTURE_SUFFIXES.at(TM_GLOW_TYPE).c_str()); strlwr(tmp_name); tglow->LoadTexture(tmp_name, pm->filename); @@ -3142,14 +3142,14 @@ void model_load_texture(polymodel *pm, int i, const char *file) { // look for reflectance map strcpy_s(tmp_name, file); - strcat_s(tmp_name, "-reflect"); + strcat_s(tmp_name, MODEL_TEXTURE_SUFFIXES.at(TM_SPEC_GLOSS_TYPE).c_str()); strlwr(tmp_name); tspecgloss->LoadTexture(tmp_name, pm->filename); // look for a legacy shine map as well strcpy_s(tmp_name, file); - strcat_s(tmp_name, "-shine"); + strcat_s(tmp_name, MODEL_TEXTURE_SUFFIXES.at(TM_SPECULAR_TYPE).c_str()); strlwr(tmp_name); tspec->LoadTexture(tmp_name, pm->filename); @@ -3163,7 +3163,7 @@ void model_load_texture(polymodel *pm, int i, const char *file) tnorm->clear(); } else { strcpy_s(tmp_name, file); - strcat_s(tmp_name, "-normal"); + strcat_s(tmp_name, MODEL_TEXTURE_SUFFIXES.at(TM_NORMAL_TYPE).c_str()); strlwr(tmp_name); tnorm->LoadTexture(tmp_name, pm->filename); @@ -3175,7 +3175,7 @@ void model_load_texture(polymodel *pm, int i, const char *file) theight->clear(); } else { strcpy_s(tmp_name, file); - strcat_s(tmp_name, "-height"); + strcat_s(tmp_name, MODEL_TEXTURE_SUFFIXES.at(TM_HEIGHT_TYPE).c_str()); strlwr(tmp_name); theight->LoadTexture(tmp_name, pm->filename); @@ -3185,7 +3185,7 @@ void model_load_texture(polymodel *pm, int i, const char *file) texture_info *tambient = &tmap->textures[TM_AMBIENT_TYPE]; strcpy_s(tmp_name, file); - strcat_s(tmp_name, "-ao"); + strcat_s(tmp_name, MODEL_TEXTURE_SUFFIXES.at(TM_AMBIENT_TYPE).c_str()); strlwr(tmp_name); tambient->LoadTexture(tmp_name, pm->filename); @@ -3194,7 +3194,7 @@ void model_load_texture(polymodel *pm, int i, const char *file) texture_info *tmisc = &tmap->textures[TM_MISC_TYPE]; strcpy_s(tmp_name, file); - strcat_s(tmp_name, "-misc"); + strcat_s(tmp_name, MODEL_TEXTURE_SUFFIXES.at(TM_MISC_TYPE).c_str()); strlwr(tmp_name); tmisc->LoadTexture(tmp_name, pm->filename); diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp index 79f90eefdc7..92a36b7814b 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp @@ -1,11 +1,29 @@ #include "ShipTextureReplacementDialogModel.h" #include "mission/object.h" - -// Sub-texture type suffixes, mirroring the strcat_s calls in modelread.cpp. -// Used both when detecting sub-texture slots in initSubTypes and when parsing -// new_texture strings on dialog reload. -static const SCP_string SUBTEXTURE_SUFFIXES[] = { "misc", "shine", "glow", "normal", "height", "ao", "reflect" }; +#include "model/model.h" + +namespace { +const SCP_vector& get_replaceable_texture_types() +{ + static const SCP_vector types = []() { + SCP_vector out; + out.reserve(MODEL_TEXTURE_SUFFIXES.size()); + for (const auto& suffix : MODEL_TEXTURE_SUFFIXES) { + out.emplace_back(suffix.second.substr(1)); // strip leading '-' + } + return out; + }(); + return types; +} + +bool is_known_subtexture_type(const SCP_string& type) +{ + return std::any_of(get_replaceable_texture_types().begin(), + get_replaceable_texture_types().end(), + [&type](const SCP_string& knownType) { return lcase_equal(type, knownType); }); +} +} namespace fso { namespace fred { @@ -122,7 +140,7 @@ namespace fso { // Only treat the suffix as a type if it's a known sub-texture type. // Texture names themselves can contain hyphens (e.g. "fighter01-01a"), // so we must not blindly strip the last segment. - for (const auto& kt : SUBTEXTURE_SUFFIXES) { + for (const auto& kt : get_replaceable_texture_types()) { if (lcase_equal(possibleType, kt)) { type = possibleType; newText = newText.substr(0, npos); @@ -188,37 +206,12 @@ namespace fso { } void ShipTextureReplacementDialogModel::initSubTypes(polymodel* model, int MapNum) { - subTypesAvailable[MapNum].insert(std::pair("misc", false)); - subTypesAvailable[MapNum].insert(std::pair("shine", false)); - subTypesAvailable[MapNum].insert(std::pair("glow", false)); - subTypesAvailable[MapNum].insert(std::pair("normal", false)); - subTypesAvailable[MapNum].insert(std::pair("height", false)); - subTypesAvailable[MapNum].insert(std::pair("ao", false)); - subTypesAvailable[MapNum].insert(std::pair("reflect", false)); - - currentTextures[MapNum].insert(std::pair("misc", "")); - currentTextures[MapNum].insert(std::pair("shine", "")); - currentTextures[MapNum].insert(std::pair("glow", "")); - currentTextures[MapNum].insert(std::pair("normal", "")); - currentTextures[MapNum].insert(std::pair("height", "")); - currentTextures[MapNum].insert(std::pair("ao", "")); - currentTextures[MapNum].insert(std::pair("reflect", "")); - - replaceMap[MapNum].insert(std::pair("misc", false)); - replaceMap[MapNum].insert(std::pair("shine", false)); - replaceMap[MapNum].insert(std::pair("glow", false)); - replaceMap[MapNum].insert(std::pair("normal", false)); - replaceMap[MapNum].insert(std::pair("height", false)); - replaceMap[MapNum].insert(std::pair("ao", false)); - replaceMap[MapNum].insert(std::pair("reflect", false)); - - inheritMap[MapNum].insert(std::pair("misc", true)); - inheritMap[MapNum].insert(std::pair("shine", true)); - inheritMap[MapNum].insert(std::pair("glow", true)); - inheritMap[MapNum].insert(std::pair("normal", true)); - inheritMap[MapNum].insert(std::pair("height", true)); - inheritMap[MapNum].insert(std::pair("ao", true)); - inheritMap[MapNum].insert(std::pair("reflect", true)); + for (const auto& type : get_replaceable_texture_types()) { + subTypesAvailable[MapNum].insert(std::pair(type, false)); + currentTextures[MapNum].insert(std::pair(type, "")); + replaceMap[MapNum].insert(std::pair(type, false)); + inheritMap[MapNum].insert(std::pair(type, true)); + } char subMap[MAX_FILENAME_LEN]; //init saftly, probly not necessary for (int j = 1; j < TM_NUM_TYPES; j++) { @@ -240,18 +233,12 @@ namespace fso { continue; } if (!type.empty()) { - if (type == "trans") { + if (lcase_equal(type, MODEL_TEXTURE_SUFFIX_TRANS.substr(1))) { // transparency map, not a replaceable subtype } else { - bool known = false; - for (const auto& kt : SUBTEXTURE_SUFFIXES) { - if (lcase_equal(type, kt)) { - subTypesAvailable[MapNum][kt] = true; - known = true; - break; - } - } - if (!known) { + if (is_known_subtexture_type(type)) { + subTypesAvailable[MapNum][type] = true; + } else { error_display(1, "Invalid Map type %s. Check your model's texture names or get a programmer", type.c_str()); } } From f4c76dcee1e2f5fa191b64aa004580d44890473a Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Thu, 7 May 2026 23:22:58 -0400 Subject: [PATCH 24/65] fix FRED2 MFC linkage when building qtFRED PR #7334 caused an unintended side effect when building both FRED and QtFRED. With QtFRED enabled, FRED2 sources were compiled with _AFXDLL defined while still linking against static MFC, causing a crash on startup. To fix this, keep the conditionals in sync between cmake/toolchain-msvc.cmake and fred2/CMakeLists.txt. Also, do not explicitly add static MFC libraries if we are using dynamic linking. --- cmake/toolchain-msvc.cmake | 1 + fred2/CMakeLists.txt | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/cmake/toolchain-msvc.cmake b/cmake/toolchain-msvc.cmake index 30195ac81b5..492405b79ca 100644 --- a/cmake/toolchain-msvc.cmake +++ b/cmake/toolchain-msvc.cmake @@ -71,6 +71,7 @@ if (MSVC_RELEASE_DEBUGGING) endif() endif() +# This should be kept in sync with the corresponding IF() in fred2/CMakeLists.txt IF(MSVC_USE_RUNTIME_DLL OR FSO_BUILD_QTFRED) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$>:Debug>DLL") add_compile_definitions(_AFXDLL) diff --git a/fred2/CMakeLists.txt b/fred2/CMakeLists.txt index 8d974199f53..67228c3edb4 100644 --- a/fred2/CMakeLists.txt +++ b/fred2/CMakeLists.txt @@ -164,11 +164,12 @@ set_source_files_properties(fred.rc PROPERTIES LANGUAGE RC ) -IF(MSVC_USE_RUNTIME_DLL) +# This should be kept in sync with the corresponding IF() in cmake/toolchain-msvc.cmake +IF(MSVC_USE_RUNTIME_DLL OR FSO_BUILD_QTFRED) set(CMAKE_MFC_FLAG 2) -ELSE(MSVC_USE_RUNTIME_DLL) +ELSE() set(CMAKE_MFC_FLAG 1) -ENDIF(MSVC_USE_RUNTIME_DLL) +ENDIF() IF(MSVC60) link_directories(${STLPORT_INCLUDE_LIB_DIRS}) @@ -197,8 +198,15 @@ FIND_PACKAGE(OpenGL REQUIRED) target_link_libraries(FRED2 PUBLIC "${OPENGL_LIBRARY}") TARGET_LINK_LIBRARIES(FRED2 PUBLIC code) # This will also link all dependencies of code -TARGET_LINK_LIBRARIES(FRED2 PUBLIC "$<$>:nafxcwd.lib>") -TARGET_LINK_LIBRARIES(FRED2 PUBLIC "$<$:nafxcw.lib>") + +# When CMAKE_MFC_FLAG=2 (DLL MFC) MSBuild already links the DLL MFC stub libs (mfcs*.lib); +# explicitly adding the static MFC libs (nafxcw*) on top causes IsDerivedFrom to be resolved +# from the static MFC copy while CRuntimeClass structs are initialized in the DLL form, +# crashing CDocTemplate's ctor on startup. +IF(NOT (MSVC_USE_RUNTIME_DLL OR FSO_BUILD_QTFRED)) + TARGET_LINK_LIBRARIES(FRED2 PUBLIC "$<$>:nafxcwd.lib>") + TARGET_LINK_LIBRARIES(FRED2 PUBLIC "$<$:nafxcw.lib>") +ENDIF() target_link_libraries(FRED2 PUBLIC glad_wgl) From 98aac122130de3e067d4e52855cb6d26daa17294 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Fri, 8 May 2026 00:38:28 -0500 Subject: [PATCH 25/65] fix unpause issue (#7427) --- freespace2/freespace.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index b55e77f5a76..f8cb631b9e5 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -5433,7 +5433,7 @@ void game_leave_state( int old_state, int new_state ) common_select_close(); } - if (new_state != GS_STATE_CONTROL_CONFIG && new_state != GS_STATE_HUD_CONFIG) { + if (new_state != GS_STATE_CONTROL_CONFIG && new_state != GS_STATE_HUD_CONFIG && new_state != GS_STATE_INGAME_OPTIONS) { // unpause all sounds, since we could be headed back to the game // only unpause if we're in-mission; we could also be in the main hall if (Game_mode & GM_IN_MISSION) { From 4086db15d96fc01d0ddb38943ad6e1289391fb87 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Fri, 8 May 2026 10:43:40 +0200 Subject: [PATCH 26/65] Fix randomrange init slowdown (#7444) --- code/utils/RandomRange.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/utils/RandomRange.h b/code/utils/RandomRange.h index c574b5ce1ac..3bf3a2201a7 100644 --- a/code/utils/RandomRange.h +++ b/code/utils/RandomRange.h @@ -84,7 +84,7 @@ class RandomRange { public: template =1 || !std::is_convertible::value) && !std::is_same_v, RandomRange>, int>::type> explicit RandomRange(T&& distributionFirstParameter, Ts&&... distributionParameters) - : m_generator(std::random_device()()), m_distribution(distributionFirstParameter, distributionParameters...) + : m_generator(seeder()), m_distribution(distributionFirstParameter, distributionParameters...) { m_minValue = static_cast(m_distribution.min()); m_maxValue = static_cast(m_distribution.max()); @@ -98,7 +98,7 @@ class RandomRange { m_constant = true; } - RandomRange() : m_generator(std::random_device()()), m_distribution() + RandomRange() : m_generator(seeder()), m_distribution() { m_minValue = static_cast(0.0); m_maxValue = static_cast(0.0); From f02c01a81eb6d0ca5242bb7cbd6501d068f61382 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Fri, 8 May 2026 11:58:57 -0500 Subject: [PATCH 27/65] fix gauges not respecting table flags (#7426) --- code/hud/hudmessage.cpp | 12 ++++++++++++ code/hud/hudsquadmsg.cpp | 12 ++++++++++++ code/mission/missiontraining.cpp | 12 ++++++++++++ 3 files changed, 36 insertions(+) diff --git a/code/hud/hudmessage.cpp b/code/hud/hudmessage.cpp index 021897e02aa..582d44d6d1b 100644 --- a/code/hud/hudmessage.cpp +++ b/code/hud/hudmessage.cpp @@ -1342,6 +1342,18 @@ bool HudGaugeTalkingHead::canRender() const return false; } + if ((Viewer_mode & (VM_CHASE)) == 0 && only_render_in_chase_view) { + return false; + } + + if (render_for_cockpit_toggle > 0) { + if (!(Viewer_mode & VM_CHASE) && Cockpit_active && (render_for_cockpit_toggle == 2)) { + return false; + } else if (!Cockpit_active && (render_for_cockpit_toggle == 1)) { + return false; + } + } + if(pop_up) { if(!popUpActive()) { return false; diff --git a/code/hud/hudsquadmsg.cpp b/code/hud/hudsquadmsg.cpp index e0c67d80cc2..71369307009 100644 --- a/code/hud/hudsquadmsg.cpp +++ b/code/hud/hudsquadmsg.cpp @@ -2860,6 +2860,18 @@ bool HudGaugeSquadMessage::canRender() const return false; } + if ((Viewer_mode & (VM_CHASE)) == 0 && only_render_in_chase_view) { + return false; + } + + if (render_for_cockpit_toggle > 0) { + if (!(Viewer_mode & VM_CHASE) && Cockpit_active && (render_for_cockpit_toggle == 2)) { + return false; + } else if (!Cockpit_active && (render_for_cockpit_toggle == 1)) { + return false; + } + } + if (!( Player->flags & PLAYER_FLAGS_MSG_MODE )) { return false; } diff --git a/code/mission/missiontraining.cpp b/code/mission/missiontraining.cpp index 27af1b78c19..169dfc14f85 100644 --- a/code/mission/missiontraining.cpp +++ b/code/mission/missiontraining.cpp @@ -213,6 +213,18 @@ bool HudGaugeDirectives::canRender() const return false; } + if ((Viewer_mode & (VM_CHASE)) == 0 && only_render_in_chase_view) { + return false; + } + + if (render_for_cockpit_toggle > 0) { + if (!(Viewer_mode & VM_CHASE) && Cockpit_active && (render_for_cockpit_toggle == 2)) { + return false; + } else if (!Cockpit_active && (render_for_cockpit_toggle == 1)) { + return false; + } + } + if(pop_up) { if(!popUpActive()) { return false; From 9180ff364e93516030d0cefc14789ea2b55003d1 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 9 May 2026 09:50:03 -0500 Subject: [PATCH 28/65] icon closeup zoom and pos (#7398) --- code/missionui/missionscreencommon.cpp | 44 ++++++++++++++------------ code/missionui/missionscreencommon.h | 3 +- code/missionui/missionshipchoice.cpp | 8 ++--- code/missionui/missionweaponchoice.cpp | 4 +-- code/ship/ship.cpp | 24 ++++++++++++++ code/ship/ship.h | 3 ++ code/weapon/weapon.h | 4 +++ code/weapon/weapons.cpp | 20 ++++++++++++ 8 files changed, 83 insertions(+), 27 deletions(-) diff --git a/code/missionui/missionscreencommon.cpp b/code/missionui/missionscreencommon.cpp index 07766876504..a9a9923293c 100644 --- a/code/missionui/missionscreencommon.cpp +++ b/code/missionui/missionscreencommon.cpp @@ -1552,15 +1552,14 @@ int restore_wss_data(ubyte *data) return offset; } -void draw_model_icon(int model_id, uint64_t flags, float closeup_zoom, int x, int y, int w, int h, ship_info *sip, int resize_mode, const vec3d *closeup_pos) +void draw_model_icon(int model_id, uint64_t flags, int x, int y, int w, int h, ship_info* sip, weapon_info* wip, float zoom_multiplier, int resize_mode) { lighting_profiles::set_non_mission_profile non_mission_lighting_profile; matrix object_orient = IDENTITY_MATRIX; angles rot_angles = vmd_zero_angles; - float zoom = closeup_zoom * 2.5f; - if(sip == NULL) + if (sip == nullptr) { //Assume it's a weapon rot_angles.h = -(PI_2); @@ -1588,14 +1587,20 @@ void draw_model_icon(int model_id, uint64_t flags, float closeup_zoom, int x, in gr_set_clip(x, y, w, h, resize_mode); g3_start_frame(1); - if(sip != NULL) + if (sip != nullptr) { - g3_set_view_matrix( &sip->closeup_pos, &vmd_identity_matrix, zoom); + const auto& closeup_pos = sip->icon_closeup_pos.value_or(sip->closeup_pos); + const auto closeup_zoom = sip->icon_closeup_zoom.value_or(sip->closeup_zoom); + const auto zoom = closeup_zoom * zoom_multiplier * 2.5f; + + g3_set_view_matrix(&closeup_pos, &vmd_identity_matrix, zoom); gr_set_proj_matrix(Proj_fov * 0.5f, gr_screen.clip_aspect, Min_draw_distance, Max_draw_distance); } else { + Assertion(wip != nullptr, "Weapon is null, get a coder!"); + polymodel *pm = model_get(model_id); bsp_info *bs = NULL; //tehe for(int i = 0; i < pm->n_models; i++) @@ -1612,24 +1617,23 @@ void draw_model_icon(int model_id, uint64_t flags, float closeup_zoom, int x, in bs = &pm->submodel[0]; } - vec3d weap_closeup = *closeup_pos; + vec3d weap_closeup = wip->icon_closeup_pos.value_or(wip->closeup_pos); float y_closeup; - float tm_zoom = closeup_zoom; + float tm_zoom = wip->icon_closeup_zoom.value_or(wip->closeup_zoom) * zoom_multiplier; - //Find the center of teh submodel - weap_closeup.xyz.x = -(bs->min.xyz.z + (bs->max.xyz.z - bs->min.xyz.z)/2.0f); - weap_closeup.xyz.y = -(bs->min.xyz.y + (bs->max.xyz.y - bs->min.xyz.y)/2.0f); - //weap_closeup.xyz.z = (weap_closeup.xyz.x/tanf(zoom / 2.0f)); - weap_closeup.xyz.z = -(bs->rad/tanf(tm_zoom/2.0f)); + if (!wip->icon_closeup_pos.has_value()) { + // Find the center of the submodel when no icon-specific position override is defined + weap_closeup.xyz.x = -(bs->min.xyz.z + (bs->max.xyz.z - bs->min.xyz.z) / 2.0f); + weap_closeup.xyz.y = -(bs->min.xyz.y + (bs->max.xyz.y - bs->min.xyz.y) / 2.0f); + weap_closeup.xyz.z = -(bs->rad / tanf(tm_zoom / 2.0f)); - y_closeup = -(weap_closeup.xyz.y/tanf(tm_zoom / 2.0f)); - if(y_closeup < weap_closeup.xyz.z) - { - weap_closeup.xyz.z = y_closeup; - } - if(bs->min.xyz.x < weap_closeup.xyz.z) - { - weap_closeup.xyz.z = bs->min.xyz.x; + y_closeup = -(weap_closeup.xyz.y / tanf(tm_zoom / 2.0f)); + if (y_closeup < weap_closeup.xyz.z) { + weap_closeup.xyz.z = y_closeup; + } + if (bs->min.xyz.x < weap_closeup.xyz.z) { + weap_closeup.xyz.z = bs->min.xyz.x; + } } g3_set_view_matrix( &weap_closeup, &vmd_identity_matrix, tm_zoom); diff --git a/code/missionui/missionscreencommon.h b/code/missionui/missionscreencommon.h index 40029d28d20..cbc5312b790 100644 --- a/code/missionui/missionscreencommon.h +++ b/code/missionui/missionscreencommon.h @@ -234,7 +234,8 @@ int store_wss_data(ubyte *data, const unsigned int max_size, interface_snd_id so int restore_wss_data(ubyte *data); class ship_info; -void draw_model_icon(int model_id, uint64_t flags, float closeup_zoom, int x1, int x2, int y1, int y2, ship_info* sip = NULL, int resize_mode = GR_RESIZE_FULL, const vec3d *closeup_pos = &vmd_zero_vector); +struct weapon_info; +void draw_model_icon(int model_id, uint64_t flags, int x1, int x2, int y1, int y2, ship_info* sip = nullptr, weapon_info* wip = nullptr, float zoom_multiplier = 1.0f, int resize_mode = GR_RESIZE_FULL); void draw_model_rotating(model_render_params *render_info, int ship_class, int model_id, int x1, int y1, int x2, int y2, float *rotation_buffer, const vec3d *closeup_pos=nullptr, float closeup_zoom = .65f, float rev_rate = REVOLUTION_RATE, uint64_t flags = MR_AUTOCENTER | MR_NO_FOGGING, int resize_mode=GR_RESIZE_FULL, select_effect_params effect_params = select_effect_params{}); void common_set_team_pointers(int team); diff --git a/code/missionui/missionshipchoice.cpp b/code/missionui/missionshipchoice.cpp index 48b0b3ea9ea..b7051b043a7 100644 --- a/code/missionui/missionshipchoice.cpp +++ b/code/missionui/missionshipchoice.cpp @@ -1515,7 +1515,7 @@ void ship_select_do(float frametime) if(Ss_icons[Carried_ss_icon.ship_class].model_index != -1) { - draw_model_icon(Ss_icons[Carried_ss_icon.ship_class].model_index, MR_AUTOCENTER | MR_NO_FOGGING | MR_NO_LIGHTING, sip->closeup_zoom / 1.25f, sx, sy, w, h, sip, GR_RESIZE_MENU); + draw_model_icon(Ss_icons[Carried_ss_icon.ship_class].model_index, MR_AUTOCENTER | MR_NO_FOGGING | MR_NO_LIGHTING, sx, sy, w, h, sip, nullptr, 0.8f, GR_RESIZE_MENU); } } } @@ -1705,7 +1705,7 @@ void draw_ship_icon_with_number(int screen_offset, int ship_class) if(ss_icon->model_index != -1) { - draw_model_icon(ss_icon->model_index, MR_AUTOCENTER | MR_NO_FOGGING | MR_NO_LIGHTING, sip->closeup_zoom / 1.25f, Ship_list_coords[gr_screen.res][screen_offset][0],Ship_list_coords[gr_screen.res][screen_offset][1], 32, 28, sip, GR_RESIZE_MENU); + draw_model_icon(ss_icon->model_index, MR_AUTOCENTER | MR_NO_FOGGING | MR_NO_LIGHTING, Ship_list_coords[gr_screen.res][screen_offset][0],Ship_list_coords[gr_screen.res][screen_offset][1], 32, 28, sip, nullptr, 0.8f, GR_RESIZE_MENU); } } @@ -2307,7 +2307,7 @@ void draw_wing_block(int wb_num, int hot_slot, int selected_slot, int class_sele draw_brackets_square(&line_draw_list, Wing_icon_coords[gr_screen.res][slot_index][0], Wing_icon_coords[gr_screen.res][slot_index][1], Wing_icon_coords[gr_screen.res][slot_index][0] + 32, Wing_icon_coords[gr_screen.res][slot_index][1] + 28, GR_RESIZE_MENU); line_draw_list.flush(); - draw_model_icon(icon->model_index, MR_AUTOCENTER | MR_NO_FOGGING | MR_NO_LIGHTING, sip->closeup_zoom / 1.25f, Wing_icon_coords[gr_screen.res][slot_index][0], Wing_icon_coords[gr_screen.res][slot_index][1], 32, 28, sip, GR_RESIZE_MENU); + draw_model_icon(icon->model_index, MR_AUTOCENTER | MR_NO_FOGGING | MR_NO_LIGHTING, Wing_icon_coords[gr_screen.res][slot_index][0], Wing_icon_coords[gr_screen.res][slot_index][1], 32, 28, sip, nullptr, 0.8f, GR_RESIZE_MENU); } } } @@ -2390,7 +2390,7 @@ void ss_blit_ship_icon(int x,int y,int ship_class,int bmap_num) draw_brackets_square(&line_draw_list, x, y, x + 32, y + 28, GR_RESIZE_MENU); line_draw_list.flush(); - draw_model_icon(icon->model_index, MR_AUTOCENTER | MR_NO_FOGGING | MR_NO_LIGHTING, sip->closeup_zoom / 1.25f, x, y, 32, 28, sip, GR_RESIZE_MENU); + draw_model_icon(icon->model_index, MR_AUTOCENTER | MR_NO_FOGGING | MR_NO_LIGHTING, x, y, 32, 28, sip, nullptr, 0.8f, GR_RESIZE_MENU); } } } diff --git a/code/missionui/missionweaponchoice.cpp b/code/missionui/missionweaponchoice.cpp index 6f8f20cf1ca..e487b9bff7b 100644 --- a/code/missionui/missionweaponchoice.cpp +++ b/code/missionui/missionweaponchoice.cpp @@ -2909,7 +2909,7 @@ void weapon_select_do(float frametime) if(icon->model_index != -1) { //Draw the model - draw_model_icon(icon->model_index, MR_NO_FOGGING | MR_NO_LIGHTING, wip->closeup_zoom / 2.5f, sx, sy, w, h, NULL, GR_RESIZE_MENU, &wip->closeup_pos); + draw_model_icon(icon->model_index, MR_NO_FOGGING | MR_NO_LIGHTING, sx, sy, w, h, nullptr, wip, 0.4f, GR_RESIZE_MENU); } else if(icon->laser_bmap != -1) { @@ -3141,7 +3141,7 @@ void wl_render_icon(int index, int x, int y, int num, int draw_num_flag, int hot if(icon->model_index != -1) { //Draw the model - draw_model_icon(icon->model_index, MR_NO_FOGGING | MR_NO_LIGHTING, Weapon_info[index].closeup_zoom * 0.4f, x, y, 56, 24, NULL, GR_RESIZE_MENU); + draw_model_icon(icon->model_index, MR_NO_FOGGING | MR_NO_LIGHTING, x, y, 56, 24, nullptr, &Weapon_info[index], 0.4f, GR_RESIZE_MENU); } else if(icon->laser_bmap != -1) { diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index a706bd8e663..24fedc63581 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -1244,6 +1244,8 @@ void ship_info::clone(const ship_info& other) closeup_pos = other.closeup_pos; closeup_zoom = other.closeup_zoom; + icon_closeup_pos = other.icon_closeup_pos; + icon_closeup_zoom = other.icon_closeup_zoom; closeup_pos_targetbox = other.closeup_pos_targetbox; closeup_zoom_targetbox = other.closeup_zoom_targetbox; @@ -1596,6 +1598,8 @@ void ship_info::move(ship_info&& other) std::swap(closeup_pos, other.closeup_pos); closeup_zoom = other.closeup_zoom; + std::swap(icon_closeup_pos, other.icon_closeup_pos); + icon_closeup_zoom = other.icon_closeup_zoom; std::swap(closeup_pos_targetbox, other.closeup_pos_targetbox); closeup_zoom_targetbox = other.closeup_zoom_targetbox; @@ -1985,6 +1989,8 @@ ship_info::ship_info() vm_vec_zero(&closeup_pos); closeup_zoom = 0.5f; + icon_closeup_pos = std::nullopt; + icon_closeup_zoom = std::nullopt; vm_vec_zero(&closeup_pos_targetbox); closeup_zoom_targetbox = 0.5f; @@ -4774,6 +4780,24 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool } } + if (optional_string("$Icon_closeup_pos:")) { + vec3d icon_pos; + stuff_vec3d(&icon_pos); + sip->icon_closeup_pos = icon_pos; + } + + if (optional_string("$Icon_closeup_zoom:")) { + float icon_zoom; + stuff_float(&icon_zoom); + + if (icon_zoom <= 0.0f) { + mprintf(("Warning! Ship '%s' has a $Icon_closeup_zoom value that is less than or equal to 0 (%f). Ignoring value.\n", sip->name, icon_zoom)); + sip->icon_closeup_zoom = std::nullopt; + } else { + sip->icon_closeup_zoom = icon_zoom; + } + } + if(optional_string("$Closeup_pos_targetbox:")) { stuff_vec3d(&sip->closeup_pos_targetbox); diff --git a/code/ship/ship.h b/code/ship/ship.h index fc25427a6bc..d656d936084 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -33,6 +33,7 @@ #include #include +#include #include class object; @@ -1346,6 +1347,8 @@ class ship_info vec3d closeup_pos; // position for camera when using ship in closeup view (eg briefing and techroom) float closeup_zoom; // zoom when using ship in closeup view (eg briefing and techroom) + std::optional icon_closeup_pos; // icon-specific position for camera when using ship in closeup view + std::optional icon_closeup_zoom; // icon-specific zoom when using ship in closeup view vec3d closeup_pos_targetbox; // position for camera when using ship in closeup view for hud target monitor float closeup_zoom_targetbox; // zoom when using ship in closeup view for hud target monitor diff --git a/code/weapon/weapon.h b/code/weapon/weapon.h index 72a550cf3dd..c491d97f200 100644 --- a/code/weapon/weapon.h +++ b/code/weapon/weapon.h @@ -35,6 +35,8 @@ #include "utils/modular_curves.h" +#include + class object; class ship_subsys; @@ -386,6 +388,8 @@ struct weapon_info vec3d closeup_pos; // position for camera to set an offset for viewing the weapon model float closeup_zoom; // zoom when using weapon model in closeup view in loadout selection + std::optional icon_closeup_pos; // icon-specific position for camera for viewing the weapon model + std::optional icon_closeup_zoom; // icon-specific zoom for viewing the weapon model char hud_filename[MAX_FILENAME_LEN]; //Name of image to display on HUD in place of text int hud_image_index; //teh index of the image diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index b04384f4dea..fed3239797e 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -985,6 +985,24 @@ int parse_weapon(int subtype, bool replace, const char *filename) } } + if (optional_string("+Icon_closeup_pos:")) { + vec3d icon_pos; + stuff_vec3d(&icon_pos); + wip->icon_closeup_pos = icon_pos; + } + + if (optional_string("+Icon_closeup_zoom:")) { + float icon_zoom; + stuff_float(&icon_zoom); + + if (icon_zoom <= 0.0f) { + mprintf(("Warning! Weapon '%s' has a +Icon_closeup_zoom value that is less than or equal to 0 (%f). Ignoring value.\n", wip->name, icon_zoom)); + wip->icon_closeup_zoom = std::nullopt; + } else { + wip->icon_closeup_zoom = icon_zoom; + } + } + // Weapon fadein effect, used when no ani is specified or weapon_select_3d is active if (first_time) { wip->selection_effect = Default_weapon_select_effect; // By default, use the FS2 effect @@ -9396,6 +9414,8 @@ void weapon_info::reset() vm_vec_zero(&this->closeup_pos); this->closeup_zoom = 1.0f; + this->icon_closeup_pos = std::nullopt; + this->icon_closeup_zoom = std::nullopt; memset(this->hud_filename, 0, sizeof(this->hud_filename)); this->hud_image_index = -1; From d6509d81abe44c0fdff09a87ce9f7fb678f14f59 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 9 May 2026 09:50:30 -0500 Subject: [PATCH 29/65] QtFRED Camera Class (#7408) --- code/io/spacemouse.cpp | 6 + code/io/spacemouse.h | 9 + qtfred/source_groups.cmake | 2 + qtfred/src/mission/CameraController.cpp | 108 +++++ qtfred/src/mission/CameraController.h | 66 +++ qtfred/src/mission/Editor.cpp | 14 +- qtfred/src/mission/EditorViewport.cpp | 458 +++++++----------- qtfred/src/mission/EditorViewport.h | 74 ++- qtfred/src/mission/FredRenderer.cpp | 12 +- qtfred/src/ui/FredView.cpp | 149 +++--- .../src/ui/dialogs/BriefingEditorDialog.cpp | 13 +- qtfred/src/ui/dialogs/BriefingEditorDialog.h | 3 +- qtfred/src/ui/widgets/BriefingMapWidget.cpp | 72 ++- qtfred/src/ui/widgets/BriefingMapWidget.h | 5 +- 14 files changed, 495 insertions(+), 496 deletions(-) create mode 100644 qtfred/src/mission/CameraController.cpp create mode 100644 qtfred/src/mission/CameraController.h diff --git a/code/io/spacemouse.cpp b/code/io/spacemouse.cpp index 09def91cb90..bc4f9435841 100644 --- a/code/io/spacemouse.cpp +++ b/code/io/spacemouse.cpp @@ -167,6 +167,12 @@ std::unique_ptr SpaceMouse::searchSpaceMice(int pollingFrequency) { return mouse; } +SpaceMouse* SpaceMouse::getSharedSpaceMouse(int pollingFrequency) +{ + static std::unique_ptr sharedMouse = SpaceMouse::searchSpaceMice(pollingFrequency); + return sharedMouse.get(); +} + #define HANDLE_NONLINEARITY(field, idx) field = copysignf(powf(field, std::get<0>(spacemouse_nonlinearity[idx])) * std::get<1>(spacemouse_nonlinearity[idx]), field) void SpaceMouseMovement::handleNonlinearities(std::array, 6>& spacemouse_nonlinearity) { diff --git a/code/io/spacemouse.h b/code/io/spacemouse.h index 03688299644..4f4ef563f9a 100644 --- a/code/io/spacemouse.h +++ b/code/io/spacemouse.h @@ -57,6 +57,15 @@ namespace io @returns An optional SpaceMouse object, if found */ static std::unique_ptr searchSpaceMice(int pollingFrequency = 10); + + /* + @brief Returns a shared SpaceMouse instance for the process. + + This avoids opening the same HID device from multiple call sites. + @param pollingFrequency Polling frequency to use when creating the shared instance. + @returns A pointer to the shared SpaceMouse instance, or nullptr if no supported device is present. + */ + static SpaceMouse* getSharedSpaceMouse(int pollingFrequency = 10); }; } } \ No newline at end of file diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 43b06165d9b..9b36b37c71e 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -19,6 +19,8 @@ if (WIN32) endif() add_file_folder("Source/Mission" + src/mission/CameraController.cpp + src/mission/CameraController.h src/mission/Editor.cpp src/mission/EditorWing.cpp src/mission/Editor.h diff --git a/qtfred/src/mission/CameraController.cpp b/qtfred/src/mission/CameraController.cpp new file mode 100644 index 00000000000..e2307099a4f --- /dev/null +++ b/qtfred/src/mission/CameraController.cpp @@ -0,0 +1,108 @@ +#include "CameraController.h" + +#include "ui/ControlBindings.h" + +#include +#include + +namespace fso::fred { + +void CameraController::resetView() { + vec3d f, u, r; + + physics_init(&view_physics); + view_physics.max_vel.xyz.z = 5.0f; + view_physics.max_rotvel.xyz.x = 1.5f; + memset(&view_controls, 0, sizeof(control_info)); + + vm_vec_make(&view_pos, 0.0f, 150.0f, -200.0f); + vm_vec_make(&f, 0.0f, -0.5f, 0.866025404f); + vm_vec_make(&u, 0.0f, 0.866025404f, 0.5f); + vm_vec_make(&r, 1.0f, 0.0f, 0.0f); + vm_vector_2_matrix(&view_orient, &f, &u, &r); +} + +void CameraController::resetViewPhysics() { + physics_init(&view_physics); + view_physics.max_vel.xyz.x *= _physicsSpeed / 3.0f; + view_physics.max_vel.xyz.y *= _physicsSpeed / 3.0f; + view_physics.max_vel.xyz.z *= _physicsSpeed / 3.0f; + view_physics.max_rear_vel *= _physicsSpeed / 3.0f; + view_physics.max_rotvel.xyz.x *= _physicsRot / 30.0f; + view_physics.max_rotvel.xyz.y *= _physicsRot / 30.0f; + view_physics.max_rotvel.xyz.z *= _physicsRot / 30.0f; + view_physics.flags |= PF_ACCELERATES | PF_SLIDE_ENABLED; +} + +void CameraController::savePosition() { + saved_cam_pos = view_pos; + saved_cam_orient = view_orient; +} + +void CameraController::restorePosition() { + view_pos = saved_cam_pos; + view_orient = saved_cam_orient; +} + +bool CameraController::hasSavedPosition() const { + return !IS_VEC_NULL(&saved_cam_orient.vec.fvec); +} + +bool CameraController::hasEyeMoved() { + if (vm_vec_cmp(&eye_pos, &Last_eye_pos) || vm_matrix_cmp(&eye_orient, &Last_eye_orient)) { + Last_eye_pos = eye_pos; + Last_eye_orient = eye_orient; + return true; + } + return false; +} + +const matrix& CameraController::getLastRotMat() const { + return view_physics.last_rotmat; +} + +bool CameraController::processControls(vec3d* pos, matrix* orient, float frametime, bool use_editor_physics) { + io::spacemouse::SpaceMouse* const spacemouse = io::spacemouse::SpaceMouse::getSharedSpaceMouse(0); + + memset(&view_controls, 0, sizeof(control_info)); + + if (spacemouse != nullptr) { + auto spacemouse_movement = spacemouse->getMovement(); + spacemouse_movement.handleNonlinearities(Fred_spacemouse_nonlinearity); + view_controls.pitch += spacemouse_movement.rotation.p; + view_controls.vertical += spacemouse_movement.translation.xyz.z; + view_controls.heading += spacemouse_movement.rotation.h; + view_controls.sideways += spacemouse_movement.translation.xyz.x; + view_controls.bank += spacemouse_movement.rotation.b; + view_controls.forward += spacemouse_movement.translation.xyz.y; + } + + auto& bindings = ControlBindings::instance(); + view_controls.pitch += bindings.isPressed(ControlAction::PitchUp) ? -1.0f : 0.0f; + view_controls.pitch += bindings.isPressed(ControlAction::PitchDown) ? 1.0f : 0.0f; + view_controls.heading += bindings.isPressed(ControlAction::YawLeft) ? -1.0f : 0.0f; + view_controls.heading += bindings.isPressed(ControlAction::YawRight) ? 1.0f : 0.0f; + view_controls.sideways += bindings.isPressed(ControlAction::MoveLeft) ? -1.0f : 0.0f; + view_controls.sideways += bindings.isPressed(ControlAction::MoveRight) ? 1.0f : 0.0f; + view_controls.forward += bindings.isPressed(ControlAction::MoveForward) ? 1.0f : 0.0f; + view_controls.forward += bindings.isPressed(ControlAction::MoveBackward) ? -1.0f : 0.0f; + view_controls.vertical += bindings.isPressed(ControlAction::MoveUp) ? 1.0f : 0.0f; + view_controls.vertical += bindings.isPressed(ControlAction::MoveDown) ? -1.0f : 0.0f; + + bool wantsUpdate = (fabs(view_controls.pitch) > (frametime / 100)) + || (fabs(view_controls.vertical) > (frametime / 100)) + || (fabs(view_controls.heading) > (frametime / 100)) + || (fabs(view_controls.sideways) > (frametime / 100)) + || (fabs(view_controls.bank) > (frametime / 100)) + || (fabs(view_controls.forward) > (frametime / 100)); + + physics_read_flying_controls(orient, &view_physics, &view_controls, frametime); + if (use_editor_physics) { + physics_sim_editor(pos, orient, &view_physics, frametime); + } else { + physics_sim(pos, orient, &view_physics, &vmd_zero_vector, frametime); + } + return wantsUpdate; +} + +} // namespace fso::fred diff --git a/qtfred/src/mission/CameraController.h b/qtfred/src/mission/CameraController.h new file mode 100644 index 00000000000..661d111ef71 --- /dev/null +++ b/qtfred/src/mission/CameraController.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include + +namespace fso::fred { + +class CameraController { + physics_info view_physics; + control_info view_controls; + + vec3d saved_cam_pos = vmd_zero_vector; + matrix saved_cam_orient = {}; + + vec3d Last_eye_pos = vmd_zero_vector; + matrix Last_eye_orient = vmd_identity_matrix; + + int _physicsSpeed = 1; + int _physicsRot = 25; + int _viewpoint = 0; + int _viewObj = -1; + int _controlMode = 0; + bool _lookatMode = false; + +public: + vec3d eye_pos; + matrix eye_orient; + + vec3d view_pos; + matrix view_orient = vmd_identity_matrix; + + void resetView(); + void resetViewPhysics(); + + void savePosition(); + void restorePosition(); + bool hasSavedPosition() const; + + bool hasEyeMoved(); + + const matrix& getLastRotMat() const; + + // Returns true if the caller should schedule a view update + bool processControls(vec3d* pos, matrix* orient, float frametime, bool use_editor_physics); + + int getPhysicsSpeed() const { return _physicsSpeed; } + void setPhysicsSpeed(int speed) { _physicsSpeed = speed; resetViewPhysics(); } + + int getPhysicsRot() const { return _physicsRot; } + void setPhysicsRot(int rot) { _physicsRot = rot; resetViewPhysics(); } + + int getViewpoint() const { return _viewpoint; } + void setViewpoint(int vp) { _viewpoint = vp; } + + int getViewObj() const { return _viewObj; } + void setViewObj(int obj) { _viewObj = obj; } + + int getControlMode() const { return _controlMode; } + void setControlMode(int mode) { _controlMode = mode; } + void toggleControlMode() { _controlMode = (_controlMode + 1) % 2; } + + bool getLookatMode() const { return _lookatMode; } + void setLookatMode(bool mode) { _lookatMode = mode; } +}; + +} // namespace fso::fred diff --git a/qtfred/src/mission/Editor.cpp b/qtfred/src/mission/Editor.cpp index 2f148873679..64e1d8d755c 100644 --- a/qtfred/src/mission/Editor.cpp +++ b/qtfred/src/mission/Editor.cpp @@ -140,8 +140,8 @@ int Editor::autosave(const char* /*desc*/) { Fred_mission_save save; save.set_always_save_display_names(_lastActiveViewport->Always_save_display_names); - save.set_view_pos(_lastActiveViewport->view_pos); - save.set_view_orient(_lastActiveViewport->view_orient); + save.set_view_pos(_lastActiveViewport->camera.view_pos); + save.set_view_orient(_lastActiveViewport->camera.view_orient); save.set_fred_alt_names(Fred_alt_names); save.set_fred_callsigns(Fred_callsigns); @@ -460,8 +460,8 @@ bool Editor::loadMission(const std::string& mission_name, int flags) { } for (auto& viewport : _viewports) { - viewport->view_orient = Parse_viewer_orient; - viewport->view_pos = Parse_viewer_pos; + viewport->camera.view_orient = Parse_viewer_orient; + viewport->camera.view_pos = Parse_viewer_pos; } if (flags & MPF_IS_TEMPLATE) { @@ -479,8 +479,7 @@ bool Editor::loadMission(const std::string& mission_name, int flags) { strcpy_s(The_mission.mission_desc, "Put mission description here"); for (auto& viewport : _viewports) { - viewport->resetView(); - viewport->resetViewPhysics(); + viewport->reset(); } } @@ -654,8 +653,7 @@ void Editor::clearMission(bool fast_reload) { unmark_all(); for (auto& viewport : _viewports) { - viewport->resetView(); - viewport->resetViewPhysics(); + viewport->reset(); } Event_annotations.clear(); diff --git a/qtfred/src/mission/EditorViewport.cpp b/qtfred/src/mission/EditorViewport.cpp index 05969c9f5f0..cc382babeca 100644 --- a/qtfred/src/mission/EditorViewport.cpp +++ b/qtfred/src/mission/EditorViewport.cpp @@ -1,15 +1,15 @@ #include +#include +#include #include #include #include #include "ui/ControlBindings.h" -#include #include "object.h" #include "EditorViewport.h" #include -#include "ui/dialogs/BriefingEditorDialog.h" #include #include #include @@ -20,50 +20,8 @@ namespace { constexpr auto SETTINGS_GROUP = "Preferences"; -const fix MAX_FRAMETIME = (F1_0 / 4); // Frametime gets saturated at this. -const fix MIN_FRAMETIME = (F1_0 / 120); - const float REDUCER = 100.0f; -void process_movement_keys(const fso::fred::ControlBindings& bindings, vec3d* mvec, angles* angs) { - mvec->xyz.x = 0.0f; - mvec->xyz.y = 0.0f; - mvec->xyz.z = 0.0f; - angs->p = 0.0f; - angs->b = 0.0f; - angs->h = 0.0f; - - if (bindings.isPressed(fso::fred::ControlAction::MoveLeft)) { - mvec->xyz.x += -1.0f; - } - if (bindings.isPressed(fso::fred::ControlAction::MoveRight)) { - mvec->xyz.x += 1.0f; - } - if (bindings.isPressed(fso::fred::ControlAction::MoveForward)) { - mvec->xyz.y += 1.0f; - } - if (bindings.isPressed(fso::fred::ControlAction::MoveBackward)) { - mvec->xyz.y += -1.0f; - } - if (bindings.isPressed(fso::fred::ControlAction::MoveUp)) { - mvec->xyz.z += 1.0f; - } - if (bindings.isPressed(fso::fred::ControlAction::MoveDown)) { - mvec->xyz.z += -1.0f; - } - if (bindings.isPressed(fso::fred::ControlAction::YawLeft)) { - angs->h += -0.1f; - } - if (bindings.isPressed(fso::fred::ControlAction::YawRight)) { - angs->h += 0.1f; - } - if (bindings.isPressed(fso::fred::ControlAction::PitchUp)) { - angs->p += -0.1f; - } - if (bindings.isPressed(fso::fred::ControlAction::PitchDown)) { - angs->p += 0.1f; - } -} void align_vector_to_axis(vec3d* v) { float x, y, z; @@ -112,6 +70,42 @@ namespace fso::fred { const char* EditorViewport::DefaultLayerName = "Default"; +EditorViewport::ViewportControlLock::ViewportControlLock(EditorViewport* viewport) : _viewport(viewport) +{ + if (_viewport != nullptr) { + _viewport->lockControls(); + } +} + +EditorViewport::ViewportControlLock::~ViewportControlLock() +{ + if (_viewport != nullptr) { + _viewport->unlockControls(); + } +} + +EditorViewport::ViewportControlLock::ViewportControlLock(ViewportControlLock&& other) noexcept + : _viewport(other._viewport) +{ + other._viewport = nullptr; +} + +EditorViewport::ViewportControlLock& EditorViewport::ViewportControlLock::operator=( + ViewportControlLock&& other) noexcept +{ + if (this == &other) { + return *this; + } + + if (_viewport != nullptr) { + _viewport->unlockControls(); + } + + _viewport = other._viewport; + other._viewport = nullptr; + return *this; +} + EditorViewport::EditorViewport(Editor* in_editor, std::unique_ptr&& in_renderer) : _renderer(std::move(in_renderer)), editor(in_editor) { renderer = _renderer.get(); @@ -120,9 +114,8 @@ EditorViewport::EditorViewport(Editor* in_editor, std::unique_ptr& vm_vec_make(&Constraint, 1.0f, 0.0f, 1.0f); vm_vec_make(&Anticonstraint, 0.0f, 1.0f, 0.0f); - resetView(); + reset(); - memset(&saved_cam_orient, 0, sizeof(saved_cam_orient)); _layerNames.emplace_back(DefaultLayerName); _layerVisibility.push_back(true); syncMissionLayerNames(); @@ -214,16 +207,51 @@ void EditorViewport::saveSettings() const { void EditorViewport::needsUpdate() { _renderer->scheduleUpdate(); } -void EditorViewport::resetViewPhysics() { - physics_init(&view_physics); - view_physics.max_vel.xyz.x *= physics_speed / 3.0f; - view_physics.max_vel.xyz.y *= physics_speed / 3.0f; - view_physics.max_vel.xyz.z *= physics_speed / 3.0f; - view_physics.max_rear_vel *= physics_speed / 3.0f; - view_physics.max_rotvel.xyz.x *= physics_rot / 30.0f; - view_physics.max_rotvel.xyz.y *= physics_rot / 30.0f; - view_physics.max_rotvel.xyz.z *= physics_rot / 30.0f; - view_physics.flags |= PF_ACCELERATES | PF_SLIDE_ENABLED; + +bool EditorViewport::areControlsLocked() const +{ + return _controlLockCount > 0; +} + +EditorViewport::ViewportControlLock EditorViewport::acquireControlLock() +{ + return ViewportControlLock(this); +} + +void EditorViewport::lockControls() +{ + ++_controlLockCount; +} + +void EditorViewport::unlockControls() +{ + Assertion(_controlLockCount > 0, "Mismatched unlock on EditorViewport controls"); + --_controlLockCount; +} + +bool EditorViewport::incMissionTime() { + const fix MAX_FRAMETIME = (F1_0 / 4); + const fix MIN_FRAMETIME = (F1_0 / 120); + + fix thistime = timer_get_fixed_seconds(); + fix time_diff; + if (!_lasttime) { + time_diff = F1_0 / 30; + } else { + time_diff = thistime - _lasttime; + } + + if (time_diff > MAX_FRAMETIME) { + time_diff = MAX_FRAMETIME; + } else if (time_diff < MIN_FRAMETIME) { + return false; + } + + Frametime = time_diff; + Missiontime += Frametime; + _lasttime = thistime; + + return true; } void EditorViewport::select_objects(const Marking_box& box) { int x, y, valid, icon_mode = 0; @@ -330,56 +358,17 @@ void EditorViewport::select_objects(const Marking_box& box) { needsUpdate(); } -void EditorViewport::resetView() { - my_pos = vmd_zero_vector; - my_pos.xyz.z = -5.0f; - vec3d f, u, r; - - physics_init(&view_physics); - view_physics.max_vel.xyz.z = 5.0f; //forward/backward - view_physics.max_rotvel.xyz.x = 1.5f; //pitch - memset(&view_controls, 0, sizeof(control_info)); - - vm_vec_make(&view_pos, 0.0f, 150.0f, -200.0f); - vm_vec_make(&f, 0.0f, -0.5f, 0.866025404f); // 30 degree angle - vm_vec_make(&u, 0.0f, 0.866025404f, 0.5f); - vm_vec_make(&r, 1.0f, 0.0f, 0.0f); - vm_vector_2_matrix(&view_orient, &f, &u, &r); - +void EditorViewport::reset() { + camera.resetView(); + camera.resetViewPhysics(); The_grid = create_default_grid(); - maybe_create_new_grid(The_grid, &view_pos, &view_orient, 1); - // vm_set_identity(&view_orient); -} - - -void EditorViewport::move_mouse(int btn, int mdx, int mdy) { - int dx, dy; - - dx = mdx - last_x; - dy = mdy - last_y; - last_x = mdx; - last_y = mdy; - - if (btn & 1) { - matrix tempm, mousem; - - if (dx || dy) { - vm_trackball(dx, dy, &mousem); - vm_matrix_x_matrix(&tempm, &trackball_orient, &mousem); - trackball_orient = tempm; - view_orient = trackball_orient; - } - } - - if (btn & 2) { - my_pos.xyz.z += (float) dy; - } + maybe_create_new_grid(The_grid, &camera.view_pos, &camera.view_orient, 1); } /////////////////////////////////////////////////// void EditorViewport::process_system_keys() { auto& bindings = ControlBindings::instance(); - if (dialogs::BriefingEditorDialog::isAnyDialogOpen()) { + if (areControlsLocked()) { return; } if (bindings.takeTriggered(ControlAction::ToggleSelectionLock)) { @@ -388,169 +377,65 @@ void EditorViewport::process_system_keys() { } -void EditorViewport::process_controls(vec3d* pos, matrix* orient, float frametime, int mode) { - static std::unique_ptr spacemouse = io::spacemouse::SpaceMouse::searchSpaceMice(0); - if (dialogs::BriefingEditorDialog::isAnyDialogOpen()) { - return; - } - - if (Flying_controls_mode) { - memset(&view_controls, 0, sizeof(control_info)); - - if (spacemouse != nullptr) { - auto spacemouse_movement = spacemouse->getMovement(); - spacemouse_movement.handleNonlinearities(Fred_spacemouse_nonlinearity); - view_controls.pitch += spacemouse_movement.rotation.p; - view_controls.vertical += spacemouse_movement.translation.xyz.z; - view_controls.heading += spacemouse_movement.rotation.h; - view_controls.sideways += spacemouse_movement.translation.xyz.x; - view_controls.bank += spacemouse_movement.rotation.b; - view_controls.forward += spacemouse_movement.translation.xyz.y; - } - - auto& bindings = ControlBindings::instance(); - view_controls.pitch += bindings.isPressed(ControlAction::PitchUp) ? -1.0f : 0.0f; - view_controls.pitch += bindings.isPressed(ControlAction::PitchDown) ? 1.0f : 0.0f; - view_controls.heading += bindings.isPressed(ControlAction::YawLeft) ? -1.0f : 0.0f; - view_controls.heading += bindings.isPressed(ControlAction::YawRight) ? 1.0f : 0.0f; - view_controls.sideways += bindings.isPressed(ControlAction::MoveLeft) ? -1.0f : 0.0f; - view_controls.sideways += bindings.isPressed(ControlAction::MoveRight) ? 1.0f : 0.0f; - view_controls.forward += bindings.isPressed(ControlAction::MoveForward) ? 1.0f : 0.0f; - view_controls.forward += bindings.isPressed(ControlAction::MoveBackward) ? -1.0f : 0.0f; - view_controls.vertical += bindings.isPressed(ControlAction::MoveUp) ? 1.0f : 0.0f; - view_controls.vertical += bindings.isPressed(ControlAction::MoveDown) ? -1.0f : 0.0f; - - if ((fabs(view_controls.pitch) > (frametime / 100)) || (fabs(view_controls.vertical) > (frametime / 100)) - || (fabs(view_controls.heading) > (frametime / 100)) || (fabs(view_controls.sideways) > (frametime / 100)) - || (fabs(view_controls.bank) > (frametime / 100)) || (fabs(view_controls.forward) > (frametime / 100))) { - needsUpdate(); - } - - //view_physics.flags |= (PF_ACCELERATES | PF_SLIDE_ENABLED); - physics_read_flying_controls(orient, &view_physics, &view_controls, frametime); - if (mode) { - physics_sim_editor(pos, orient, &view_physics, frametime); - } else { - physics_sim(pos, orient, &view_physics, &vmd_zero_vector, frametime); - } - } else { - vec3d movement_vec, rel_movement_vec; - angles rotangs; - matrix newmat, rotmat; - - process_movement_keys(ControlBindings::instance(), &movement_vec, &rotangs); - if (spacemouse != nullptr) { - auto spacemouse_movement = spacemouse->getMovement(); - spacemouse_movement.handleNonlinearities(Fred_spacemouse_nonlinearity); - movement_vec += spacemouse_movement.translation; - rotangs += spacemouse_movement.rotation; - } - - vm_vec_rotate(&rel_movement_vec, &movement_vec, &The_grid->gmatrix); - vm_vec_add2(pos, &rel_movement_vec); - - vm_angles_2_matrix(&rotmat, &rotangs); - if (rotangs.h && view.Universal_heading) { - vm_transpose(orient); - } - vm_matrix_x_matrix(&newmat, orient, &rotmat); - *orient = newmat; - if (rotangs.h && view.Universal_heading) { - vm_transpose(orient); - } - } -} - -/** -* @brief Increments mission time -* -* @details This only increments the mission time if the time difference is greater than the minimum frametime to avoid -* excessive computation -* -* @return @c true if the mission time was incremented, @c false otherwise. -*/ -bool EditorViewport::inc_mission_time() { - fix thistime = timer_get_fixed_seconds(); - fix time_diff; // This holds the computed time difference since the last time this function was called - if (!lasttime) { - time_diff = F1_0 / 30; - } else { - time_diff = thistime - lasttime; - } - - if (time_diff > MAX_FRAMETIME) { - time_diff = MAX_FRAMETIME; - } else if (time_diff < MIN_FRAMETIME) { - return false; - } - - Frametime = time_diff; - Missiontime += Frametime; - lasttime = thistime; - - return true; -} - void EditorViewport::game_do_frame(const int cur_object_index) { int cmode; - vec3d viewer_position, control_pos; + vec3d control_pos; object* objp; matrix control_orient; - if (!inc_mission_time()) { - // Don't do anything if the mission time wasn't incremented + if (!incMissionTime()) { return; } // sync all timestamps across the entire frame timer_start_frame(); - viewer_position = my_orient.vec.fvec; - vm_vec_scale(&viewer_position, my_pos.xyz.z); - - if ((viewpoint == 1) && !query_valid_object(view_obj)) { - viewpoint = 0; + if ((camera.getViewpoint() == 1) && !query_valid_object(camera.getViewObj())) { + camera.setViewpoint(0); } process_system_keys(); - cmode = Control_mode; - if ((viewpoint == 1) && !cmode) { + const auto controlsLocked = areControlsLocked(); + cmode = camera.getControlMode(); + if ((camera.getViewpoint() == 1) && !cmode) { cmode = 2; } control_pos = Last_control_pos; control_orient = Last_control_orient; - // if ((key & KEY_MASK) == key) // unmodified switch (cmode) { case 0: // Control the viewer's location and orientation - process_controls(&view_pos, &view_orient, f2fl(Frametime), 1); - control_pos = view_pos; - control_orient = view_orient; + if (!controlsLocked && camera.processControls(&camera.view_pos, &camera.view_orient, f2fl(Frametime), true)) { + needsUpdate(); + } + control_pos = camera.view_pos; + control_orient = camera.view_orient; break; case 2: // Control viewpoint object - if (!Objects[view_obj].flags[Object::Object_Flags::Locked_from_editing]) { - process_controls(&Objects[view_obj].pos, &Objects[view_obj].orient, f2fl(Frametime)); - object_moved(&Objects[view_obj]); - control_pos = Objects[view_obj].pos; - control_orient = Objects[view_obj].orient; + if (!controlsLocked && !Objects[camera.getViewObj()].flags[Object::Object_Flags::Locked_from_editing]) { + camera.processControls(&Objects[camera.getViewObj()].pos, &Objects[camera.getViewObj()].orient, + f2fl(Frametime), false); + object_moved(&Objects[camera.getViewObj()]); + control_pos = Objects[camera.getViewObj()].pos; + control_orient = Objects[camera.getViewObj()].orient; } break; case 1: // Control the current object's location and orientation - if (query_valid_object(cur_object_index) && !Objects[cur_object_index].flags[Object::Object_Flags::Locked_from_editing]) { + if (!controlsLocked && query_valid_object(cur_object_index) && !Objects[cur_object_index].flags[Object::Object_Flags::Locked_from_editing]) { vec3d delta_pos, leader_old_pos; matrix leader_orient, leader_transpose, tmp; object* leader; leader = &Objects[cur_object_index]; - leader_old_pos = leader->pos; // save original position - leader_orient = leader->orient; // save original orientation + leader_old_pos = leader->pos; + leader_orient = leader->orient; vm_copy_transpose(&leader_transpose, &leader_orient); - process_controls(&leader->pos, &leader->orient, f2fl(Frametime)); - vm_vec_sub(&delta_pos, &leader->pos, &leader_old_pos); // get position change + camera.processControls(&leader->pos, &leader->orient, f2fl(Frametime), false); + vm_vec_sub(&delta_pos, &leader->pos, &leader_old_pos); control_pos = leader->pos; control_orient = leader->orient; @@ -562,36 +447,19 @@ void EditorViewport::game_do_frame(const int cur_object_index) { matrix rot_trans; vec3d tmpv1, tmpv2; - // change rotation matrix to rotate in opposite direction. This rotation - // matrix is what the leader ship has rotated by. - vm_copy_transpose(&rot_trans, &view_physics.last_rotmat); - - // get point relative to our point of rotation (make POR the origin). Since - // only the leader has been moved yet, and not the objects, we have to use - // the old leader's position. + vm_copy_transpose(&rot_trans, &camera.getLastRotMat()); vm_vec_sub(&tmpv1, &objp->pos, &leader_old_pos); - - // convert point from real-world coordinates to leader's relative coordinate - // system (z=forward vec, y=up vec, x=right vec vm_vec_rotate(&tmpv2, &tmpv1, &leader_orient); - - // now rotate the point by the transpose from above. vm_vec_rotate(&tmpv1, &tmpv2, &rot_trans); - - // convert point back into real-world coordinates vm_vec_rotate(&tmpv2, &tmpv1, &leader_transpose); - - // and move origin back to real-world origin. Object is now at its correct - // position. Note we used the leader's new position, instead of old position. vm_vec_add(&objp->pos, &leader->pos, &tmpv2); - // Now fix the object's orientation to what it should be. - vm_matrix_x_matrix(&tmp, &objp->orient, &view_physics.last_rotmat); - vm_orthogonalize_matrix(&tmp); // safety check + vm_matrix_x_matrix(&tmp, &objp->orient, &camera.getLastRotMat()); + vm_orthogonalize_matrix(&tmp); objp->orient = tmp; } else { vm_vec_add2(&objp->pos, &delta_pos); - vm_matrix_x_matrix(&tmp, &objp->orient, &view_physics.last_rotmat); + vm_matrix_x_matrix(&tmp, &objp->orient, &camera.getLastRotMat()); objp->orient = tmp; } } @@ -608,7 +476,6 @@ void EditorViewport::game_do_frame(const int cur_object_index) { objp = GET_NEXT(objp); } - // Notify the editor that the mission has changed editor->missionChanged(); } @@ -618,29 +485,29 @@ void EditorViewport::game_do_frame(const int cur_object_index) { Assert(0); } - if (Lookat_mode && query_valid_object(cur_object_index)) { + if (camera.getLookatMode() && query_valid_object(cur_object_index)) { float dist; - dist = vm_vec_dist(&view_pos, &Objects[cur_object_index].pos); - vm_vec_scale_add(&view_pos, &Objects[cur_object_index].pos, &view_orient.vec.fvec, -dist); + dist = vm_vec_dist(&camera.view_pos, &Objects[cur_object_index].pos); + vm_vec_scale_add(&camera.view_pos, &Objects[cur_object_index].pos, &camera.view_orient.vec.fvec, -dist); } - switch (viewpoint) { + switch (camera.getViewpoint()) { case 0: - eye_pos = view_pos; - eye_orient = view_orient; + camera.eye_pos = camera.view_pos; + camera.eye_orient = camera.view_orient; break; case 1: - eye_pos = Objects[view_obj].pos; - eye_orient = Objects[view_obj].orient; + camera.eye_pos = Objects[camera.getViewObj()].pos; + camera.eye_orient = Objects[camera.getViewObj()].orient; break; default: Assert(0); } - maybe_create_new_grid(The_grid, &eye_pos, &eye_orient); + maybe_create_new_grid(The_grid, &camera.eye_pos, &camera.eye_orient); if (Cursor_over != Last_cursor_over) { Last_cursor_over = Cursor_over; @@ -655,10 +522,8 @@ void EditorViewport::game_do_frame(const int cur_object_index) { } // redraw screen if current viewpoint moved or rotated - if (vm_vec_cmp(&eye_pos, &Last_eye_pos) || vm_matrix_cmp(&eye_orient, &Last_eye_orient)) { + if (camera.hasEyeMoved()) { needsUpdate(); - Last_eye_pos = eye_pos; - Last_eye_orient = eye_orient; } } @@ -666,20 +531,20 @@ void EditorViewport::level_controlled() { int cmode, count = 0; object* objp; - cmode = Control_mode; - if ((viewpoint == 1) && !cmode) { + cmode = camera.getControlMode(); + if ((camera.getViewpoint() == 1) && !cmode) { cmode = 2; } switch (cmode) { case 0: // Control the viewer's location and orientation - level_object(&view_orient); + level_object(&camera.view_orient); break; case 2: // Control viewpoint object - if (!Objects[view_obj].flags[Object::Object_Flags::Locked_from_editing]) { - level_object(&Objects[view_obj].orient); - object_moved(&Objects[view_obj]); + if (!Objects[camera.getViewObj()].flags[Object::Object_Flags::Locked_from_editing]) { + level_object(&Objects[camera.getViewObj()].orient); + object_moved(&Objects[camera.getViewObj()]); ///! \todo Notify. editor->autosave("level object"); editor->missionChanged(); @@ -722,20 +587,20 @@ void EditorViewport::verticalize_controlled() { int cmode, count = 0; object* objp; - cmode = Control_mode; - if ((viewpoint == 1) && !cmode) { + cmode = camera.getControlMode(); + if ((camera.getViewpoint() == 1) && !cmode) { cmode = 2; } switch (cmode) { case 0: // Control the viewer's location and orientation - verticalize_object(&view_orient); + verticalize_object(&camera.view_orient); break; case 2: // Control viewpoint object - if (!Objects[view_obj].flags[Object::Object_Flags::Locked_from_editing]) { - verticalize_object(&Objects[view_obj].orient); - object_moved(&Objects[view_obj]); + if (!Objects[camera.getViewObj()].flags[Object::Object_Flags::Locked_from_editing]) { + verticalize_object(&Objects[camera.getViewObj()].orient); + object_moved(&Objects[camera.getViewObj()]); ///! \todo notify. editor->autosave("align object"); editor->missionChanged(); @@ -885,14 +750,14 @@ int EditorViewport::select_object(int cx, int cy) { return -1; } - p0 = view_pos; + p0 = camera.view_pos; vm_vec_scale_add(&p1, &p0, &v, 100.0f); for (auto objp = GET_FIRST(&obj_used_list); objp != END_OF_LIST(&obj_used_list); objp = GET_NEXT(objp)) { if (object_check_collision(objp, &p0, &p1, &hitpos)) { - hitpos.xyz.x = objp->pos.xyz.x - view_pos.xyz.x; - hitpos.xyz.y = objp->pos.xyz.y - view_pos.xyz.y; - hitpos.xyz.z = objp->pos.xyz.z - view_pos.xyz.z; + hitpos.xyz.x = objp->pos.xyz.x - camera.view_pos.xyz.x; + hitpos.xyz.y = objp->pos.xyz.y - camera.view_pos.xyz.y; + hitpos.xyz.z = objp->pos.xyz.z - camera.view_pos.xyz.z; dist = hitpos.xyz.x * hitpos.xyz.x + hitpos.xyz.y * hitpos.xyz.y + hitpos.xyz.z * hitpos.xyz.z; if (dist < best_dist) { best = OBJ_INDEX(objp); @@ -1312,10 +1177,10 @@ int EditorViewport::create_object(vec3d* pos, int waypoint_instance, bool create vec3d EditorViewport::getCreatePosition(int x, int y, float fallbackDist) { vec3d dir, pos; g3_point_to_vec_delayed(&dir, x, y); - if (fvi_ray_plane(&pos, &The_grid->center, &The_grid->gmatrix.vec.uvec, &view_pos, &dir, 0.0f) >= 0.0f) { + if (fvi_ray_plane(&pos, &The_grid->center, &The_grid->gmatrix.vec.uvec, &camera.view_pos, &dir, 0.0f) >= 0.0f) { return pos; } - vm_vec_scale_add(&pos, &view_pos, &view_orient.vec.fvec, fallbackDist); + vm_vec_scale_add(&pos, &camera.view_pos, &camera.view_orient.vec.fvec, fallbackDist); return pos; } @@ -1487,7 +1352,7 @@ int EditorViewport::drag_objects(int x, int y) */ // Do not move ships that we are currently centered around (Lookat_mode). The vector math will start going haywire and return NAN - if (!query_valid_object(editor->currentObject) || Lookat_mode) + if (!query_valid_object(editor->currentObject) || camera.getLookatMode()) return -1; if (Dup_drag == 1) { @@ -1526,11 +1391,11 @@ int EditorViewport::drag_objects(int x, int y) vec3d tmpObject = obj; tmpAnticonstraint.xyz.x = 0.0f; - r = fvi_ray_plane(&int_pnt, &tmpObject, &tmpAnticonstraint, &view_pos, &cursor_dir, 0.0f); + r = fvi_ray_plane(&int_pnt, &tmpObject, &tmpAnticonstraint, &camera.view_pos, &cursor_dir, 0.0f); // If intersected behind viewer, don't move. Too confusing, not what user wants. - vm_vec_sub(&vec1, &int_pnt, &view_pos); - vm_vec_sub(&vec2, &obj, &view_pos); + vm_vec_sub(&vec1, &int_pnt, &camera.view_pos); + vm_vec_sub(&vec2, &obj, &camera.view_pos); if ((r>=0.0f) && (vm_vec_dot(&vec1, &vec2) >= 0.0f)) { vec3d tmp1; vm_vec_sub( &tmp1, &int_pnt, &obj ); @@ -1544,11 +1409,11 @@ int EditorViewport::drag_objects(int x, int y) } else { // Move in x-z plane, defined by grid. Preserve height. - r = fvi_ray_plane(&int_pnt, &obj, &Anticonstraint, &view_pos, &cursor_dir, 0.0f); + r = fvi_ray_plane(&int_pnt, &obj, &Anticonstraint, &camera.view_pos, &cursor_dir, 0.0f); // If intersected behind viewer, don't move. Too confusing, not what user wants. - vm_vec_sub(&vec1, &int_pnt, &view_pos); - vm_vec_sub(&vec2, &obj, &view_pos); + vm_vec_sub(&vec1, &int_pnt, &camera.view_pos); + vm_vec_sub(&vec2, &obj, &camera.view_pos); if ((r>=0.0f) && (vm_vec_dot(&vec1, &vec2) >= 0.0f)) distance_moved = vm_vec_dist(&obj, &int_pnt); } @@ -1728,7 +1593,7 @@ void EditorViewport::cancel_drag() { void EditorViewport::view_universe(bool just_marked) { int max = 0; float dist, largest = 20.0f; - vec3d center, p1, p2; // center of all the objects collectively + vec3d center, p1, p2; vertex v; object *ptr; @@ -1778,8 +1643,8 @@ void EditorViewport::view_universe(bool just_marked) { } dist = fl_sqrt(largest) + 1.0f; - vm_vec_scale_add(&view_pos, ¢er, &view_orient.vec.fvec, -dist); - g3_set_view_matrix(&view_pos, &view_orient, 0.5f); + vm_vec_scale_add(&camera.view_pos, ¢er, &camera.view_orient.vec.fvec, -dist); + g3_set_view_matrix(&camera.view_pos, &camera.view_orient, 0.5f); ptr = GET_FIRST(&obj_used_list); while (ptr != END_OF_LIST(&obj_used_list)) { @@ -1789,10 +1654,10 @@ void EditorViewport::view_universe(bool just_marked) { if (g3_project_vertex(&v) & PF_OVERFLOW) Int3(); - while (v.codes & CC_OFF) { // is point off screen? - dist += 5.0f; // zoom out a little and check again. - vm_vec_scale_add(&view_pos, ¢er, &view_orient.vec.fvec, -dist); - g3_set_view_matrix(&view_pos, &view_orient, 0.5f); + while (v.codes & CC_OFF) { + dist += 5.0f; + vm_vec_scale_add(&camera.view_pos, ¢er, &camera.view_orient.vec.fvec, -dist); + g3_set_view_matrix(&camera.view_pos, &camera.view_orient, 0.5f); g3_rotate_vertex(&v, &ptr->pos); if (g3_project_vertex(&v) & PF_OVERFLOW) Int3(); @@ -1803,13 +1668,14 @@ void EditorViewport::view_universe(bool just_marked) { } dist *= 1.1f; - vm_vec_scale_add(&view_pos, ¢er, &view_orient.vec.fvec, -dist); - g3_set_view_matrix(&view_pos, &view_orient, 0.5f); + vm_vec_scale_add(&camera.view_pos, ¢er, &camera.view_orient.vec.fvec, -dist); + g3_set_view_matrix(&camera.view_pos, &camera.view_orient, 0.5f); needsUpdate(); } void EditorViewport::view_object(int obj_num) { - vm_vec_scale_add(&view_pos, &Objects[obj_num].pos, &view_orient.vec.fvec, Objects[obj_num].radius * -3.0f); + vm_vec_scale_add(&camera.view_pos, &Objects[obj_num].pos, &camera.view_orient.vec.fvec, + Objects[obj_num].radius * -3.0f); needsUpdate(); } diff --git a/qtfred/src/mission/EditorViewport.h b/qtfred/src/mission/EditorViewport.h index 4dceef9754f..ce19e1a49b4 100644 --- a/qtfred/src/mission/EditorViewport.h +++ b/qtfred/src/mission/EditorViewport.h @@ -1,6 +1,7 @@ #pragma once +#include "CameraController.h" #include "FredRenderer.h" #include "Editor.h" #include "IDialogProvider.h" @@ -49,26 +50,9 @@ struct ViewSettings { class EditorViewport { std::unique_ptr _renderer; //!< Internal, owned pointer - /** - * A lot of this stuff doesn't belong here - * @todo: Move camera stuff into own class - */ - int last_x = 0; - int last_y = 0; - - matrix my_orient = vmd_identity_matrix; - matrix trackball_orient = vmd_identity_matrix; - matrix Last_eye_orient = vmd_identity_matrix; - matrix Last_control_orient = vmd_identity_matrix; int Last_cursor_over = -1; - int Flying_controls_mode = 1; - - fix lasttime = 0; - - bool inc_mission_time(); void process_system_keys(); - void process_controls(vec3d* pos, matrix* orient, float frametime, int mode = 0); void level_object(matrix* orient); void initialSetup(); @@ -82,7 +66,23 @@ class EditorViewport { bool isLayerVisible(size_t layerIndex) const; void syncMissionLayerNames() const; void setObjectLayerByIndex(int objectIndex, size_t layerIndex); + public: + class ViewportControlLock { + public: + explicit ViewportControlLock(EditorViewport* viewport); + ~ViewportControlLock(); + + ViewportControlLock(const ViewportControlLock&) = delete; + ViewportControlLock& operator=(const ViewportControlLock&) = delete; + + ViewportControlLock(ViewportControlLock&& other) noexcept; + ViewportControlLock& operator=(ViewportControlLock&& other) noexcept; + + private: + EditorViewport* _viewport = nullptr; + }; + static const char* DefaultLayerName; enum { @@ -92,17 +92,15 @@ class EditorViewport { EditorViewport(Editor* in_editor, std::unique_ptr&& in_renderer); void needsUpdate(); + bool areControlsLocked() const; + [[nodiscard]] ViewportControlLock acquireControlLock(); - void resetView(); + void reset(); void select_objects(const Marking_box& box); - void resetViewPhysics(); - void game_do_frame(const int cur_object_index); - void move_mouse(int btn, int mdx, int mdy); - int object_check_collision(object* objp, vec3d* p0, vec3d* p1, vec3d* hitpos); int select_object(int cx, int cy); @@ -151,36 +149,19 @@ class EditorViewport { void view_object(int obj_num); - vec3d Last_eye_pos; - - vec3d eye_pos; - vec3d Last_control_pos = vmd_zero_vector; - vec3d my_pos; - matrix eye_orient; - control_info view_controls; + CameraController camera; ViewSettings view; int Cursor_over = -1; CursorMode Editing_mode = CursorMode::Moving; - matrix view_orient = vmd_identity_matrix; - vec3d view_pos; - physics_info view_physics; grid* The_grid; - int physics_speed = 1; - int physics_rot = 25; - vec3d Constraint; vec3d Anticonstraint; bool Single_axis_constraint = false; - int viewpoint = 0; - int view_obj = -1; - - int Control_mode = 0; - bool Selection_lock = false; bool button_down = false; @@ -192,9 +173,6 @@ class EditorViewport { object_orient_pos rotation_backup[MAX_OBJECTS]; - vec3d saved_cam_pos = vmd_zero_vector; - matrix saved_cam_orient; - vec3d original_pos = vmd_zero_vector; bool moved = false; @@ -202,7 +180,6 @@ class EditorViewport { int Duped_wing; bool Group_rotate = true; - bool Lookat_mode = false; int toolbar_icon_size = 24; ///< Toolbar icon size in pixels (16, 24, or 32) bool Offer_autosave_recovery = true; bool Move_ships_when_undocking = true; @@ -225,7 +202,16 @@ class EditorViewport { IDialogProvider* dialogProvider = nullptr; private: + fix _lasttime = 0; + vec3d Last_control_pos = vmd_zero_vector; + matrix Last_control_orient = vmd_identity_matrix; + int _controlLockCount = 0; + + bool incMissionTime(); void loadSettings(); + + void lockControls(); + void unlockControls(); }; } // namespace fso::fred diff --git a/qtfred/src/mission/FredRenderer.cpp b/qtfred/src/mission/FredRenderer.cpp index 8ff7505f4b9..ead4409f1ba 100644 --- a/qtfred/src/mission/FredRenderer.cpp +++ b/qtfred/src/mission/FredRenderer.cpp @@ -569,8 +569,8 @@ void FredRenderer::render_compass() { gr_set_clip(gr_screen.max_w - 100, 0, 100, 100); g3_start_frame(0); // ** Accounted for // required !!! - vm_vec_scale_add2(&eye, &_viewport->eye_orient.vec.fvec, -1.5f); - g3_set_view_matrix(&eye, &_viewport->eye_orient, 1.0f); + vm_vec_scale_add2(&eye, &_viewport->camera.eye_orient.vec.fvec, -1.5f); + g3_set_view_matrix(&eye, &_viewport->camera.eye_orient, 1.0f); v.xyz.x = 1.0f; v.xyz.y = v.xyz.z = 0.0f; @@ -866,7 +866,7 @@ void FredRenderer::render_one_model_htl(object* objp, } else Assert(0); - float size = fl_sqrt(vm_vec_dist(&_viewport->eye_pos, &objp->pos) / 20.0f); + float size = fl_sqrt(vm_vec_dist(&_viewport->camera.eye_pos, &objp->pos) / 20.0f); if (size < LOLLIPOP_SIZE) { size = LOLLIPOP_SIZE; @@ -952,7 +952,7 @@ void FredRenderer::render_frame(int cur_object_index, font::set_font(font::FONT1); light_reset(); - g3_set_view_matrix(&_viewport->eye_pos, &_viewport->eye_orient, 0.5f); + g3_set_view_matrix(&_viewport->camera.eye_pos, &_viewport->camera.eye_orient, 0.5f); // Force max star detail so the editor always shows the full Num_stars count // regardless of the player's graphics quality setting (Detail.num_stars can be 0). @@ -1060,7 +1060,7 @@ void FredRenderer::render_frame(int cur_object_index, } disable_htl(); - sprintf(buf, "(%.1f,%.1f,%.1f)", _viewport->eye_pos.xyz.x, _viewport->eye_pos.xyz.y, _viewport->eye_pos.xyz.z); + sprintf(buf, "(%.1f,%.1f,%.1f)", _viewport->camera.eye_pos.xyz.x, _viewport->camera.eye_pos.xyz.y, _viewport->camera.eye_pos.xyz.z); gr_get_string_size(&w, &h, buf); gr_set_color_fast(&colour_white); gr_string(gr_screen.max_w - w - 2, 2, buf); @@ -1082,7 +1082,7 @@ void FredRenderer::render_frame(int cur_object_index, gr_reset_clip(); g3_start_frame(0); // ** Accounted for - g3_set_view_matrix(&_viewport->eye_pos, &_viewport->eye_orient, 0.5f); + g3_set_view_matrix(&_viewport->camera.eye_pos, &_viewport->camera.eye_orient, 0.5f); } void FredRenderer::resize(int width, int height) { // Make sure the following call targets the right view port diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index c186c79737d..52b2291cf90 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -206,9 +206,8 @@ void FredView::setEditor(Editor* editor, EditorViewport* viewport) { QSettings settings; _tbLocalMove = settings.value("FredView/transformLocalMove", false).toBool(); _tbLocalRotate = settings.value("FredView/transformLocalRotate", false).toBool(); - _viewport->physics_speed = settings.value("FredView/cameraSpeedMove", 1).toInt(); - _viewport->physics_rot = settings.value("FredView/cameraSpeedRot", 25).toInt(); - _viewport->resetViewPhysics(); + _viewport->camera.setPhysicsSpeed(settings.value("FredView/cameraSpeedMove", 1).toInt()); + _viewport->camera.setPhysicsRot(settings.value("FredView/cameraSpeedRot", 25).toInt()); } connect(fred, &Editor::missionLoaded, this, &FredView::on_mission_loaded); @@ -235,11 +234,11 @@ void FredView::setEditor(Editor* editor, EditorViewport* viewport) { &FredView::viewIdle, this, [this]() { ui->actionZoomSelected->setEnabled(query_valid_object(fred->currentObject)); }); - connect(this, &FredView::viewIdle, this, [this]() { ui->actionOrbitSelected->setChecked(_viewport->Lookat_mode); }); + connect(this, &FredView::viewIdle, this, [this]() { ui->actionOrbitSelected->setChecked(_viewport->camera.getLookatMode()); }); connect(this, &FredView::viewIdle, this, - [this]() { ui->actionRestore_Camera_Pos->setEnabled(!IS_VEC_NULL(&_viewport->saved_cam_orient.vec.fvec)); }); + [this]() { ui->actionRestore_Camera_Pos->setEnabled(_viewport->camera.hasSavedPosition()); }); connect(this, &FredView::viewIdle, this, [this]() { ui->actionRevert->setEnabled(!saveName.isEmpty()); }); connect(this, &FredView::viewIdle, this, [this]() { ui->actionUndo->setEnabled(fred->undoAvailable != 0); }); connect(this, &FredView::viewIdle, this, [this]() { ui->actionDisable_Undo->setChecked(fred->autosaveDisabled != 0); }); @@ -342,8 +341,8 @@ bool FredView::saveMissionToCurrentPath() { Fred_mission_save save; save.set_save_format(_missionSaveFormat); save.set_always_save_display_names(_viewport->Always_save_display_names); - save.set_view_pos(_viewport->view_pos); - save.set_view_orient(_viewport->view_orient); + save.set_view_pos(_viewport->camera.view_pos); + save.set_view_orient(_viewport->camera.view_orient); save.set_fred_alt_names(Fred_alt_names); save.set_fred_callsigns(Fred_callsigns); @@ -359,8 +358,8 @@ bool FredView::saveMissionAs() { Fred_mission_save save; save.set_save_format(_missionSaveFormat); save.set_always_save_display_names(_viewport->Always_save_display_names); - save.set_view_pos(_viewport->view_pos); - save.set_view_orient(_viewport->view_orient); + save.set_view_pos(_viewport->camera.view_pos); + save.set_view_orient(_viewport->camera.view_orient); save.set_fred_alt_names(Fred_alt_names); save.set_fred_callsigns(Fred_callsigns); @@ -464,14 +463,14 @@ void FredView::on_actionRevert_triggered(bool) { void FredView::on_actionUndo_triggered(bool) { // Preserve camera state and saveName because autoload() triggers missionLoaded which would overwrite them - auto savedViewPos = _viewport->view_pos; - auto savedViewOrient = _viewport->view_orient; + auto savedViewPos = _viewport->camera.view_pos; + auto savedViewOrient = _viewport->camera.view_orient; auto savedSaveName = saveName; fred->autoload(); - _viewport->view_pos = savedViewPos; - _viewport->view_orient = savedViewOrient; + _viewport->camera.view_pos = savedViewPos; + _viewport->camera.view_orient = savedViewOrient; saveName = savedSaveName; } @@ -544,8 +543,8 @@ void FredView::on_actionFS1_Mission_triggered(bool) { Fred_mission_save fileSave; fileSave.set_save_format(_missionSaveFormat); fileSave.set_always_save_display_names(_viewport->Always_save_display_names); - fileSave.set_view_pos(_viewport->view_pos); - fileSave.set_view_orient(_viewport->view_orient); + fileSave.set_view_pos(_viewport->camera.view_pos); + fileSave.set_view_orient(_viewport->camera.view_orient); fileSave.set_fred_alt_names(Fred_alt_names); fileSave.set_fred_callsigns(Fred_callsigns); @@ -1024,8 +1023,7 @@ void FredView::initializeTransformBar() { _transformToolBar->addWidget(_transformMoveSpeedCombo); connect(_transformMoveSpeedCombo, QOverload::of(&QComboBox::activated), this, [this](int idx) { if (!_viewport) return; - _viewport->physics_speed = _transformMoveSpeedCombo->itemData(idx).toInt(); - _viewport->resetViewPhysics(); + _viewport->camera.setPhysicsSpeed(_transformMoveSpeedCombo->itemData(idx).toInt()); }); addFixedSpacer(8); @@ -1044,8 +1042,7 @@ void FredView::initializeTransformBar() { _transformToolBar->addWidget(_transformRotSpeedCombo); connect(_transformRotSpeedCombo, QOverload::of(&QComboBox::activated), this, [this](int idx) { if (!_viewport) return; - _viewport->physics_rot = _transformRotSpeedCombo->itemData(idx).toInt(); - _viewport->resetViewPhysics(); + _viewport->camera.setPhysicsRot(_transformRotSpeedCombo->itemData(idx).toInt()); }); addFixedSpacer(8); @@ -1420,8 +1417,8 @@ void FredView::updateUI() { _statusBarUnitsLabel->setText(tr("Units = %1 Meters").arg(_viewport->The_grid->square_size)); setWindowModified(isMissionModified()); - if (_viewport->viewpoint == 1) { - _statusBarViewmode->setText(tr("Viewpoint: %1").arg(object_name(_viewport->view_obj))); + if (_viewport->camera.getViewpoint() == 1) { + _statusBarViewmode->setText(tr("Viewpoint: %1").arg(object_name(_viewport->camera.getViewObj()))); } else { _statusBarViewmode->setText(tr("Viewpoint: Camera")); } @@ -2040,8 +2037,8 @@ void FredView::closeEvent(QCloseEvent* event) { settings.setValue("FredView/transformLocalMove", _tbLocalMove); settings.setValue("FredView/transformLocalRotate", _tbLocalRotate); if (_viewport) { - settings.setValue("FredView/cameraSpeedMove", _viewport->physics_speed); - settings.setValue("FredView/cameraSpeedRot", _viewport->physics_rot); + settings.setValue("FredView/cameraSpeedMove", _viewport->camera.getPhysicsSpeed()); + settings.setValue("FredView/cameraSpeedRot", _viewport->camera.getPhysicsRot()); } if (!maybePromptToSaveMissionChanges(tr("closing QtFRED"))) { @@ -2070,26 +2067,26 @@ void FredView::on_actionUnlock_All_Objects_triggered(bool /*enabled*/) { fred->unlockAllObjects(); } void FredView::onUpdateViewSpeeds() { - ui->actionx1->setChecked(_viewport->physics_speed == 1); - ui->actionx2->setChecked(_viewport->physics_speed == 2); - ui->actionx3->setChecked(_viewport->physics_speed == 3); - ui->actionx5->setChecked(_viewport->physics_speed == 5); - ui->actionx8->setChecked(_viewport->physics_speed == 8); - ui->actionx10->setChecked(_viewport->physics_speed == 10); - ui->actionx50->setChecked(_viewport->physics_speed == 50); - ui->actionx100->setChecked(_viewport->physics_speed == 100); - - ui->actionRotx1->setChecked(_viewport->physics_rot == 2); - ui->actionRotx5->setChecked(_viewport->physics_rot == 10); - ui->actionRotx12->setChecked(_viewport->physics_rot == 25); - ui->actionRotx25->setChecked(_viewport->physics_rot == 50); - ui->actionRotx50->setChecked(_viewport->physics_rot == 100); + ui->actionx1->setChecked(_viewport->camera.getPhysicsSpeed() == 1); + ui->actionx2->setChecked(_viewport->camera.getPhysicsSpeed() == 2); + ui->actionx3->setChecked(_viewport->camera.getPhysicsSpeed() == 3); + ui->actionx5->setChecked(_viewport->camera.getPhysicsSpeed() == 5); + ui->actionx8->setChecked(_viewport->camera.getPhysicsSpeed() == 8); + ui->actionx10->setChecked(_viewport->camera.getPhysicsSpeed() == 10); + ui->actionx50->setChecked(_viewport->camera.getPhysicsSpeed() == 50); + ui->actionx100->setChecked(_viewport->camera.getPhysicsSpeed() == 100); + + ui->actionRotx1->setChecked(_viewport->camera.getPhysicsRot() == 2); + ui->actionRotx5->setChecked(_viewport->camera.getPhysicsRot() == 10); + ui->actionRotx12->setChecked(_viewport->camera.getPhysicsRot() == 25); + ui->actionRotx25->setChecked(_viewport->camera.getPhysicsRot() == 50); + ui->actionRotx50->setChecked(_viewport->camera.getPhysicsRot() == 100); // Keep the bottom-bar combos in sync (covers changes made via keyboard shortcuts or menu). if (_transformMoveSpeedCombo) { QSignalBlocker bl(_transformMoveSpeedCombo); for (int i = 0; i < _transformMoveSpeedCombo->count(); ++i) { - if (_transformMoveSpeedCombo->itemData(i).toInt() == _viewport->physics_speed) { + if (_transformMoveSpeedCombo->itemData(i).toInt() == _viewport->camera.getPhysicsSpeed()) { _transformMoveSpeedCombo->setCurrentIndex(i); break; } @@ -2098,7 +2095,7 @@ void FredView::onUpdateViewSpeeds() { if (_transformRotSpeedCombo) { QSignalBlocker bl(_transformRotSpeedCombo); for (int i = 0; i < _transformRotSpeedCombo->count(); ++i) { - if (_transformRotSpeedCombo->itemData(i).toInt() == _viewport->physics_rot) { + if (_transformRotSpeedCombo->itemData(i).toInt() == _viewport->camera.getPhysicsRot()) { _transformRotSpeedCombo->setCurrentIndex(i); break; } @@ -2107,112 +2104,99 @@ void FredView::onUpdateViewSpeeds() { } void FredView::on_actionx1_triggered(bool enabled) { if (enabled) { - _viewport->physics_speed = 1; - _viewport->resetViewPhysics(); + _viewport->camera.setPhysicsSpeed(1); } } void FredView::on_actionx2_triggered(bool enabled) { if (enabled) { - _viewport->physics_speed = 2; - _viewport->resetViewPhysics(); + _viewport->camera.setPhysicsSpeed(2); } } void FredView::on_actionx3_triggered(bool enabled) { if (enabled) { - _viewport->physics_speed = 3; - _viewport->resetViewPhysics(); + _viewport->camera.setPhysicsSpeed(3); } } void FredView::on_actionx5_triggered(bool enabled) { if (enabled) { - _viewport->physics_speed = 5; - _viewport->resetViewPhysics(); + _viewport->camera.setPhysicsSpeed(5); } } void FredView::on_actionx8_triggered(bool enabled) { if (enabled) { - _viewport->physics_speed = 8; - _viewport->resetViewPhysics(); + _viewport->camera.setPhysicsSpeed(8); } } void FredView::on_actionx10_triggered(bool enabled) { if (enabled) { - _viewport->physics_speed = 10; - _viewport->resetViewPhysics(); + _viewport->camera.setPhysicsSpeed(10); } } void FredView::on_actionx50_triggered(bool enabled) { if (enabled) { - _viewport->physics_speed = 50; - _viewport->resetViewPhysics(); + _viewport->camera.setPhysicsSpeed(50); } } void FredView::on_actionx100_triggered(bool enabled) { if (enabled) { - _viewport->physics_speed = 100; - _viewport->resetViewPhysics(); + _viewport->camera.setPhysicsSpeed(100); } } void FredView::on_actionRotx1_triggered(bool enabled) { if (enabled) { - _viewport->physics_rot = 2; - _viewport->resetViewPhysics(); + _viewport->camera.setPhysicsRot(2); } } void FredView::on_actionRotx5_triggered(bool enabled) { if (enabled) { - _viewport->physics_rot = 10; - _viewport->resetViewPhysics(); + _viewport->camera.setPhysicsRot(10); } } void FredView::on_actionRotx12_triggered(bool enabled) { if (enabled) { - _viewport->physics_rot = 25; - _viewport->resetViewPhysics(); + _viewport->camera.setPhysicsRot(25); } } void FredView::on_actionRotx25_triggered(bool enabled) { if (enabled) { - _viewport->physics_rot = 50; - _viewport->resetViewPhysics(); + _viewport->camera.setPhysicsRot(50); } } void FredView::on_actionRotx50_triggered(bool enabled) { if (enabled) { - _viewport->physics_rot = 100; - _viewport->resetViewPhysics(); + _viewport->camera.setPhysicsRot(100); } } void FredView::onUpdateCameraControlActions() { - ui->actionCamera->setChecked(_viewport->viewpoint == 0); - ui->actionCurrent_Ship->setChecked(_viewport->viewpoint == 1); + ui->actionCamera->setChecked(_viewport->camera.getViewpoint() == 0); + ui->actionCurrent_Ship->setChecked(_viewport->camera.getViewpoint() == 1); - _controlModeCamera->setChecked(_viewport->Control_mode == 0); - _controlModeCurrentShip->setChecked(_viewport->Control_mode == 1); + _controlModeCamera->setChecked(_viewport->camera.getControlMode() == 0); + _controlModeCurrentShip->setChecked(_viewport->camera.getControlMode() == 1); } void FredView::on_actionCamera_triggered(bool enabled) { if (enabled) { - _viewport->viewpoint = 0; + _viewport->camera.setViewpoint(0); _viewport->needsUpdate(); } } void FredView::on_actionCurrent_Ship_triggered(bool enabled) { if (enabled) { - _viewport->viewpoint = 1; - _viewport->view_obj = fred->currentObject; + _viewport->camera.setViewpoint(1); + _viewport->camera.setViewObj(fred->currentObject); _viewport->needsUpdate(); } } void FredView::on_actionControlModeCamera_triggered(bool enabled) { if (enabled) { - _viewport->Control_mode = 0; + _viewport->camera.setControlMode(0); } } void FredView::on_actionControlModeCurrentShip_triggered(bool enabled) { if (enabled) { - _viewport->Control_mode = 1; + _viewport->camera.setControlMode(1); } } @@ -2638,28 +2622,25 @@ void FredView::on_actionSelectionList_triggered(bool checked) { } } void FredView::on_actionOrbitSelected_triggered(bool enabled) { - _viewport->Lookat_mode = enabled; - if (_viewport->Lookat_mode && query_valid_object(fred->currentObject)) { + _viewport->camera.setLookatMode(enabled); + if (_viewport->camera.getLookatMode() && query_valid_object(fred->currentObject)) { vec3d v, loc; matrix m; loc = Objects[fred->currentObject].pos; - vm_vec_sub(&v, &loc, &_viewport->view_pos); + vm_vec_sub(&v, &loc, &_viewport->camera.view_pos); if (v.xyz.x || v.xyz.y || v.xyz.z) { vm_vector_2_matrix(&m, &v, NULL, NULL); - _viewport->view_orient = m; + _viewport->camera.view_orient = m; } } } void FredView::on_actionSave_Camera_Pos_triggered(bool) { - _viewport->saved_cam_pos = _viewport->view_pos; - _viewport->saved_cam_orient = _viewport->view_orient; + _viewport->camera.savePosition(); } void FredView::on_actionRestore_Camera_Pos_triggered(bool) { - _viewport->view_pos = _viewport->saved_cam_pos; - _viewport->view_orient = _viewport->saved_cam_orient; - + _viewport->camera.restorePosition(); _viewport->needsUpdate(); } void FredView::on_actionClone_Marked_Objects_triggered(bool) { @@ -2745,7 +2726,7 @@ void FredView::onSetGroup(int group) { fred->updateAllViewports(); } void FredView::on_actionControl_Object_triggered(bool) { - _viewport->Control_mode = (_viewport->Control_mode + 1) % 2; + _viewport->camera.toggleControlMode(); } void FredView::on_actionLevel_Object_triggered(bool) { _viewport->level_controlled(); diff --git a/qtfred/src/ui/dialogs/BriefingEditorDialog.cpp b/qtfred/src/ui/dialogs/BriefingEditorDialog.cpp index 695c53d5e87..6b56b2294ad 100644 --- a/qtfred/src/ui/dialogs/BriefingEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/BriefingEditorDialog.cpp @@ -15,7 +15,6 @@ #include #include -#include #include #include @@ -27,8 +26,6 @@ namespace fso::fred::dialogs { -int BriefingEditorDialog::_openDialogCount = 0; - namespace { float movementSpeedScaleForIndex(int index) { switch (index) { @@ -78,7 +75,7 @@ BriefingEditorDialog::BriefingEditorDialog(FredView* parent, EditorViewport* vie : QDialog(parent), SexpTreeEditorInterface(flagset()), ui(new Ui::BriefingEditorDialog()), _model(new BriefingEditorDialogModel(this, viewport)), _viewport(viewport) { - ++_openDialogCount; + _viewportLock.emplace(_viewport->acquireControlLock()); this->setFocus(); ui->setupUi(this); @@ -100,11 +97,7 @@ BriefingEditorDialog::BriefingEditorDialog(FredView* parent, EditorViewport* vie } BriefingEditorDialog::~BriefingEditorDialog() { - _openDialogCount = std::max(0, _openDialogCount - 1); -} - -bool BriefingEditorDialog::isAnyDialogOpen() { - return _openDialogCount > 0; + _viewportLock.reset(); } void BriefingEditorDialog::accept() @@ -115,6 +108,7 @@ void BriefingEditorDialog::accept() ui->defaultMusicWidget->stopPlayback(); ui->musicPackWidget->stopPlayback(); QDialog::accept(); + _viewportLock.reset(); // unlock before restoring the grid so the viewport can process controls again create_default_grid(); // restore the grid back to the normal version } // else: validation failed, don't close @@ -129,6 +123,7 @@ void BriefingEditorDialog::reject() ui->defaultMusicWidget->stopPlayback(); ui->musicPackWidget->stopPlayback(); QDialog::reject(); // actually close + _viewportLock.reset(); // unlock before restoring the grid so the viewport can process controls again create_default_grid(); // restore the grid back to the normal version } // else: do nothing, don't close diff --git a/qtfred/src/ui/dialogs/BriefingEditorDialog.h b/qtfred/src/ui/dialogs/BriefingEditorDialog.h index 1bed0b4cc63..95d24d42ea3 100644 --- a/qtfred/src/ui/dialogs/BriefingEditorDialog.h +++ b/qtfred/src/ui/dialogs/BriefingEditorDialog.h @@ -24,7 +24,6 @@ class BriefingEditorDialog : public QDialog, public SexpTreeEditorInterface { public: explicit BriefingEditorDialog(FredView* parent, EditorViewport* viewport); ~BriefingEditorDialog() override; - static bool isAnyDialogOpen(); void accept() override; void reject() override; @@ -95,7 +94,7 @@ class BriefingEditorDialog : public QDialog, public SexpTreeEditorInterface { std::unique_ptr _model; EditorViewport* _viewport; fso::fred::BriefingMapWidget* _mapWidget = nullptr; - static int _openDialogCount; + std::optional _viewportLock; void initializeUi(); void setupMapWidget(); diff --git a/qtfred/src/ui/widgets/BriefingMapWidget.cpp b/qtfred/src/ui/widgets/BriefingMapWidget.cpp index 58154393ac3..92f5d3fedb0 100644 --- a/qtfred/src/ui/widgets/BriefingMapWidget.cpp +++ b/qtfred/src/ui/widgets/BriefingMapWidget.cpp @@ -191,6 +191,11 @@ void BriefingMapWidget::initBriefingMap() { Briefing = savedBriefing; } + // Initialize the camera controller physics so it is ready before the first frame. + // The dialog's combo-box handlers will call setMovementSpeedScale/setRotationSpeedScale + // after the widget is shown, which will re-apply the correct speed values. + _cameraController.resetViewPhysics(); + _initialized = true; _renderTimer->start(); mprintf(("BriefingMapWidget: init complete, timer started for render diagnostics.\n")); @@ -277,6 +282,7 @@ void BriefingMapWidget::applyStageTransition(int stageNum, int transitionTime) { Brief_text_wipe_time_elapsed = BRIEF_TEXT_WIPE_TIME + 1.0f; brief_reset_icons(stageNum); _currentStage = stageNum; + _cameraController.resetViewPhysics(); // clear residual velocity from previous stage Briefing = savedBriefing; } @@ -427,11 +433,13 @@ void BriefingMapWidget::applyCameraToCurrentStage(const vec3d& pos, const matrix } void BriefingMapWidget::setMovementSpeedScale(float scale) { - _movementSpeedScale = std::max(0.01f, scale); + // Dialog sends 4.0, 8.0, 16.0; map to physicsSpeed 1, 2, 4 preserving the 1:2:4 ratio. + _cameraController.setPhysicsSpeed(std::max(1, fl2ir(scale / 4.0f))); } void BriefingMapWidget::setRotationSpeedScale(float scale) { - _rotationSpeedScale = std::max(0.01f, scale); + // Dialog sends 0.0625, 0.125, 0.25; map to physicsRot 8, 15, 30 (max_rotvel *= physicsRot/30). + _cameraController.setPhysicsRot(std::max(1, fl2ir(scale * 120.0f))); } void BriefingMapWidget::applyCameraPoseLikeKeyboardControls(const vec3d& camPos, const matrix& camOrient, bool updateModel) { @@ -521,7 +529,7 @@ void BriefingMapWidget::renderFrame() { gr_use_viewport(mainView); gr_screen_resize(static_cast(mainSize.first), static_cast(mainSize.second)); g3_start_frame(0); - g3_set_view_matrix(&_viewport->eye_pos, &_viewport->eye_orient, 0.5f); + g3_set_view_matrix(&_viewport->camera.eye_pos, &_viewport->camera.eye_orient, 0.5f); } _rendering = false; return; @@ -611,7 +619,7 @@ void BriefingMapWidget::renderFrame() { gr_use_viewport(mainView); gr_screen_resize(static_cast(mainSize.first), static_cast(mainSize.second)); g3_start_frame(0); - g3_set_view_matrix(&_viewport->eye_pos, &_viewport->eye_orient, 0.5f); + g3_set_view_matrix(&_viewport->camera.eye_pos, &_viewport->camera.eye_orient, 0.5f); } _rendering = false; @@ -656,47 +664,21 @@ void BriefingMapWidget::keyReleaseEvent(QKeyEvent* event) { } void BriefingMapWidget::applyBoundCameraControls(float frametime) { - auto& bindings = ControlBindings::instance(); - vec3d camPos = brief_get_current_cam_pos(); - matrix camOrient = brief_get_current_cam_orient(); - const auto oldPos = camPos; - const auto oldOrient = camOrient; - - vec3d movementVec = ZERO_VECTOR; - angles rotangs{}; - - movementVec.xyz.x += bindings.isPressed(ControlAction::MoveLeft) ? -1.0f : 0.0f; - movementVec.xyz.x += bindings.isPressed(ControlAction::MoveRight) ? 1.0f : 0.0f; - movementVec.xyz.y += bindings.isPressed(ControlAction::MoveForward) ? 1.0f : 0.0f; - movementVec.xyz.y += bindings.isPressed(ControlAction::MoveBackward) ? -1.0f : 0.0f; - movementVec.xyz.z += bindings.isPressed(ControlAction::MoveUp) ? 1.0f : 0.0f; - movementVec.xyz.z += bindings.isPressed(ControlAction::MoveDown) ? -1.0f : 0.0f; - rotangs.h += bindings.isPressed(ControlAction::YawLeft) ? -0.1f * _rotationSpeedScale : 0.0f; - rotangs.h += bindings.isPressed(ControlAction::YawRight) ? 0.1f * _rotationSpeedScale : 0.0f; - rotangs.p += bindings.isPressed(ControlAction::PitchUp) ? -0.1f * _rotationSpeedScale : 0.0f; - rotangs.p += bindings.isPressed(ControlAction::PitchDown) ? 0.1f * _rotationSpeedScale : 0.0f; - - const auto frameScale = std::max(frametime * 30.0f * _movementSpeedScale, 0.0f); - if (movementVec.xyz.x != 0.0f) { - vm_vec_scale_add2(&camPos, &camOrient.vec.rvec, movementVec.xyz.x * frameScale); - } - if (movementVec.xyz.y != 0.0f) { - vm_vec_scale_add2(&camPos, &camOrient.vec.fvec, movementVec.xyz.y * frameScale); - } - if (movementVec.xyz.z != 0.0f) { - vm_vec_scale_add2(&camPos, &camOrient.vec.uvec, movementVec.xyz.z * frameScale); - } - - if (rotangs.p != 0.0f || rotangs.h != 0.0f || rotangs.b != 0.0f) { - matrix rotmat; - matrix newmat; - vm_angles_2_matrix(&rotmat, &rotangs); - vm_matrix_x_matrix(&newmat, &camOrient, &rotmat); - camOrient = newmat; - } - - if (vm_vec_cmp(&oldPos, &camPos) || vm_matrix_cmp(&oldOrient, &camOrient)) { - applyCameraPoseLikeKeyboardControls(camPos, camOrient, true); + // Sync from briefing globals each frame so externally-driven moves + // (stage transitions, paste, coordinates dialog) are picked up before + // applying user input via the shared CameraController. + _cameraController.view_pos = brief_get_current_cam_pos(); + _cameraController.view_orient = brief_get_current_cam_orient(); + + if (_cameraController.processControls( + &_cameraController.view_pos, + &_cameraController.view_orient, + frametime, + false)) { + applyCameraPoseLikeKeyboardControls( + _cameraController.view_pos, + _cameraController.view_orient, + true); } } diff --git a/qtfred/src/ui/widgets/BriefingMapWidget.h b/qtfred/src/ui/widgets/BriefingMapWidget.h index bdd0835ed44..3bdcc3d4d99 100644 --- a/qtfred/src/ui/widgets/BriefingMapWidget.h +++ b/qtfred/src/ui/widgets/BriefingMapWidget.h @@ -5,6 +5,7 @@ #include #include "globalincs/pstypes.h" +#include "mission/CameraController.h" #include "osapi/osapi.h" #include "ui/QtGraphicsOperations.h" @@ -92,6 +93,8 @@ class BriefingMapWidget : public QWidget { void applyCameraPoseLikeKeyboardControls(const vec3d& camPos, const matrix& camOrient, bool updateModel); void applyBoundCameraControls(float frametime); + CameraController _cameraController; + BriefingMapWindow* _window = nullptr; QTimer* _renderTimer = nullptr; std::unique_ptr _briefingViewport; // our os::Viewport for gr_use_viewport @@ -126,8 +129,6 @@ class BriefingMapWidget : public QWidget { int _cutFadeFrame = 0; int _pendingCutStage = -1; - float _movementSpeedScale = 1.0f; - float _rotationSpeedScale = 1.0f; }; } // namespace fso::fred From 5f6309be49c424189934188cbd881dce29a11632 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 9 May 2026 09:50:51 -0500 Subject: [PATCH 30/65] make prop dialog true direct edit (#7418) --- .../mission/dialogs/PropEditorDialogModel.cpp | 164 ++++++++---------- .../mission/dialogs/PropEditorDialogModel.h | 3 +- qtfred/src/ui/dialogs/PropEditorDialog.cpp | 13 +- 3 files changed, 75 insertions(+), 105 deletions(-) diff --git a/qtfred/src/mission/dialogs/PropEditorDialogModel.cpp b/qtfred/src/mission/dialogs/PropEditorDialogModel.cpp index 653cd6d6b1d..035bcc67814 100644 --- a/qtfred/src/mission/dialogs/PropEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/PropEditorDialogModel.cpp @@ -17,54 +17,11 @@ PropEditorDialogModel::PropEditorDialogModel(QObject* parent, EditorViewport* vi } bool PropEditorDialogModel::apply() { - if (!hasValidSelection()) { - return true; - } - - if (!validateData()) { - return false; - } - - for (auto obj_idx : _selectedPropObjects) { - if (!query_valid_object(obj_idx) || Objects[obj_idx].type != OBJ_PROP) { - continue; - } - - auto instance = Objects[obj_idx].instance; - auto prp = prop_id_lookup(instance); - if (prp == nullptr) { - continue; - } - - if (!hasMultipleSelection()) { - strcpy_s(prp->prop_name, _propName.c_str()); - } - - for (size_t i = 0; i < _flagLabels.size(); ++i) { - auto state = _flagState[i]; - if (state == Qt::PartiallyChecked) { - continue; - } - - auto flag_index = _flagLabels[i].second; - if (flag_index >= Num_parse_prop_flags) { - continue; - } - - auto& def = Parse_prop_flags[flag_index]; - if (!stricmp(def.name, "no_collide")) { - Objects[obj_idx].flags.set(Object::Object_Flags::Collides, state != Qt::Checked); - } - } - } - - _editor->missionChanged(); return true; } -void PropEditorDialogModel::reject() { - // no-op -} +void PropEditorDialogModel::reject() {} + void PropEditorDialogModel::initializeData() { _flagLabels.clear(); @@ -109,40 +66,6 @@ void PropEditorDialogModel::initializeData() { _modified = false; } -bool PropEditorDialogModel::validateData() { - _bypass_errors = false; - - if (hasMultipleSelection()) { - // Name is not editable for multi-select and only flags are applied. - return true; - } - - SCP_trim(_propName); - if (_propName.empty()) { - showErrorDialogNoCancel("A prop name cannot be empty."); - return false; - } - - std::unordered_set selected_instances; - for (auto obj_idx : _selectedPropObjects) { - if (query_valid_object(obj_idx) && Objects[obj_idx].type == OBJ_PROP) { - selected_instances.insert(Objects[obj_idx].instance); - } - } - - for (size_t i = 0; i < Props.size(); ++i) { - if (selected_instances.find(static_cast(i)) != selected_instances.end() || !Props[i].has_value()) { - continue; - } - - if (!stricmp(_propName.c_str(), Props[i].value().prop_name)) { - showErrorDialogNoCancel("This prop name is already being used by another prop."); - return false; - } - } - - return true; -} void PropEditorDialogModel::showErrorDialogNoCancel(const SCP_string& message) { if (_bypass_errors) { @@ -254,11 +177,49 @@ const SCP_string& PropEditorDialogModel::getPropName() const { return _propName; } -void PropEditorDialogModel::setPropName(const SCP_string& name) { +bool PropEditorDialogModel::setPropName(const SCP_string& name) { if (hasMultipleSelection()) { - return; + return true; + } + + _bypass_errors = false; + + SCP_string trimmed = name; + SCP_trim(trimmed); + + if (trimmed.empty()) { + showErrorDialogNoCancel("A prop name cannot be empty."); + return false; + } + + std::unordered_set selected_instances; + for (auto obj_idx : _selectedPropObjects) { + if (query_valid_object(obj_idx) && Objects[obj_idx].type == OBJ_PROP) { + selected_instances.insert(Objects[obj_idx].instance); + } + } + + for (size_t i = 0; i < Props.size(); ++i) { + if (selected_instances.find(static_cast(i)) != selected_instances.end() || !Props[i].has_value()) { + continue; + } + if (!stricmp(trimmed.c_str(), Props[i].value().prop_name)) { + showErrorDialogNoCancel("This prop name is already being used by another prop."); + return false; + } + } + + auto obj_idx = _selectedPropObjects.front(); + auto prp = prop_id_lookup(Objects[obj_idx].instance); + if (prp == nullptr) { + return false; } - modify(_propName, name); + + strcpy_s(prp->prop_name, trimmed.c_str()); + _propName = trimmed; + set_modified(); + _editor->missionChanged(); + return true; } const SCP_vector>& PropEditorDialogModel::getFlagLabels() const { @@ -274,11 +235,32 @@ void PropEditorDialogModel::setFlagState(size_t index, int state) { return; } - if (_flagState[index] != state) { - _flagState[index] = state; - set_modified(); - Q_EMIT modelChanged(); + if (_flagState[index] == state) { + return; + } + + _flagState[index] = state; + + if (state == Qt::PartiallyChecked) { + return; + } + + auto flag_index = _flagLabels[index].second; + if (flag_index >= Num_parse_prop_flags) { + return; + } + + auto& def = Parse_prop_flags[flag_index]; + for (auto obj_idx : _selectedPropObjects) { + if (!query_valid_object(obj_idx) || Objects[obj_idx].type != OBJ_PROP) { + continue; + } + if (!stricmp(def.name, "no_collide")) { + Objects[obj_idx].flags.set(Object::Object_Flags::Collides, state != Qt::Checked); + } } + set_modified(); + // Caller is responsible for triggering missionChanged() (deferred to avoid FlagListWidget re-entrancy) } SCP_string PropEditorDialogModel::getLayer() const @@ -317,10 +299,7 @@ void PropEditorDialogModel::selectNextProp() { } return; } - - if (apply()) { - selectPropFromObjectList(GET_NEXT(&Objects[_selectedPropObjects.front()]), true); - } + selectPropFromObjectList(GET_NEXT(&Objects[_selectedPropObjects.front()]), true); } void PropEditorDialogModel::selectPreviousProp() { @@ -330,10 +309,7 @@ void PropEditorDialogModel::selectPreviousProp() { } return; } - - if (apply()) { - selectPropFromObjectList(GET_PREV(&Objects[_selectedPropObjects.front()]), false); - } + selectPropFromObjectList(GET_PREV(&Objects[_selectedPropObjects.front()]), false); } void PropEditorDialogModel::onSelectedObjectChanged(int) { diff --git a/qtfred/src/mission/dialogs/PropEditorDialogModel.h b/qtfred/src/mission/dialogs/PropEditorDialogModel.h index ceb607678ce..b51ec3d55b4 100644 --- a/qtfred/src/mission/dialogs/PropEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/PropEditorDialogModel.h @@ -18,7 +18,7 @@ class PropEditorDialogModel : public AbstractDialogModel { bool hasMultipleSelection() const; static bool hasAnyPropsInMission(); const SCP_string& getPropName() const; - void setPropName(const SCP_string& name); + bool setPropName(const SCP_string& name); const SCP_vector>& getFlagLabels() const; const SCP_vector& getFlagState() const; @@ -41,7 +41,6 @@ class PropEditorDialogModel : public AbstractDialogModel { private: // NOLINT(readability-redundant-access-specifiers) void initializeData(); - bool validateData(); void showErrorDialogNoCancel(const SCP_string& message); void selectPropFromObjectList(object* start, bool forward); void selectFirstPropInMission(); diff --git a/qtfred/src/ui/dialogs/PropEditorDialog.cpp b/qtfred/src/ui/dialogs/PropEditorDialog.cpp index d3cab36b7e3..2e5171f5c19 100644 --- a/qtfred/src/ui/dialogs/PropEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/PropEditorDialog.cpp @@ -35,10 +35,9 @@ PropEditorDialog::PropEditorDialog(FredView* parent, EditorViewport* viewport) } } } - // Applying immediately can re-enter FlagListWidget while it is still processing - // itemChanged, which may invalidate the underlying item/model pointers. - // Queue the apply until the current signal stack unwinds. - QMetaObject::invokeMethod(this, [this]() { _model->apply(); }, Qt::QueuedConnection); + // Defer missionChanged to avoid re-entering FlagListWidget while it processes itemChanged, + // which could invalidate the underlying item/model pointers. + QMetaObject::invokeMethod(this, [this]() { _viewport->editor->missionChanged(); }, Qt::QueuedConnection); }); resize(QDialog::sizeHint()); @@ -90,8 +89,7 @@ void PropEditorDialog::updateUi() { } void PropEditorDialog::on_propNameLineEdit_editingFinished() { - _model->setPropName(ui->propNameLineEdit->text().toUtf8().constData()); - if (!_model->apply()) { + if (!_model->setPropName(ui->propNameLineEdit->text().toUtf8().constData())) { updateUi(); } } @@ -108,9 +106,6 @@ void PropEditorDialog::on_layerCombo_currentIndexChanged(int index) { if (index < 0) return; _model->setLayer(ui->layerCombo->itemData(index).toString().toUtf8().constData()); - if (!_model->apply()) { - updateUi(); - } } } // namespace fso::fred::dialogs From 76c00dbc069418db5670a11ee91ff283f56c0070 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 11 May 2026 06:53:58 -0500 Subject: [PATCH 31/65] Add set-guard-range sexp (#7251) * set-guard-range * make sure old path remains unchanged * address feedback again --- code/ai/aicode.cpp | 27 +++++++++++++++++++------- code/parse/sexp.cpp | 47 +++++++++++++++++++++++++++++++++++++++++++++ code/parse/sexp.h | 1 + code/ship/ship.cpp | 1 + code/ship/ship.h | 1 + 5 files changed, 70 insertions(+), 7 deletions(-) diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index d7c5a18d916..ad556704659 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -323,6 +323,18 @@ void ai_cleanup_dock_mode_objective(object *objp); // the "autopilot" object *Autopilot_flight_leader = NULL; +static inline float ai_guard_threshold(const object* guarded_objp, float threshold) +{ + if (guarded_objp != nullptr && guarded_objp->type == OBJ_SHIP && guarded_objp->instance >= 0) { + const float configured = Ships[guarded_objp->instance].max_guard_radius; + if (configured > 0.0f) { + return configured; + } + } + + return threshold; +} + /** * Sets the timestamp used to tell is it is a good time for this team to rearm. * Ends a 'bad rearm time' @@ -5117,9 +5129,10 @@ int maybe_resume_previous_mode(object *objp, ai_info *aip) // If guarding ship is far away from guardee and enemy is far away from guardee, // then stop chasing and resume guarding. - if (dist > (MAX_GUARD_DIST + guard_objp->radius) * 6) { + if (dist > ai_guard_threshold(guard_objp, (MAX_GUARD_DIST + guard_objp->radius) * 6)) + { if ((En_objp != NULL) && (En_objp->type == OBJ_SHIP)) { - if (vm_vec_dist_quick(&guard_objp->pos, &En_objp->pos) > (MAX_GUARD_DIST + guard_objp->radius) * 6) { + if (vm_vec_dist_quick(&guard_objp->pos, &En_objp->pos) > ai_guard_threshold(guard_objp, (MAX_GUARD_DIST + guard_objp->radius) * 6)) { Assert(aip->previous_mode == AIM_GUARD); aip->mode = aip->previous_mode; aip->submode = AIS_GUARD_PATROL; @@ -10517,7 +10530,7 @@ int ai_guard_find_nearby_bomb(object *guarding_objp, object *guarded_objp) dist = vm_vec_dist_quick(&bomb_objp->pos, &guarded_objp->pos); - if (dist < (MAX_GUARD_DIST + guarded_objp->radius)*3) { + if (dist < ai_guard_threshold(guarded_objp, (MAX_GUARD_DIST + guarded_objp->radius) * 3)) { dist_to_guarding_obj = vm_vec_dist_quick(&bomb_objp->pos, &guarding_objp->pos); if ( dist_to_guarding_obj < closest_dist_to_guarding_obj ) { closest_dist_to_guarding_obj = dist_to_guarding_obj; @@ -10561,11 +10574,11 @@ void ai_guard_find_nearby_ship(object *guarding_objp, object *guarded_objp) if (Ship_info[eshipp->ship_info_index].class_type >= 0 && (Ship_types[Ship_info[eshipp->ship_info_index].class_type].flags[Ship::Type_Info_Flags::AI_guards_attack])) { dist = vm_vec_dist_quick(&enemy_objp->pos, &guarded_objp->pos); - if (dist < (MAX_GUARD_DIST + guarded_objp->radius)*3) + if (dist < ai_guard_threshold(guarded_objp, (MAX_GUARD_DIST + guarded_objp->radius) * 3)) { guard_object_was_hit(guarding_objp, enemy_objp); - } - else if ((dist < 3000.0f) && (Ai_info[eshipp->ai_index].target_objnum == guarding_aip->guard_objnum)) + } else if ((dist < ai_guard_threshold(guarded_objp, 3000.0f)) && + (Ai_info[eshipp->ai_index].target_objnum == guarding_aip->guard_objnum)) { guard_object_was_hit(guarding_objp, enemy_objp); } @@ -10592,7 +10605,7 @@ void ai_guard_find_nearby_asteroid(object *guarding_objp, object *guarded_objp) if ( asteroid_objp->type == OBJ_ASTEROID ) { // Attack asteroid if near guarded ship dist = vm_vec_dist_quick(&asteroid_objp->pos, &guarded_objp->pos); - if ( dist < (MAX_GUARD_DIST + guarded_objp->radius)*2) { + if (dist < ai_guard_threshold(guarded_objp, (MAX_GUARD_DIST + guarded_objp->radius) * 2)) { dist_to_self = vm_vec_dist_quick(&asteroid_objp->pos, &guarding_objp->pos); if ( OBJ_INDEX(guarded_objp) == asteroid_collide_objnum(asteroid_objp) ) { if( dist_to_self < closest_danger_asteroid_dist ) { diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index b54df485689..3c3cda2c890 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -525,6 +525,7 @@ SCP_vector Operators = { { "ship-no-guardian", OP_SHIP_NO_GUARDIAN, 1, INT_MAX, SEXP_ACTION_OPERATOR, }, { "ship-guardian-threshold", OP_SHIP_GUARDIAN_THRESHOLD, 2, INT_MAX, SEXP_ACTION_OPERATOR, }, { "ship-subsys-guardian-threshold", OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD, 3, INT_MAX, SEXP_ACTION_OPERATOR, }, + { "set-guard-range", OP_SET_GUARD_RANGE, 2, INT_MAX, SEXP_ACTION_OPERATOR, }, // MjnMixael { "self-destruct", OP_SELF_DESTRUCT, 1, INT_MAX, SEXP_ACTION_OPERATOR, }, { "destroy-instantly", OP_DESTROY_INSTANTLY, 1, INT_MAX, SEXP_ACTION_OPERATOR, }, // Admiral MS { "destroy-instantly-with-debris", OP_DESTROY_INSTANTLY_WITH_DEBRIS, 1, INT_MAX, SEXP_ACTION_OPERATOR, }, // Asteroth @@ -19521,6 +19522,29 @@ void sexp_ship_guardian_threshold(int node) } } +// MjnMixael +void sexp_set_guard_range(int node) +{ + int range, n = node; + bool is_nan, is_nan_forever; + + range = eval_num(n, is_nan, is_nan_forever); + if (is_nan || is_nan_forever) + return; + n = CDR(n); + + for (; n != -1; n = CDR(n)) { + auto ship_entry = eval_ship(n); + if (!ship_entry || !ship_entry->has_shipp()) { + continue; + } + + // Intentionally no lower bound validation beyond disabling at <= 0. + // Mission authors may choose very small positive values for highly restrictive escort behavior. + ship_entry->shipp()->max_guard_radius = (range > 0) ? static_cast(range) : -1.0f; + } +} + // Goober5000 void sexp_ship_subsys_guardian_threshold(int node) { @@ -28925,6 +28949,11 @@ int eval_sexp(int cur_node, int referenced_node) sexp_val = SEXP_TRUE; break; + case OP_SET_GUARD_RANGE: + sexp_set_guard_range(node); + sexp_val = SEXP_TRUE; + break; + case OP_SHIP_SUBSYS_TARGETABLE: sexp_ship_deal_with_subsystem_flag(cur_node, node, Ship::Subsystem_Flags::Untargetable, true, false); sexp_val = SEXP_TRUE; @@ -31676,6 +31705,7 @@ int query_operator_return_type(int op) case OP_SHIP_NO_GUARDIAN: case OP_SHIP_GUARDIAN_THRESHOLD: case OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD: + case OP_SET_GUARD_RANGE: case OP_SHIP_VANISH: case OP_PROP_VANISH: case OP_DESTROY_INSTANTLY: @@ -32390,6 +32420,12 @@ int query_operator_argument_type(int op_index, int argnum) else return OPF_SUBSYS_OR_GENERIC; + case OP_SET_GUARD_RANGE: + if (argnum == 0) + return OPF_NUMBER; + else + return OPF_SHIP; + case OP_SHIP_SUBSYS_TARGETABLE: case OP_SHIP_SUBSYS_UNTARGETABLE: if (argnum == 0) @@ -36975,6 +37011,7 @@ int get_category(int op_id) case OP_JUMP_NODE_HIDE_JUMPNODE: case OP_SHIP_GUARDIAN_THRESHOLD: case OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD: + case OP_SET_GUARD_RANGE: case OP_SET_SKYBOX_MODEL: case OP_SHIP_CREATE: case OP_PROP_CREATE: @@ -37334,6 +37371,7 @@ int get_subcategory(int op_id) case OP_ALTER_SHIP_FLAG: case OP_ALTER_WING_FLAG: + case OP_SET_GUARD_RANGE: case OP_PROTECT_SHIP: case OP_UNPROTECT_SHIP: case OP_BEAM_PROTECT_SHIP: @@ -40530,6 +40568,15 @@ SCP_vector Sexp_help = { "\t2:\tShip housing the subsystem(s) (ships must be in-mission).\r\n" "\t3+:\tSubsystems to make unkillable." }, + // MjnMixael + { OP_SET_GUARD_RANGE, "set-guard-range\r\n" + "\tSets the max range in meters at which any ships guarding this ship will engage with threats.\r\n" + "This range will override the default dynamic range behavior for ships obeying a guard order.\r\n" + "If the value is <= 0, regular dynamic guard range behavior will resume. Positive values are used as is with no size validation based on ship class.\r\n\r\n" + "Takes 2 or more arguments...\r\n" + "\t1:\tGuard range cap in meters (<= 0 disables cap).\r\n" + "\t2+:\tShip(s) to apply the cap to (ships must be in-mission)." }, + // Goober5000 { OP_SHIP_STEALTHY, "ship-stealthy\r\n" "\tCauses the ships listed in this sexpression to become stealth ships (i.e. invisible to radar).\r\n\r\n" diff --git a/code/parse/sexp.h b/code/parse/sexp.h index 1c73e0ce919..6e5c8b8b4e9 100644 --- a/code/parse/sexp.h +++ b/code/parse/sexp.h @@ -698,6 +698,7 @@ enum : int { OP_JUMP_NODE_HIDE_JUMPNODE, // WMC OP_SHIP_GUARDIAN_THRESHOLD, // Goober5000 OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD, // Goober5000 + OP_SET_GUARD_RANGE, //MjnMixael OP_SET_SKYBOX_MODEL, // taylor OP_SHIP_CREATE, OP_PROP_CREATE, // MjnMixael diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 24fedc63581..fb5f6107e62 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -7229,6 +7229,7 @@ void ship::clear() ship_max_hull_strength = 0.0f; ship_guardian_threshold = 0; + max_guard_radius = -1.0f; ship_name[0] = 0; display_name.clear(); diff --git a/code/ship/ship.h b/code/ship/ship.h index d656d936084..788b8c86215 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -622,6 +622,7 @@ class ship float max_weapon_regen_per_second; // wookieejedi - make this a ship object variable int ship_guardian_threshold; // Goober5000 - now also determines whether ship is guardian'd + float max_guard_radius; // Optional clamp for guard engagement/resume ranges; <= 0 means unused char ship_name[NAME_LENGTH]; From 2046f9b5ac79a14e5f73879e5ec3eaa8a8224949 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Mon, 11 May 2026 07:55:15 -0400 Subject: [PATCH 32/65] Update ParticleEffect.h (#7446) Curve input follow-up to #6926 and #6889, in that it adds two new curve input Nebula and Particle Detail Levels for use within the particle effects table. These new inputs will allow modders to develop effects that can properly be optimized per varying particle and nebula detail levels. --- code/particle/ParticleEffect.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/code/particle/ParticleEffect.h b/code/particle/ParticleEffect.h index 0824139850e..f44e971baf3 100644 --- a/code/particle/ParticleEffect.h +++ b/code/particle/ParticleEffect.h @@ -273,10 +273,12 @@ class ParticleEffect { modular_curves_submember_input<&ParticleSource::getEffect, &SCP_vector::size>, ModularCurvesMathOperators::division>{}}, std::pair {"Total Particle Count", modular_curves_global_submember_input{}}, + std::pair {"Particle Detail Level", modular_curves_global_submember_input{}}, std::pair {"Particle Usage Score", modular_curves_math_input< modular_curves_global_submember_input, modular_curves_global_submember_input, ModularCurvesMathOperators::division>{}}, + std::pair {"Nebula Detail Level", modular_curves_global_submember_input{}}, std::pair {"Nebula Usage Score", modular_curves_math_input< modular_curves_global_submember_input, modular_curves_global_submember_input, From 1a34123f0c8ab97a2f34afcd8112bb8f78139730 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 12 May 2026 07:48:27 -0400 Subject: [PATCH 33/65] fix qtfred heap corruption in FastDebug builds (#7450) FastDebug FSO compiled with /MDd (debug CRT) but imported Qt's Release libraries via CMAKE_MAP_IMPORTED_CONFIG_FASTDEBUG = Release Debug. Qt Release ships /MD, so any allocation crossing the qtfred <-> Qt boundary went into one heap and came back out of the other, tripping _CrtIsValidHeapPointer on a perfectly-valid std::string dtor. Latent for ages because it only surfaces when an allocation actually crosses; opening a mission file is one such path. To paper over the resulting STL layout difference, FastDebug also forced _ITERATOR_DEBUG_LEVEL=0 so qtfred's std::string matched Release Qt's. Not needed once we link Debug Qt, and keeping it would introduce a fresh mismatch (qtfred IDL=0 vs Debug Qt IDL=2). Fix: - Map FastDebug imports to "Debug Release \"\"" in qtfred/CMakeLists. Debug picks Qt's debug libs; Release is a defensive fallback; the empty entry falls back to plain IMPORTED_LOCATION, required because Qt's host tools (uic/moc/rcc/qhelpgenerator) ship a single variant with no IMPORTED_LOCATION_. Must precede find_package(Qt5): CMake snapshots the variable into each target's MAP_IMPORTED_CONFIG property at creation time. (Also why the prior "Release Debug" was silently a no-op -- set after find_package, ignored; CMake fell back to Release on its own.) - Extend the qwindows/qsqlite plugin POST_BUILD copies' "d"-suffix generator expression to match FastDebug as well as Debug, so the Debug Qt runtime finds qwindowsd.dll / qsqlited.dll alongside. - Drop the FastDebug-only _ITERATOR_DEBUG_LEVEL=0 from the MSVC toolchain file. Qt was the sole consumer; pure-C deps don't care. Cost: FastDebug STL containers pick up MSVC's debug-iterator overhead. Reasonable trade for a config named "Debug with optimizations". Co-authored-by: Claude Opus 4.7 (1M context) --- cmake/toolchain-msvc.cmake | 1 - qtfred/CMakeLists.txt | 22 ++++++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/cmake/toolchain-msvc.cmake b/cmake/toolchain-msvc.cmake index 492405b79ca..dcff50681ba 100644 --- a/cmake/toolchain-msvc.cmake +++ b/cmake/toolchain-msvc.cmake @@ -114,7 +114,6 @@ if(IS_X86) endif() -add_compile_definitions("$<$:_ITERATOR_DEBUG_LEVEL=0>") target_compile_definitions(compiler INTERFACE _CRT_SECURE_NO_DEPRECATE _CRT_SECURE_NO_WARNINGS _SECURE_SCL=0 NOMINMAX _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING) if (FSO_FATAL_WARNINGS) diff --git a/qtfred/CMakeLists.txt b/qtfred/CMakeLists.txt index 94311a35409..cc373406b3c 100644 --- a/qtfred/CMakeLists.txt +++ b/qtfred/CMakeLists.txt @@ -8,6 +8,18 @@ SET(QT5_INSTALL_ROOT "" CACHE PATH list(APPEND CMAKE_PREFIX_PATH "${QT5_INSTALL_ROOT}") +# Must be set before find_package -- CMake snapshots this into each imported target's +# MAP_IMPORTED_CONFIG_FASTDEBUG property at target-creation time, not at link time. +# The list entries are tried in order: +# "Debug" -- matches the per-config IMPORTED_LOCATION_DEBUG on Qt's libraries. +# "Release" -- per-config fallback if any target ships only a Release variant. +# "" -- the empty entry means "fall back to the suffix-less IMPORTED_LOCATION". +# Required because Qt's host tools (uic/moc/rcc/qhelpgenerator) set +# only IMPORTED_LOCATION (no IMPORTED_CONFIGURATIONS), so without this +# CMake errors with "IMPORTED_LOCATION not set for imported target +# Qt5::uic configuration FastDebug". +set(CMAKE_MAP_IMPORTED_CONFIG_FASTDEBUG Debug Release "") + find_package(Qt5 COMPONENTS Widgets OpenGL Help REQUIRED) include(source_groups.cmake) @@ -51,8 +63,6 @@ if(WIN32) set_property(TARGET qtfred PROPERTY QT5_NO_LINK_QTMAIN ON) endif() -set(CMAKE_MAP_IMPORTED_CONFIG_FASTDEBUG Release Debug) - target_link_libraries(qtfred PUBLIC code @@ -169,11 +179,11 @@ if (WIN32) # Windows requires that the qwindows DLL is copied as well execute_process(COMMAND ${QT_QMAKE_EXECUTABLE} -query QT_INSTALL_PLUGINS OUTPUT_VARIABLE QT_INSTALL_PLUGINS OUTPUT_STRIP_TRAILING_WHITESPACE) - set(qwindows_path "${QT_INSTALL_PLUGINS}/platforms/qwindows$<$:d>.dll") + set(qwindows_path "${QT_INSTALL_PLUGINS}/platforms/qwindows$<$,$>:d>.dll") add_custom_command(TARGET qtfred POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${qwindows_path}" "$/platforms/qwindows$<$:d>.dll" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${qwindows_path}" "$/platforms/qwindows$<$,$>:d>.dll" VERBATIM) install(FILES ${qwindows_path} @@ -182,11 +192,11 @@ if (WIN32) ) # Qt Help requires the SQLite SQL driver - set(qsqlite_path "${QT_INSTALL_PLUGINS}/sqldrivers/qsqlite$<$:d>.dll") + set(qsqlite_path "${QT_INSTALL_PLUGINS}/sqldrivers/qsqlite$<$,$>:d>.dll") add_custom_command(TARGET qtfred POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${qsqlite_path}" "$/sqldrivers/qsqlite$<$:d>.dll" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${qsqlite_path}" "$/sqldrivers/qsqlite$<$,$>:d>.dll" VERBATIM) install(FILES ${qsqlite_path} From 6c0d490489dafa0328e37e468bbab1ac3d012ca9 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 12 May 2026 06:49:27 -0500 Subject: [PATCH 34/65] Qtfred error checker (#7389) * new qtfred error checker * fix some issues * clang * more clang * clean up and fixes * missed a comment * add help documentation * surface auto corrected issues more clearly * some cleanup --- code/globalincs/pstypes.h | 1 + code/mission/missionparse.cpp | 78 +- code/mission/missionparse.h | 5 + fred2/fred.cpp | 1 + freespace2/freespace.cpp | 1 + .../doc/dialogs/ErrorCheckerDialog.html | 79 +- qtfred/source_groups.cmake | 7 + qtfred/src/main.cpp | 1 + qtfred/src/mission/Editor.cpp | 1359 +-------------- qtfred/src/mission/Editor.h | 30 +- qtfred/src/mission/EditorViewport.cpp | 2 + qtfred/src/mission/EditorViewport.h | 6 +- .../dialogs/ErrorCheckerDialogModel.cpp | 60 + .../mission/dialogs/ErrorCheckerDialogModel.h | 37 + .../dialogs/PreferencesDialogModel.cpp | 15 +- .../mission/dialogs/PreferencesDialogModel.h | 11 +- .../ShipEditor/ShipGoalsDialogModel.cpp | 38 +- .../dialogs/ShipEditor/ShipGoalsDialogModel.h | 2 +- qtfred/src/ui/FredView.cpp | 204 ++- qtfred/src/ui/FredView.h | 11 + qtfred/src/ui/dialogs/ErrorCheckerDialog.cpp | 297 ++++ qtfred/src/ui/dialogs/ErrorCheckerDialog.h | 71 + qtfred/src/ui/dialogs/PreferencesDialog.cpp | 11 +- qtfred/src/ui/dialogs/PreferencesDialog.h | 3 +- qtfred/src/ui/util/ErrorChecker.cpp | 1517 +++++++++++++++++ qtfred/src/ui/util/ErrorChecker.h | 147 ++ qtfred/ui/ErrorCheckerDialog.ui | 97 ++ qtfred/ui/PreferencesDialog.ui | 20 +- test/src/test_stubs.cpp | 1 + 29 files changed, 2675 insertions(+), 1437 deletions(-) create mode 100644 qtfred/src/mission/dialogs/ErrorCheckerDialogModel.cpp create mode 100644 qtfred/src/mission/dialogs/ErrorCheckerDialogModel.h create mode 100644 qtfred/src/ui/dialogs/ErrorCheckerDialog.cpp create mode 100644 qtfred/src/ui/dialogs/ErrorCheckerDialog.h create mode 100644 qtfred/src/ui/util/ErrorChecker.cpp create mode 100644 qtfred/src/ui/util/ErrorChecker.h create mode 100644 qtfred/ui/ErrorCheckerDialog.ui diff --git a/code/globalincs/pstypes.h b/code/globalincs/pstypes.h index 90b80c7a8fb..55d94de70b6 100644 --- a/code/globalincs/pstypes.h +++ b/code/globalincs/pstypes.h @@ -364,6 +364,7 @@ const float PI_4 = (PI/4.0f); extern int Fred_running; // Is Fred running, or FreeSpace? +extern int Qtfred_running; // Distinguishes QtFRED from legacy Fred2; Fred_running is set in both, but Qtfred_running only in QtFRED. extern bool running_unittests; const size_t INVALID_SIZE = static_cast(-1); diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index 70b4f64a465..d018a087f5c 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -175,6 +175,27 @@ p_object *Player_start_pobject; // something before that ship has even been loaded yet) SCP_vector Parse_names; +SCP_vector Mission_parse_warnings; + +// Routes a parse-time auto-correction notice to the right surface for the app: +// outside QtFRED, the existing Warning(LOCATION, ...) popup; inside QtFRED, the +// Mission_parse_warnings queue so the ErrorChecker can present it without a popup. +static void parse_warning_or_record(SCP_FORMAT_STRING const char* fmt, ...) SCP_FORMAT_STRING_ARGS(1, 2); +static void parse_warning_or_record(const char* fmt, ...) +{ + SCP_string msg; + va_list args; + va_start(args, fmt); + vsprintf(msg, fmt, args); + va_end(args); + + if (Qtfred_running) { + Mission_parse_warnings.push_back(std::move(msg)); + } else { + Warning(LOCATION, "%s", msg.c_str()); + } +} + SCP_vector Fred_texture_replacements; SCP_unordered_set Fred_migrated_immobile_ships; @@ -2426,10 +2447,10 @@ int parse_create_object_sub(p_object *p_objp, bool standalone_ship) // will accept were apparently written out incorrectly with Fred. This Int3() should // trap these instances. #ifndef NDEBUG - if (Fred_running) + if (Fred_running && !Qtfred_running) { std::set default_orders, remaining_orders; - + default_orders = ship_get_default_orders_accepted(&Ship_info[shipp->ship_info_index]); std::set_difference(p_objp->orders_accepted.begin(), p_objp->orders_accepted.end(), default_orders.begin(), default_orders.end(), std::inserter(remaining_orders, remaining_orders.begin())); @@ -2963,7 +2984,7 @@ void resolve_parse_flags(object *objp, flagset &par if ((parse_flags[Mission::Parse_Object_Flags::OF_No_shields]) && (parse_flags[Mission::Parse_Object_Flags::OF_Force_shields_on])) { - Warning(LOCATION, "The parser found a ship with both the \"force-shields-on\" and \"no-shields\" flags; this is inconsistent!"); + parse_warning_or_record("Ship %s has both the \"force-shields-on\" and \"no-shields\" flags; this is inconsistent.", shipp->ship_name); } if (parse_flags[Mission::Parse_Object_Flags::OF_No_shields]) objp->flags.set(Object::Object_Flags::No_shields); @@ -3458,7 +3479,7 @@ int parse_object(mission *pm, int /*flag*/, p_object *p_objp) || (p_objp->arrival_location == ArrivalLocation::ABOVE_SHIP) || (p_objp->arrival_location == ArrivalLocation::BELOW_SHIP) || (p_objp->arrival_location == ArrivalLocation::TO_LEFT_OF_SHIP) || (p_objp->arrival_location == ArrivalLocation::TO_RIGHT_OF_SHIP) )) { - Warning(LOCATION, "Arrival distance for ship %s cannot be %d. Setting to 1.\n", p_objp->name, p_objp->arrival_distance); + parse_warning_or_record("Arrival distance for ship %s cannot be %d — corrected to 1.", p_objp->name, p_objp->arrival_distance); p_objp->arrival_distance = 1; } } @@ -3483,7 +3504,7 @@ int parse_object(mission *pm, int /*flag*/, p_object *p_objp) stuff_int(&delay); if (delay < 0) { - Warning(LOCATION, "Cannot have arrival delay < 0 on ship %s", p_objp->name); + parse_warning_or_record("Arrival delay on ship %s cannot be negative — corrected to 0.", p_objp->name); delay = 0; } @@ -3520,7 +3541,7 @@ int parse_object(mission *pm, int /*flag*/, p_object *p_objp) stuff_int(&delay); if (delay < 0) { - Warning(LOCATION, "Cannot have departure delay < 0 (ship %s)", p_objp->name); + parse_warning_or_record("Departure delay on ship %s cannot be negative — corrected to 0.", p_objp->name); delay = 0; } @@ -3752,7 +3773,7 @@ int parse_object(mission *pm, int /*flag*/, p_object *p_objp) stuff_int(&p_objp->destroy_before_mission_time); if (p_objp->destroy_before_mission_time < 0) { - Warning(LOCATION, "Cannot set a negative 'destroy before mission' value (ship %s)", p_objp->name); + parse_warning_or_record("'Destroy before mission' value on ship %s cannot be negative — corrected to 0.", p_objp->name); p_objp->destroy_before_mission_time = 0; } @@ -3847,7 +3868,7 @@ int parse_object(mission *pm, int /*flag*/, p_object *p_objp) if (optional_string("+Persona Index:")) { stuff_int(&p_objp->persona_index); if (p_objp->persona_index < -1 || p_objp->persona_index >= (int)Personas.size()) { - Warning(LOCATION, "Persona index %d for %s is out of range! Setting to -1.", p_objp->persona_index, p_objp->name); + parse_warning_or_record("Persona index %d for ship %s is out of range — corrected to -1.", p_objp->persona_index, p_objp->name); p_objp->persona_index = -1; } } @@ -4915,7 +4936,7 @@ void parse_wing(mission *pm) || (wingp->arrival_location == ArrivalLocation::ABOVE_SHIP) || (wingp->arrival_location == ArrivalLocation::BELOW_SHIP) || (wingp->arrival_location == ArrivalLocation::TO_LEFT_OF_SHIP) || (wingp->arrival_location == ArrivalLocation::TO_RIGHT_OF_SHIP) )) { - Warning(LOCATION, "Arrival distance for wing %s cannot be %d. Setting to 1.\n", wingp->name, wingp->arrival_distance); + parse_warning_or_record("Arrival distance for wing %s cannot be %d — corrected to 1.", wingp->name, wingp->arrival_distance); wingp->arrival_distance = 1; } } @@ -4940,7 +4961,7 @@ void parse_wing(mission *pm) stuff_int(&delay); if (delay < 0) { - Warning(LOCATION, "Cannot have arrival delay < 0 on wing %s", wingp->name); + parse_warning_or_record("Arrival delay on wing %s cannot be negative — corrected to 0.", wingp->name); delay = 0; } @@ -4977,7 +4998,7 @@ void parse_wing(mission *pm) stuff_int(&delay); if (delay < 0) { - Warning(LOCATION, "Cannot have departure delay < 0 on wing %s", wingp->name); + parse_warning_or_record("Departure delay on wing %s cannot be negative — corrected to 0.", wingp->name); delay = 0; } @@ -5152,7 +5173,7 @@ void parse_wing(mission *pm) // Goober5000 - if this is a player start object, there shouldn't be a wing arrival delay (Mantis #2678) if ((p_objp->flags[Mission::Parse_Object_Flags::OF_Player_start]) && (wingp->arrival_delay != 0)) { - Warning(LOCATION, "Wing %s specifies an arrival delay of %ds, but it also contains a player. The arrival delay will be reset to 0.", wingp->name, abs(wingp->arrival_delay)); + parse_warning_or_record("Wing %s specifies an arrival delay of %ds, but it also contains a player — corrected to 0.", wingp->name, abs(wingp->arrival_delay)); if (!Fred_running && wingp->arrival_delay > 0) { // timestamp has been set, so set it again wingp->arrival_delay = timestamp(0); @@ -5423,7 +5444,9 @@ void post_process_ships_wings() // error checking for custom wings if (strcmp(Starting_wing_names[0], TVT_wing_names[0]) != 0) { - Error(LOCATION, "The first starting wing and the first team-versus-team wing must have the same wing name.\n"); + // In QtFRED this is surfaced via ErrorChecker::checkPlayerWings so the editor can load the mission. + if (!Qtfred_running) + Error(LOCATION, "The first starting wing and the first team-versus-team wing must have the same wing name.\n"); } // set up wing indexes @@ -5610,7 +5633,7 @@ void post_process_ships_wings() for (int i = 1; i < MAX_STARTING_WINGS; i++) { // If there was a wing for this squadron entry, check the last one. If it's empty, we found a mistake, so move the wing names over. if (Squadron_wing_names_found[i] && !Squadron_wing_names_found[i - 1]) { - Warning(LOCATION, "Squadron wings are not in the correct order and may cause wings to disappear in multi.\n\nEither wing %s should exist or the %s entry needs to come before it in the list.\n\nPlease go back and fix the mission.", Squadron_wing_names[i - 1], Squadron_wing_names[i]); + parse_warning_or_record("Squadron wings are not in the correct order and may cause wings to disappear in multi. Either wing %s should exist or the %s entry needs to come before it in the list — wing names have been swapped.", Squadron_wing_names[i - 1], Squadron_wing_names[i]); char temp_chars[NAME_LENGTH]; strcpy_s(temp_chars, Squadron_wing_names[i - 1]); strcpy_s(Squadron_wing_names[i - 1], Squadron_wing_names[i]); @@ -5648,7 +5671,7 @@ void parse_event(mission *pm) // sanity check on the repeat count variable // _argv[-1] - negative repeat count is now legal; means repeat indefinitely. if ( event->repeat_count == 0 ){ - Warning(LOCATION, "Repeat count for mission event %s is 0.\nMust be >= 1 or negative! Setting to 1.", event->name.c_str() ); + parse_warning_or_record("Repeat count for mission event %s is 0 — must be >= 1 or negative; corrected to 1.", event->name.c_str()); event->repeat_count = 1; } } @@ -5665,7 +5688,7 @@ void parse_event(mission *pm) // sanity check on the trigger count variable // negative trigger count is also legal if ( event->trigger_count == 0 ){ - Warning(LOCATION, "Trigger count for mission event %s is 0.\nMust be >= 1 or negative! Setting to 1.", event->name.c_str() ); + parse_warning_or_record("Trigger count for mission event %s is 0 — must be >= 1 or negative; corrected to 1.", event->name.c_str()); event->trigger_count = 1; } } @@ -6021,7 +6044,7 @@ void parse_reinforcement(mission *pm) stuff_int(&delay); if (delay < 0) { - Warning(LOCATION, "Cannot have arrival delay < 0 on reinforcement %s", ptr->name); + parse_warning_or_record("Arrival delay on reinforcement %s cannot be negative — corrected to 0.", ptr->name); delay = 0; } @@ -6041,14 +6064,14 @@ void parse_reinforcement(mission *pm) if (rforce_obj == NULL) { if ((instance = wing_name_lookup(ptr->name, 1)) == -1) { - Warning(LOCATION, "Reinforcement %s not found as ship or wing", ptr->name); + parse_warning_or_record("Reinforcement %s not found as ship or wing — declaration ignored.", ptr->name); return; } } else { // Individual ships in wings can't be reinforcements - FUBAR if (rforce_obj->wingnum >= 0) { - Warning(LOCATION, "Reinforcement %s is part of a wing - Ignoring reinforcement declaration", ptr->name); + parse_warning_or_record("Reinforcement %s is part of a wing — reinforcement declaration ignored.", ptr->name); return; } else @@ -6698,6 +6721,9 @@ bool parse_mission(mission *pm, int flags) int saved_warning_count = Global_warning_count; int saved_error_count = Global_error_count; + // Reset the parse-time warning queue so each load starts fresh (only consumed by QtFRED). + Mission_parse_warnings.clear(); + // reset parse error stuff Num_unknown_ship_classes = 0; Num_unknown_prop_classes = 0; @@ -6798,7 +6824,9 @@ bool parse_mission(mission *pm, int flags) if (!post_process_mission(pm)) return false; - if ((saved_warning_count - Global_warning_count) > 10 || (saved_error_count - Global_error_count) > 0) { + // QtFRED surfaces parse issues through its own error checker, so skip this summary popup there; Fred2 and the game still show it. + if (!Qtfred_running && + ((saved_warning_count - Global_warning_count) > 10 || (saved_error_count - Global_error_count) > 0)) { char text[512]; sprintf(text, "Warning!\n\nThe current mission has generated %d warnings and/or errors during load. These are usually caused by corrupted ship models or syntax errors in the mission file. While FreeSpace Open will attempt to compensate for these issues, it cannot guarantee a trouble-free gameplay experience. Source Code Project staff cannot provide assistance or support for these problems, as they are caused by the mission's data files, not FreeSpace Open's source code.", (saved_warning_count - Global_warning_count) + (saved_error_count - Global_error_count)); popup(PF_TITLE_BIG | PF_TITLE_RED | PF_USE_AFFIRMATIVE_ICON | PF_NO_NETWORKING, 1, POPUP_OK, text); @@ -6936,7 +6964,9 @@ bool post_process_mission(mission *pm) error_msg += "\n\n(Bad node appears to be: "; error_msg += bad_node_str; error_msg += ")\n"; - Warning(LOCATION, "%s", error_msg.c_str()); + // QtFRED surfaces SEXP errors through ErrorChecker's fred_check_sexp; skip the popup there. + if (!Qtfred_running) + Warning(LOCATION, "%s", error_msg.c_str()); // syntax errors are recoverable in Fred but not FS if (!Fred_running && !sexp_recoverable_error(result)) { @@ -7734,7 +7764,8 @@ void mission_parse_set_up_initial_docks() // display an error if necessary if (dfi.maintained_variables.int_value == 0) { - Warning(LOCATION, "In the docking group containing %s, every ship has an arrival cue set to false. The group will not appear in-mission!\n", pobjp->name); + if (!Qtfred_running) + Warning(LOCATION, "In the docking group containing %s, every ship has an arrival cue set to false. The group will not appear in-mission!\n", pobjp->name); // for FRED, we must arbitrarily choose a dock leader, otherwise the entire docked group will not be loaded if (Fred_running) @@ -7742,7 +7773,8 @@ void mission_parse_set_up_initial_docks() } else if (dfi.maintained_variables.int_value > 1) { - Warning(LOCATION, "In the docking group containing %s, there is more than one ship with a non-false arrival cue! There can only be one such ship. Setting all arrival cues except %s to false...\n", dfi.maintained_variables.objp_value->name, dfi.maintained_variables.objp_value->name); + if (!Qtfred_running) + Warning(LOCATION, "In the docking group containing %s, there is more than one ship with a non-false arrival cue! There can only be one such ship. Setting all arrival cues except %s to false...\n", dfi.maintained_variables.objp_value->name, dfi.maintained_variables.objp_value->name); } // clear dfi stuff diff --git a/code/mission/missionparse.h b/code/mission/missionparse.h index eea950e897e..baf540616e6 100644 --- a/code/mission/missionparse.h +++ b/code/mission/missionparse.h @@ -566,6 +566,11 @@ extern fix Mission_end_time; extern SCP_vector Parse_names; +// Populated when Qtfred_running and a parse-time auto-correction fires. Drained by +// QtFRED's ErrorChecker so the corrections are visible to the designer instead of +// silently buried. Outside of QtFRED these sites still call Warning(LOCATION, ...). +extern SCP_vector Mission_parse_warnings; + extern char Player_start_shipname[NAME_LENGTH]; extern int Player_start_shipnum; extern p_object *Player_start_pobject; diff --git a/fred2/fred.cpp b/fred2/fred.cpp index e176125c30f..49c1733097a 100644 --- a/fred2/fred.cpp +++ b/fred2/fred.cpp @@ -106,6 +106,7 @@ pending_message Pending_messages[MAX_PENDING_MESSAGES]; CFREDApp theApp; int Fred_running = 1; +int Qtfred_running = 0; int FrameCount = 0; bool Fred_active = true; int Update_window = 1; diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index f8cb631b9e5..10de4806fae 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -399,6 +399,7 @@ int Show_net_stats; bool Pre_player_entry = false; int Fred_running = 0; +int Qtfred_running = 0; bool running_unittests = false; // required for hudtarget... kinda dumb, but meh diff --git a/qtfred/help-src/doc/dialogs/ErrorCheckerDialog.html b/qtfred/help-src/doc/dialogs/ErrorCheckerDialog.html index 1e49bf8168f..a9b1359f211 100644 --- a/qtfred/help-src/doc/dialogs/ErrorCheckerDialog.html +++ b/qtfred/help-src/doc/dialogs/ErrorCheckerDialog.html @@ -8,7 +8,82 @@

Error Checker

-

Accessible via Tools › Error Checker.

-
Full documentation for this tool is not yet written.
+

Opens via Tools › Error Checker. Also runs automatically +before saving when issues are present.

+

Scans the entire mission for structural problems, design errors, configuration +warnings, and potential issues. Results are listed by severity with a description +of each problem found.

+ +

Controls

+ + + + + + +
ControlDescription
RerunRuns the check again. Use this after making fixes to confirm + they are resolved.
Check Potential IssuesWhen checked, Potential Issue entries are + included in the results alongside errors and warnings. When unchecked, + potential issues are silently omitted.
Apply Auto-CorrectionsWhen checked, FRED automatically corrects + issues it can resolve without user input each time a check runs. Only applies + to correctable errors and warnings; Critical Errors are never auto-corrected. + Auto-corrections are applied before results are displayed.
Status barShows a summary of the most recent check: the total + number of entries found, or No check has been run yet before the + first run.
+ +

Severity levels

+ + + + + + +
SeverityMeaning
Critical ErrorA serious structural problem that FRED cannot + automatically correct. The mission may fail to load or behave unpredictably + in-game. Must be fixed manually before the mission is usable.
ErrorA mission design problem that must be fixed. The mission may + not play correctly.
WarningA problem that can be (or has been) auto-corrected. Review + the change before saving.
Potential IssueA situation that may be intentional but is worth + reviewing. Only shown when Check Potential Issues is + checked.
+ +

What is checked

+

The checker inspects the following areas of the mission:

+
    +
  • Object list integrity and name uniqueness
  • +
  • Ships — SEXPs, AI goals, anchor references, docking configuration, and loadout + weapon availability
  • +
  • Wings — structure, SEXPs, AI goals, anchor references, wave thresholds, and + accepted orders consistency
  • +
  • Player starts and player wing membership
  • +
  • Waypoint path names for conflicts with object names
  • +
  • Reinforcement name references
  • +
  • Mission events and mission objectives — SEXP validation
  • +
  • Briefings — icon ID uniqueness
  • +
  • Debriefings — SEXP validation
  • +
  • Asteroid field target ship name validity
  • +
  • Initially-docked groups — must have exactly one non-false arrival cue
  • +
  • Team loadout — weapons used in starting wings must be present in the + team loadout pool
  • +
+ +

Pre-save mode

+

When saving a mission that has unresolved errors, the Error Checker opens +automatically in pre-save mode. In this mode three additional buttons appear at the +bottom of the dialog:

+ + + + + +
ButtonDescription
CancelAborts the save and returns to the editor so issues can be + addressed.
Save As IsSaves the mission despite the reported issues.
Fix and SaveApplies all available auto-corrections and then saves. + Only available when Apply Auto-Corrections is + checked.
+ +

Preferences

+

The Check Potential Issues and Apply Auto-Corrections +settings are also found in File › Preferences › Error +Checker. They share the same stored value — changing one updates the other. +Use Preferences to set the default behavior for all future check runs.

diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 9b36b37c71e..6b3704bfa26 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -58,6 +58,8 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/DebriefingDialogModel.h src/mission/dialogs/FictionViewerDialogModel.cpp src/mission/dialogs/FictionViewerDialogModel.h + src/mission/dialogs/ErrorCheckerDialogModel.cpp + src/mission/dialogs/ErrorCheckerDialogModel.h src/mission/dialogs/FormWingDialogModel.cpp src/mission/dialogs/FormWingDialogModel.h src/mission/dialogs/GlobalShipFlagsDialogModel.cpp @@ -168,6 +170,8 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/CommandBriefingDialog.h src/ui/dialogs/DebriefingDialog.cpp src/ui/dialogs/DebriefingDialog.h + src/ui/dialogs/ErrorCheckerDialog.cpp + src/ui/dialogs/ErrorCheckerDialog.h src/ui/dialogs/FictionViewerDialog.cpp src/ui/dialogs/FictionViewerDialog.h src/ui/dialogs/FormWingDialog.cpp @@ -285,6 +289,8 @@ add_file_folder("Source/UI/Panels" add_file_folder("Source/UI/Util" src/ui/util/default_dir.cpp src/ui/util/default_dir.h + src/ui/util/ErrorChecker.cpp + src/ui/util/ErrorChecker.h src/ui/util/ImageRenderer.cpp src/ui/util/ImageRenderer.h src/ui/util/menu.cpp @@ -342,6 +348,7 @@ add_file_folder("UI" ui/CustomStringsDialog.ui ui/CustomWingNamesDialog.ui ui/DebriefingDialog.ui + ui/ErrorCheckerDialog.ui ui/FictionViewerDialog.ui ui/FormWingDialog.ui ui/FredView.ui diff --git a/qtfred/src/main.cpp b/qtfred/src/main.cpp index e52d1068377..a08dd820264 100644 --- a/qtfred/src/main.cpp +++ b/qtfred/src/main.cpp @@ -29,6 +29,7 @@ // Globals needed by the engine when built in 'FRED' mode. int Fred_running = 1; +int Qtfred_running = 1; int Show_cpu = 0; // Empty functions to make fred link with the sexp_mission_set_subspace diff --git a/qtfred/src/mission/Editor.cpp b/qtfred/src/mission/Editor.cpp index 64e1d8d755c..14275f9347c 100644 --- a/qtfred/src/mission/Editor.cpp +++ b/qtfred/src/mission/Editor.cpp @@ -65,7 +65,7 @@ std::pair query_referenced_in_ai_goals(sexp_ref_type type, const return std::make_pair(-1, sexp_src::NONE); } -// Used in the FRED drop-down menu and in error_check_initial_orders +// Used in the FRED drop-down menu and in ErrorChecker::checkInitialOrders // NOTE: Certain goals (Form On Wing, Rearm, Chase Weapon, Fly To Ship) aren't listed here. This may or may not be intentional, // but if they are added in the future, it will be necessary to verify correct functionality in the various FRED dialog functions. ai_goal_list Ai_goal_list[] = { @@ -341,8 +341,8 @@ bool Editor::loadMission(const std::string& mission_name, int flags) { if (!Fred_migrated_immobile_ships.empty()) { SCP_string msg = "The \"immobile\" ship flag has been superseded by the \"don't-change-position\", and \"don't-change-orientation\" flags. " "All ships which previously had \"Does Not Move\" checked in the ship flags editor will now have both \"Does Not Change Position\" and " - "\"Does Not Change Orientation\" checked. After you close this dialog, the error checker will check for any potential issues, including " - "issues involving these flags.\n\nThe following ships have been migrated:"; + "\"Does Not Change Orientation\" checked.\n\nWould you like to open the error checker now to review any potential issues " + "involving these flags?\n\nThe following ships have been migrated:"; for (int shipnum : Fred_migrated_immobile_ships) { msg += "\n\t"; @@ -350,11 +350,14 @@ bool Editor::loadMission(const std::string& mission_name, int flags) { } truncate_message_lines(msg, 30); - _lastActiveViewport->dialogProvider->showButtonDialog(DialogType::Information, + auto z = _lastActiveViewport->dialogProvider->showButtonDialog(DialogType::Question, "Ship Flag Migration", msg, - { DialogButton::Ok }); - _lastActiveViewport->Error_checker_checks_potential_issues_once = true; + { DialogButton::Yes, DialogButton::No }); + if (z == DialogButton::Yes) { + // Consumed by FredView::autoRunErrorChecker after loadMission returns. + _lastActiveViewport->Error_checker_force_display_potentials_once = true; + } } obj_merge_created_list(); @@ -416,21 +419,7 @@ bool Editor::loadMission(const std::string& mission_name, int flags) { _weapon_usage[i][Team_data[i].weaponry_pool[j]] = 0; } } - // double check the used pool is empty - for (j = 0; j < static_cast(Weapon_info.size()); j++) { - if (_weapon_usage[i][j] != 0) { - Warning(LOCATION, - "%s is used in wings of team %d but was not in the loadout. Fixing now", - Weapon_info[j].name, - i + 1); - - // add the weapon as a new entry - Team_data[i].weaponry_pool[Team_data[i].num_weapon_choices] = j; - Team_data[i].weaponry_count[Team_data[i].num_weapon_choices] = _weapon_usage[i][j]; - strcpy_s(Team_data[i].weaponry_amount_variable[Team_data[i].num_weapon_choices], ""); - strcpy_s(Team_data[i].weaponry_pool_variable[Team_data[i].num_weapon_choices++], ""); - } - } + // Weapons used in wings but missing from the loadout pool are flagged by the error checker. } Assert(Mission_palette >= 0); @@ -2132,1324 +2121,28 @@ int Editor::get_prev_visible_subsys(ship * shipp, ship_subsys * *prev_subsys) { Int3(); // should be impossible to miss return 0; } -bool Editor::global_error_check() { - int z; - - z = global_error_check_impl(); - if (!z) { - _lastActiveViewport->dialogProvider->showButtonDialog(DialogType::Information, - "Woohoo!", - "No errors were detected in this mission", - { DialogButton::Ok }); - } - - for (z = 0; z < obj_count; z++) { - if (err_flags[z]) { - delete[] names[z]; - } - } - - obj_count = 0; - - return !z; +const ai_goal_list* Editor::getAi_goal_list() +{ + return Ai_goal_list; } -int Editor::global_error_check_impl() { - - char buf[256]; - const char* str; - int bs, i, j, n, s, t, z, ai, count, ship, wing, obj, team, point, multi; - object* ptr; - brief_stage* sp; - SCP_string anchor_message; - SCP_set anchors_checked; - - g_err = multi = 0; - if (The_mission.game_type & MISSION_TYPE_MULTI) { - multi = 1; - } - -// if (!stricmp(The_mission.name, "Untitled")) -// if (error("You haven't given this mission a title yet.\nThis is done from the Mission Specs Editor (Shift-N).")) -// return 1; - - // cycle though all the objects and verify every possible aspect of them - obj_count = t = 0; - ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - names[obj_count] = NULL; - err_flags[obj_count] = 0; - i = ptr->instance; - if ((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) { - if (i < 0 || i >= MAX_SHIPS) { - return internal_error("An object has an illegal ship index"); - } - - z = Ships[i].ship_info_index; - if ((z < 0) || (z >= static_cast(Ship_info.size()))) { - return internal_error("A ship has an illegal class"); - } - - if (ptr->type == OBJ_START) { - t++; - if (!(Ship_info[z].flags[Ship::Info_Flags::Player_ship])) { - ptr->type = OBJ_SHIP; - Player_starts--; - t--; - if (error("Invalid ship type for a player. Ship has been reset to non-player ship.")) { - return 1; - } - } - - for (n = count = 0; n < MAX_SHIP_PRIMARY_BANKS; n++) { - if (Ships[i].weapons.primary_bank_weapons[n] >= 0) { - count++; - } - } - - if (!count) { - if (error("Player \"%s\" has no primary weapons. Should have at least 1", Ships[i].ship_name)) { - return 1; - } - } - - for (n = count = 0; n < MAX_SHIP_SECONDARY_BANKS; n++) { - if (Ships[i].weapons.secondary_bank_weapons[n] >= 0) { - count++; - } - } - } - - if (Ships[i].objnum != OBJ_INDEX(ptr)) { - return internal_error("Object/ship references are corrupt"); - } - - names[obj_count] = Ships[i].ship_name; - wing = Ships[i].wingnum; - if (wing >= 0) { // ship is part of a wing, so check this - if (wing < 0 || wing >= MAX_WINGS) { // completely out of range? - return internal_error("A ship has an illegal wing index"); - } - - j = Wings[wing].wave_count; - if (!j) { - return internal_error("A ship is in a non-existent wing"); - } - - if (j < 0 || j > MAX_SHIPS_PER_WING) { - return internal_error("Invalid number of ships in wing \"%s\"", Wings[z].name); - } - - while (j--) { - if (wing_objects[wing][j] == OBJ_INDEX(ptr)) { // look for object in wing's table - break; - } - } - - if (j < 0) { - return internal_error("Ship/wing references are corrupt"); - } - - // wing squad logo check - Goober5000 - if (strlen(Wings[wing].wing_squad_filename) > 0) //-V805 - { - if (The_mission.game_type & MISSION_TYPE_MULTI) { - if (error("Wing squad logos are not displayed in multiplayer games.")) { - return 1; - } - } else { - if (ptr->type == OBJ_START) { - if (error( - "A squad logo was assigned to the player's wing. The player's squad logo will be displayed instead of the wing squad logo on ships in this wing.")) { - return 1; - } - } - } - } - } - - if ((Ships[i].flags[Ship::Ship_Flags::Kill_before_mission]) && (Ships[i].hotkey >= 0)) { - if (error("Ship flagged as \"destroy before mission start\" has a hotkey assignment")) { - return 1; - } - } - - if ((Ships[i].flags[Ship::Ship_Flags::Kill_before_mission]) && (ptr->type == OBJ_START)) { - if (error("Player start flagged as \"destroy before mission start\"")) { - return 1; - } - } - } else if (ptr->type == OBJ_WAYPOINT) { - int waypoint_num; - waypoint_list* wp_list = find_waypoint_list_with_instance(i, &waypoint_num); - - if (wp_list == NULL) { - return internal_error("Object references an illegal waypoint path number"); - } - - if (waypoint_num < 0 || (uint) waypoint_num >= wp_list->get_waypoints().size()) { - return internal_error("Object references an illegal waypoint number in path"); - } - - waypoint_stuff_name(buf, i); - names[obj_count] = new char[strlen(buf) + 1]; - strcpy(names[obj_count], buf); - err_flags[obj_count] = 1; - } else if (ptr->type == OBJ_POINT) { - //Shouldn't be needed anymore. - //If we really do need it, call me and I'll write a is_valid function for jumpnodes -WMC - } else if (ptr->type == OBJ_JUMP_NODE) { - //nothing needs to be done here, we just need to make sure the else doesn't occur - } else { - return internal_error("An unknown object type (%d) was detected", ptr->type); - } - - for (i = 0; i < obj_count; i++) { - if (names[i] && names[obj_count]) { - if (!stricmp(names[i], names[obj_count])) { - return internal_error("Duplicate object names (%s)", names[i]); - } - } - } - - obj_count++; - ptr = GET_NEXT(ptr); - } - - if (t != Player_starts) { - return internal_error("Total number of player ships is incorrect"); - } - - if (obj_count != Num_objects) { - return internal_error("Num_objects is incorrect"); - } - - count = 0; - for (i = 0; i < MAX_SHIPS; i++) { - if (Ships[i].objnum >= 0) { // is ship being used? - count++; - if (!query_valid_object(Ships[i].objnum)) { - return internal_error("Ship uses an unused object"); - } - - z = Objects[Ships[i].objnum].type; - if ((z != OBJ_SHIP) && (z != OBJ_START)) { - return internal_error("Object should be a ship, but isn't"); - } - - if (fred_check_sexp(Ships[i].arrival_cue, OPR_BOOL, "arrival cue of ship \"%s\"", Ships[i].ship_name)) { - return -1; - } - - if (fred_check_sexp(Ships[i].departure_cue, OPR_BOOL, "departure cue of ship \"%s\"", Ships[i].ship_name)) { - return -1; - } - - if (Ships[i].arrival_location != ArrivalLocation::AT_LOCATION) { - if (!Ships[i].arrival_anchor.isValid()) { - if (error("Ship \"%s\" requires a valid arrival target", Ships[i].ship_name)) { - return 1; - } - } - if (Ships[i].arrival_location == ArrivalLocation::FROM_DOCK_BAY) { - check_anchor_for_hangar_bay(anchor_message, anchors_checked, Ships[i].arrival_anchor, Ships[i].ship_name, true, true); - if (!anchor_message.empty() && error("%s", anchor_message.c_str())) { - return 1; - } - } - } - - if (Ships[i].departure_location != DepartureLocation::AT_LOCATION) { - if (!Ships[i].departure_anchor.isValid()) { - if (error("Ship \"%s\" requires a valid departure target", Ships[i].ship_name)) { - return 1; - } - } - if (Ships[i].departure_location == DepartureLocation::TO_DOCK_BAY) { - check_anchor_for_hangar_bay(anchor_message, anchors_checked, Ships[i].departure_anchor, Ships[i].ship_name, true, false); - if (!anchor_message.empty() && error("%s", anchor_message.c_str())) { - return 1; - } - } - } - - ai = Ships[i].ai_index; - if (ai < 0 || ai >= MAX_AI_INFO) { - return internal_error("AI index out of range for ship \"%s\"", Ships[i].ship_name); - } - - if (Ai_info[ai].shipnum != i) { - return internal_error("AI/ship references are corrupt"); - } - - if ((str = error_check_initial_orders(Ai_info[ai].goals, i, -1)) != nullptr) { - if (*str == '*') { - return internal_error("Initial orders error for ship \"%s\"\n\n%s", Ships[i].ship_name, str + 1); - } else if (*str == '!') { - return 1; - } else if (error("Initial orders error for ship \"%s\"\n\n%s", Ships[i].ship_name, str)) { - return 1; - } - } - - - for (dock_instance* dock_ptr = Objects[Ships[i].objnum].dock_list; dock_ptr != NULL; - dock_ptr = dock_ptr->next) { - obj = OBJ_INDEX(dock_ptr->docked_objp); - - if (!query_valid_object(obj)) { - return internal_error("Ship \"%s\" initially docked with non-existant ship", Ships[i].ship_name); - } - - if (Objects[obj].type != OBJ_SHIP && Objects[obj].type != OBJ_START) { - return internal_error("Ship \"%s\" initially docked with non-ship object", Ships[i].ship_name); - } - - ship = get_ship_from_obj(obj); - if (!ship_docking_valid(i, ship) && !ship_docking_valid(ship, i)) { - return internal_error("Docking illegal between \"%s\" and \"%s\" (initially docked)", - Ships[i].ship_name, - Ships[ship].ship_name); - } - - auto dock_list = get_docking_list(Ship_info[Ships[i].ship_info_index].model_num); - point = dock_ptr->dockpoint_used; - if (point < 0 || point >= (int)dock_list.size()) { - internal_error("Invalid docker point (\"%s\" initially docked with \"%s\")", - Ships[i].ship_name, - Ships[ship].ship_name); - } - - dock_list = get_docking_list(Ship_info[Ships[ship].ship_info_index].model_num); - point = dock_find_dockpoint_used_by_object(dock_ptr->docked_objp, &Objects[Ships[i].objnum]); - if (point < 0 || point >= (int)dock_list.size()) { - internal_error("Invalid dockee point (\"%s\" initially docked with \"%s\")", - Ships[i].ship_name, - Ships[ship].ship_name); - } - } - - wing = Ships[i].wingnum; - bool is_in_loadout_screen = (ptr->type == OBJ_START); - if (!is_in_loadout_screen && wing >= 0) { - if (multi && The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) { - for (n = 0; n < MAX_TVT_WINGS; n++) { - if (!strcmp(Wings[wing].name, TVT_wing_names[n])) { - is_in_loadout_screen = true; - break; - } - } - } else { - for (n = 0; n < MAX_STARTING_WINGS; n++) { - if (!strcmp(Wings[wing].name, Starting_wing_names[n])) { - is_in_loadout_screen = true; - break; - } - } - } - } - if (is_in_loadout_screen) { - int illegal = 0; - z = Ships[i].ship_info_index; - for (n = 0; n < MAX_SHIP_PRIMARY_BANKS; n++) { - if (Ships[i].weapons.primary_bank_weapons[n] >= 0 - && !Ship_info[z].allowed_weapons[Ships[i].weapons.primary_bank_weapons[n]]) { - illegal++; - } - } - - for (n = 0; n < MAX_SHIP_SECONDARY_BANKS; n++) { - if (Ships[i].weapons.secondary_bank_weapons[n] >= 0 - && !Ship_info[z].allowed_weapons[Ships[i].weapons.secondary_bank_weapons[n]]) { - illegal++; - } - } - - if (illegal && error("%d illegal weapon(s) found on ship \"%s\"", illegal, Ships[i].ship_name)) { - return 1; - } - } - } - } - - if (count != ship_get_num_ships()) { - return internal_error("num_ships is incorrect"); - } - - count = 0; - for (i = 0; i < MAX_WINGS; i++) { - team = -1; - j = Wings[i].wave_count; - if (j) { // is wing being used? - count++; - if (j < 0 || j > MAX_SHIPS_PER_WING) { - return internal_error("Invalid number of ships in wing \"%s\"", Wings[i].name); - } - - while (j--) { - obj = wing_objects[i][j]; - if (obj < 0 || obj >= MAX_OBJECTS) { - return internal_error("Wing_objects has an illegal object index"); - } - - if (!query_valid_object(obj)) { - return internal_error("Wing_objects references an unused object"); - } - -// Now, at this point, we can assume several things. We have a valid object because -// we passed query_valid_object(), and all valid objects were already checked above, -// so this object has valid information, such as the instance. - - if ((Objects[obj].type == OBJ_SHIP) || (Objects[obj].type == OBJ_START)) { - ship = Objects[obj].instance; - wing_bash_ship_name(buf, Wings[i].name, j + 1); - if (stricmp(buf, Ships[ship].ship_name) != 0) { - return internal_error("Ship \"%s\" in wing should be called \"%s\"", - Ships[ship].ship_name, - buf); - } - - int ship_type = ship_query_general_type(ship); - if (ship_type < 0 || !(Ship_types[ship_type].flags[Ship::Type_Info_Flags::AI_can_form_wing])) { - if (error("Ship \"%s\" is an illegal type to be in a wing", Ships[ship].ship_name)) { - return 1; - } - } - } else { - return internal_error("Wing_objects of \"%s\" references an illegal object type", Wings[i].name); - } - - if (Ships[ship].wingnum != i) { - return internal_error("Wing/ship references are corrupt"); - } - - if (ship != Wings[i].ship_index[j]) { - return internal_error("Ship/wing references are corrupt"); - } - - if (team < 0) { - team = Ships[ship].team; - } else if (team != Ships[ship].team && team < 999) { - if (error("ship teams mixed within same wing (\"%s\")", Wings[i].name)) { - return 1; - } - } - } - - if ((Wings[i].special_ship < 0) || (Wings[i].special_ship >= Wings[i].wave_count)) { - return internal_error("Special ship out of range for \"%s\"", Wings[i].name); - } - - if (Wings[i].num_waves < 0) { - return internal_error("Number of waves for \"%s\" is negative", Wings[i].name); - } - - if (Wings[i].threshold < 0) { - return internal_error("Threshold for \"%s\" is invalid", Wings[i].name); - } - - if (Wings[i].threshold + Wings[i].wave_count > MAX_SHIPS_PER_WING) { - Wings[i].threshold = MAX_SHIPS_PER_WING - Wings[i].wave_count; - if (error("Threshold for wing \"%s\" is higher than allowed. Reset to %d", - Wings[i].name, - Wings[i].threshold)) { - return 1; - } - } - - for (j = 0; j < obj_count; j++) { - if (names[j]) { - if (!stricmp(names[j], Wings[i].name)) { - return internal_error("Wing name is also used by an object (%s)", names[j]); - } - } - } - - if (fred_check_sexp(Wings[i].arrival_cue, OPR_BOOL, "arrival cue of wing \"%s\"", Wings[i].name)) { - return -1; - } - - if (fred_check_sexp(Wings[i].departure_cue, OPR_BOOL, "departure cue of wing \"%s\"", Wings[i].name)) { - return -1; - } - - if (Wings[i].arrival_location != ArrivalLocation::AT_LOCATION) { - if (!Wings[i].arrival_anchor.isValid()) { - if (error("Wing \"%s\" requires a valid arrival target", Wings[i].name)) { - return 1; - } - } - if (Wings[i].arrival_location == ArrivalLocation::FROM_DOCK_BAY) { - check_anchor_for_hangar_bay(anchor_message, anchors_checked, Wings[i].arrival_anchor, Wings[i].name, false, true); - if (!anchor_message.empty() && error("%s", anchor_message.c_str())) { - return 1; - } - } - } - - if (Wings[i].departure_location != DepartureLocation::AT_LOCATION) { - if (!Wings[i].departure_anchor.isValid()) { - if (error("Wing \"%s\" requires a valid departure target", Wings[i].name)) { - return 1; - } - } - if (Wings[i].departure_location == DepartureLocation::TO_DOCK_BAY) { - check_anchor_for_hangar_bay(anchor_message, anchors_checked, Wings[i].departure_anchor, Wings[i].name, false, false); - if (!anchor_message.empty() && error("%s", anchor_message.c_str())) { - return 1; - } - } - } - - if ((str = error_check_initial_orders(Wings[i].ai_goals, -1, i)) != nullptr) { - if (*str == '*') { - return internal_error("Initial orders error for wing \"%s\"\n\n%s", Wings[i].name, str + 1); - } else if (*str == '!') { - return 1; - } else if (error("Initial orders error for wing \"%s\"\n\n%s", Wings[i].name, str)) { - return 1; - } - } - - } - } - - if (count != Num_wings) { - return internal_error("Num_wings is incorrect"); - } - - for (const auto &ii: Waypoint_lists) { - for (z = 0; z < obj_count; z++) { - if (names[z]) { - if (!stricmp(names[z], ii.get_name())) { - return internal_error("Waypoint path name is also used by an object (%s)", names[z]); - } - } - } - - for (const auto &jj: ii.get_waypoints()) { - waypoint_stuff_name(buf, jj); - for (z = 0; z < obj_count; z++) { - if (names[z]) { - if (!stricmp(names[z], buf)) { - break; - } - } - } - - if (z == obj_count) { - return internal_error("Waypoint \"%s\" not linked to an object", buf); - } - } - } - - if (Player_starts > MAX_PLAYERS) { - return internal_error("Number of player starts exceeds max limit"); - } - - if (!multi && (Player_starts > 1)) { - if (error("Multiple player starts exist, but this is a single player mission")) { - return 1; - } - } - - if (Num_reinforcements > MAX_REINFORCEMENTS) { - return internal_error("Number of reinforcements exceeds max limit"); - } - - for (i = 0; i < Num_reinforcements; i++) { - z = 0; - for (ship = 0; ship < MAX_SHIPS; ship++) { - if ((Ships[ship].objnum >= 0) && !stricmp(Ships[ship].ship_name, Reinforcements[i].name)) { - z = 1; - break; - } - } - - for (wing = 0; wing < MAX_WINGS; wing++) { - if (Wings[wing].wave_count && !stricmp(Wings[wing].name, Reinforcements[i].name)) { - z = 1; - break; - } - } - - if (!z) { - return internal_error("Reinforcement name not found in ships or wings"); - } - } - -/* for (i=0; i= MAX_SHIPS) // hacked! -1 should be illegal.. - return internal_error("Message originator index is out of range"); - - if (Ships[z].objnum == -1) - return internal_error("Message originator points to nonexistant ship"); +int Editor::getAigoal_list_size() { + // sizeof works here because Ai_goal_list is defined as an array in this same TU (above); + // keep this function defined alongside that array so it sees the array type, not a pointer. + return sizeof(Ai_goal_list) / sizeof(ai_goal_list); +} +SCP_vector Editor::get_docking_list(int model_index) { + int i; + polymodel *pm; + SCP_vector out; - if (fred_check_sexp(Messages[i].sexp, OPR_BOOL, - "Message formula from \"%s\"", Ships[Messages[i].who_from].ship_name)) - return -1; - }*/ + pm = model_get(model_index); + out.reserve(pm->n_docks); - Assert( - (Player_start_shipnum >= 0) && (Player_start_shipnum < MAX_SHIPS) && (Ships[Player_start_shipnum].objnum >= 0)); - i = global_error_check_player_wings(multi); - if (i) { - return i; - } - - for (i = 0; i < (int)Mission_events.size(); i++) { - if (fred_check_sexp(Mission_events[i].formula, OPR_NULL, "mission event \"%s\"", Mission_events[i].name.c_str())) { - return -1; - } - } - - for (i = 0; i < (int)Mission_goals.size(); i++) { - if (fred_check_sexp(Mission_goals[i].formula, OPR_BOOL, "mission goal \"%s\"", Mission_goals[i].name.c_str())) { - return -1; - } - } - - for (bs = 0; bs < Num_teams; bs++) { - for (s = 0; s < Briefings[bs].num_stages; s++) { - sp = &Briefings[bs].stages[s]; - t = sp->num_icons; - for (i = 0; i < t - 1; i++) { - for (j = i + 1; j < t; j++) { - if ((sp->icons[i].id > 0) && (sp->icons[i].id == sp->icons[j].id)) { - if (error("Duplicate icon IDs %d in briefing stage %d", sp->icons[i].id, s + 1)) { - return 1; - } - } - } - } - } - } - - for (j = 0; j < Num_teams; j++) { - for (i = 0; i < Debriefings[j].num_stages; i++) { - if (fred_check_sexp(Debriefings[j].stages[i].formula, OPR_BOOL, "debriefing stage %d", i + 1)) { - return -1; - } - } - } - - // for all wings, be sure that the orders accepted for all ships are the same for all ships - // in the wing - for (i = 0; i < MAX_WINGS; i++) { - int starting_wing; - - if (!Wings[i].wave_count) { - continue; - } - - // determine if this wing is a starting wing of the player - starting_wing = (ship_starting_wing_lookup(Wings[i].name) != -1); - - // first, be sure this isn't a reinforcement wing. - if (starting_wing && (Wings[i].flags[Ship::Wing_Flags::Reinforcement])) { - if (error( - "Starting Wing %s marked as reinforcement. This wing\nshould either be renamed, or unmarked as reinforcement.", - Wings[i].name)) { - return 1; - } - } - - std::set default_orders; - int default_orders_idx = -1; - for (j = 0; j < Wings[i].wave_count; j++) { - // exclude players from the check - if (Objects[Ships[Wings[i].ship_index[j]].objnum].type == OBJ_START) { - continue; - } - - const std::set& orders = Ships[Wings[i].ship_index[j]].orders_accepted; - - if (default_orders_idx < 0) { - default_orders_idx = j; - default_orders = orders; - - } else if (default_orders != orders) { - if (error( - "%s and %s will accept different orders. All ships in a wing must accept the same Player Orders.", - Ships[Wings[i].ship_index[j]].ship_name, - Ships[Wings[i].ship_index[default_orders_idx]].ship_name)) { - return 1; - } - } - } - -/* Goober5000 - this is not necessary - // make sure that these ignored orders are the same for all starting wings of the player - if ( starting_wing ) { - if ( starting_orders == -1 ) { - starting_orders = default_orders; - } else { - if ( starting_orders != default_orders ) { - if ( error("Player starting wing %s has orders which don't match other starting wings\n", Wings[i].name) ){ - return 1; - } - } - } - } -*/ - } - - //This should never ever be a problem -WMC - /* - if (Num_jump_nodes < 0){ - return internal_error("Jump node count is illegal"); - }*/ - - // FIXME: This call was in the original function but the code of that function was entirely commented out - //fred_check_message_personas(); - - return g_err; -} -int Editor::error(const char* msg, ...) { - char buf[2048]; - va_list args; - - va_start(args, msg); - vsnprintf(buf, sizeof(buf) - 1, msg, args); - va_end(args); - buf[sizeof(buf) - 1] = '\0'; - - g_err = 1; - if (_lastActiveViewport->dialogProvider->showButtonDialog(DialogType::Error, - "Error", - buf, - { DialogButton::Ok, DialogButton::Cancel }) - == DialogButton::Ok) { - return 0; - } - - return 1; -} -int Editor::internal_error(const char* msg, ...) { - SCP_string buf; - va_list args; - - va_start(args, msg); - vsprintf(buf, msg, args); - va_end(args); - - g_err = 1; - -#ifndef NDEBUG - buf += "\n\nThis is an internal error. Please notify a coder about this. Click cancel to debug."; - - if (_lastActiveViewport->dialogProvider->showButtonDialog(DialogType::Error, - "Internal Error", - buf, - { DialogButton::Ok, DialogButton::Cancel }) - == DialogButton::Cancel) - Int3(); // drop to debugger so the problem can be analyzed. -#else - _lastActiveViewport->dialogProvider->showButtonDialog(DialogType::Error, "Error", buf, { DialogButton::Ok }); -#endif - - return -1; -} -int Editor::fred_check_sexp(int sexp, int type, const char* location, ...) { - SCP_string location_buf, sexp_buf, error_buf, bad_node_str, issue_msg; - int err = 0, z, faulty_node; - va_list args; - - va_start(args, location); - vsprintf(location_buf, location, args); - va_end(args); - - if (sexp == -1) - return 0; - - z = check_sexp_syntax(sexp, type, 1, &faulty_node); - if (z) - { - convert_sexp_to_string(sexp_buf, sexp, SEXP_ERROR_CHECK_MODE); - truncate_message_lines(sexp_buf, 30); - - stuff_sexp_text_string(bad_node_str, faulty_node, SEXP_ERROR_CHECK_MODE); - if (!bad_node_str.empty()) // the previous function adds a space at the end - bad_node_str.pop_back(); - - sprintf(error_buf, "Error in %s: %s\n\n%s\n\n(Bad node appears to be: %s)", location_buf.c_str(), sexp_error_message(z), sexp_buf.c_str(), bad_node_str.c_str()); - - if (z < 0 && z > -100) - err = 1; - - if (err) - return internal_error("%s", error_buf.c_str()); - - if (error("%s", error_buf.c_str())) - return 1; - } - - if (_lastActiveViewport->Error_checker_checks_potential_issues || _lastActiveViewport->Error_checker_checks_potential_issues_once) - z = check_sexp_potential_issues(sexp, &faulty_node, issue_msg); - if (z) - { - convert_sexp_to_string(sexp_buf, sexp, SEXP_ERROR_CHECK_MODE); - truncate_message_lines(sexp_buf, 30); - - stuff_sexp_text_string(bad_node_str, faulty_node, SEXP_ERROR_CHECK_MODE); - if (!bad_node_str.empty()) // the previous function adds a space at the end - bad_node_str.pop_back(); - - sprintf(error_buf, "Potential issue detected in %s:\n\n%s\n\n%s\n\n(Suspect node appears to be: %s)", location_buf.c_str(), issue_msg.c_str(), sexp_buf.c_str(), bad_node_str.c_str()); - - if (_lastActiveViewport->dialogProvider->showButtonDialog(DialogType::Warning, "Warning", error_buf.c_str(), { DialogButton::Ok, DialogButton::Cancel }) != DialogButton::Ok) - return 1; - } - _lastActiveViewport->Error_checker_checks_potential_issues_once = false; - - return 0; -} -const char* Editor::error_check_initial_orders(ai_goal* goals, int ship, int wing) { - char *source; - int i, j, flag, found, inst, team, team2; - object *ptr; - - if (ship >= 0) { - source = Ships[ship].ship_name; - team = Ships[ship].team; - for (i=0; i= 0); - Assert(Wings[wing].wave_count > 0); - source = Wings[wing].name; - team = Ships[Objects[wing_objects[wing][0]].instance].team; - for (j=0; j 0) { - if (*goals[i].target_name == '<') - return "Invalid target"; - - if (!stricmp(goals[i].target_name, source)) { - if (ship >= 0) - return "Target of ship's goal is itself"; - else - return "Target of wing's goal is itself"; - } - } - - inst = team2 = -1; - if (flag == 1) { // target waypoint required - if (find_matching_waypoint_list(goals[i].target_name) == NULL) - return "*Invalid target waypoint path name"; - - } else if (flag == 2) { // target ship required - ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - if (ptr->type == OBJ_SHIP || ptr->type == OBJ_START) { - inst = ptr->instance; - if (!stricmp(goals[i].target_name, Ships[inst].ship_name)) { - found = 1; - break; - } - } - - ptr = GET_NEXT(ptr); - } - - if (!found) - return "*Invalid target ship name"; - - if (wing >= 0) { // check if target ship is in wing - if (Ships[inst].wingnum == wing && Objects[Ships[inst].objnum].type != OBJ_START) - return "Target ship of wing's goal is within said wing"; - } - - team2 = Ships[inst].team; - - } else if (flag == 3) { // target wing required - for (j=0; j= MAX_WINGS) - return "*Invalid target wing name"; - - if (ship >= 0) { // check if ship is in target wing - if (Ships[ship].wingnum == j) - return "Target wing of ship's goal is same wing said ship is part of"; - } - - team2 = Ships[Objects[wing_objects[j][0]].instance].team; - - } else if (flag == 4) { - ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - if (ptr->type == OBJ_SHIP || ptr->type == OBJ_START) { - inst = ptr->instance; - if (!stricmp(goals[i].target_name, Ships[inst].ship_name)) { - found = 2; - break; - } - - } else if (ptr->type == OBJ_WAYPOINT) { - if (!stricmp(goals[i].target_name, object_name(OBJ_INDEX(ptr)))) { - found = 1; - break; - } - } - - ptr = GET_NEXT(ptr); - } - - if (!found) - return "*Invalid target ship or waypoint name"; - - if (found == 2) { - if (wing >= 0) { // check if target ship is in wing - if (Ships[inst].wingnum == wing && Objects[Ships[inst].objnum].type != OBJ_START) - return "Target ship of wing's goal is within said wing"; - } - - team2 = Ships[inst].team; - } - } - - switch (goals[i].ai_mode) { - case AI_GOAL_DESTROY_SUBSYSTEM: - Assert(flag == 2 && inst >= 0); - if (ship_find_subsys(&Ships[inst], goals[i].docker.name) < 0) - return "Unknown subsystem type"; - - break; - - case AI_GOAL_DOCK: { - int dock1 = -1, dock2 = -1, model1, model2; - - Assert(flag == 2 && inst >= 0); - if (!ship_docking_valid(ship, inst)) - return "Docking illegal between given ship types"; - - model1 = Ship_info[Ships[ship].ship_info_index].model_num; - auto model1Docks = get_docking_list(model1); - for (j = 0; j < (int)model1Docks.size(); ++j) { - if (!stricmp(goals[i].docker.name, model1Docks[j].c_str())) { - dock1 = j; - break; - } - } - - model2 = Ship_info[Ships[inst].ship_info_index].model_num; - auto model2Docks = get_docking_list(model2); - for (j = 0; j < (int)model2Docks.size(); ++j) { - if (!stricmp(goals[i].dockee.name, model2Docks[j].c_str())) { - dock2 = j; - break; - } - } - - if (dock1 < 0) - return "Invalid docker point"; - - if (dock2 < 0) - return "Invalid dockee point"; - - if ((dock1 >= 0) && (dock2 >= 0)) { - if ( !(model_get_dock_index_type(model1, dock1) & model_get_dock_index_type(model2, dock2)) ) - return "Dock points are incompatible"; - } - - break; - } - - default: - break; - } - - switch (goals[i].ai_mode) { - case AI_GOAL_GUARD: - case AI_GOAL_GUARD_WING: - if (team != team2) { // MK, added support for TEAM_NEUTRAL. Won't this work? - if (ship >= 0) - return "Ship assigned to guard a different team"; - else - return "Wing assigned to guard a different team"; - } - break; - - case AI_GOAL_CHASE: - case AI_GOAL_CHASE_WING: - case AI_GOAL_DESTROY_SUBSYSTEM: - case AI_GOAL_DISARM_SHIP: - case AI_GOAL_DISARM_SHIP_TACTICAL: - case AI_GOAL_DISABLE_SHIP: - case AI_GOAL_DISABLE_SHIP_TACTICAL: - if (team == team2) { - if (ship >= 0) - return "Ship assigned to attack same team"; - else - return "Wings assigned to attack same team"; - } - break; - - default: - break; - } - } - - return NULL; -} -const char* Editor::get_order_name(ai_goal_mode order) { - if (order == AI_GOAL_NONE) // special case - return "None"; - - for (auto& entry : Ai_goal_list) - if (entry.def == order) - return entry.name; - - return "???"; -} -const ai_goal_list* Editor::getAi_goal_list() -{ - return Ai_goal_list; -} -int Editor::getAigoal_list_size() { - return sizeof(Ai_goal_list) / sizeof(ai_goal_list); -} -SCP_vector Editor::get_docking_list(int model_index) { - int i; - polymodel *pm; - SCP_vector out; - - pm = model_get(model_index); - out.reserve(pm->n_docks); - - for (i=0; in_docks; i++) - out.push_back(pm->docking_bays[i].name); + for (i=0; in_docks; i++) + out.emplace_back(pm->docking_bays[i].name); return out; } -int Editor::global_error_check_player_wings(int multi) { - int i, z, err; - int starting_wing_count[MAX_STARTING_WINGS]; - int tvt_wing_count[MAX_TVT_WINGS]; - - object *ptr; - SCP_string starting_wing_list = ""; - SCP_string tvt_wing_list = ""; - - // check team wings in tvt - if ( multi && The_mission.game_type & MISSION_TYPE_MULTI_TEAMS ) - { - for (i=0; i 1 wave for multiplayer - if ( multi ) - { - if ( The_mission.game_type & MISSION_TYPE_MULTI_TEAMS ) - { - for (i=0; i= 0 && Wings[TVT_wings[i]].num_waves > 1) - { - Wings[TVT_wings[i]].num_waves = 1; - if (error("%s wing must contain only 1 wave.\nThis change has been made for you.", TVT_wing_names[i])) - return 1; - } - } - } - else - { - for (i=0; i= 0 && Wings[Starting_wings[i]].num_waves > 1) - { - Wings[Starting_wings[i]].num_waves = 1; - if (error("%s wing must contain only 1 wave.\nThis change has been made for you.", Starting_wing_names[i])) - return 1; - } - } - } - } - - // check number of ships in player wing - if ( multi && The_mission.game_type & MISSION_TYPE_MULTI_TEAMS ) - { - for (i=0; i= 0 && Wings[TVT_wings[i]].wave_count > 4) - { - if (error("%s wing has too many ships. Should only have 4 max.", TVT_wing_names[i])) - return 1; - } - } - } - else - { - for (i=0; i= 0 && Wings[Starting_wings[i]].wave_count > 4) - { - if (error("%s wing has too many ships. Should only have 4 max.", Starting_wing_names[i])) - return 1; - } - } - } - - // check arrival delay in tvt - if ( multi && The_mission.game_type & MISSION_TYPE_MULTI_TEAMS ) - { - for (i=0; i= 0 && Wings[TVT_wings[i]].arrival_delay > 0) - { - if (error("%s wing shouldn't have a non-zero arrival delay", TVT_wing_names[i])) - return 1; - } - } - } - - // check mixed-species in a wing for multi missions - if (multi) - { - if ( The_mission.game_type & MISSION_TYPE_MULTI_TEAMS ) - { - for (i=0; i= 0) - { - if (global_error_check_mixed_player_wing(TVT_wings[i])) - return 1; - } - } - } - else - { - for (i=0; i= 0) - { - if (global_error_check_mixed_player_wing(Starting_wings[i])) - return 1; - } - } - } - } - - for (i=0; i 2) - starting_wing_list += ","; - starting_wing_list += " "; - } - else - { - starting_wing_list += "or "; - starting_wing_list += Starting_wing_names[i]; - } - } - for (i=0; i 2) - tvt_wing_list += ","; - tvt_wing_list += " "; - } - else - { - tvt_wing_list += "or "; - tvt_wing_list += TVT_wing_names[i]; - } - } - - // check players in wings - ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) - { - int ship_instance = ptr->instance; - err = 0; - - // this ship is a player? - if (ptr->type == OBJ_START) - { - // check if this ship is in a wing - z = Ships[ship_instance].wingnum; - if (z < 0) - { - err = 1; - } - else - { - int in_starting_wing = 0; - int in_tvt_wing = 0; - - // check which wing the player is in - for (i=0; i& teams, const SCP_vector& types) const { Assertion(Shield_sys_teams.size() == teams.size(), "Mismatched shield data from global shield dialog!"); diff --git a/qtfred/src/mission/Editor.h b/qtfred/src/mission/Editor.h index ecb005f913c..08636d1d70c 100644 --- a/qtfred/src/mission/Editor.h +++ b/qtfred/src/mission/Editor.h @@ -3,7 +3,6 @@ #include "EditorViewport.h" #include "FredRenderer.h" -#include #include #include #include @@ -265,9 +264,7 @@ class Editor : public QObject { void select_next_object(); void select_previous_object(); - bool global_error_check(); - - SCP_vector get_docking_list(int model_index); + static SCP_vector get_docking_list(int model_index); bool compareShieldSysData(const SCP_vector& teams, const SCP_vector& types) const; void exportShieldSysData(SCP_vector& teams, SCP_vector& types) const; @@ -283,7 +280,8 @@ class Editor : public QObject { static const ai_goal_list* getAi_goal_list(); static int getAigoal_list_size(); - const char* error_check_initial_orders(ai_goal* goals, int ship, int wing); + + void generate_team_weaponry_usage_list(int team, int* arr); private: void clearMission(bool fast_reload = false); @@ -309,12 +307,6 @@ class Editor : public QObject { bool already_deleting_wing = false; - // used by error checker, but needed in more than just one function. - char* names[MAX_OBJECTS]; - char err_flags[MAX_OBJECTS]; - int obj_count = 0; - int g_err = 0; - // ship and weapon usage pools int _ship_usage[MAX_TVT_TEAMS][MAX_SHIP_CLASSES]; int _weapon_usage[MAX_TVT_TEAMS][MAX_WEAPON_TYPES]; @@ -365,8 +357,6 @@ class Editor : public QObject { void generate_wing_weaponry_usage_list(int* arr, int wing); - void generate_team_weaponry_usage_list(int team, int* arr); - void generate_ship_usage_list(int* arr, int wing); int get_visible_sub_system_count(ship* shipp); @@ -375,20 +365,6 @@ class Editor : public QObject { int get_prev_visible_subsys(ship* shipp, ship_subsys** prev_subsys); - int global_error_check_impl(); - - int error(SCP_FORMAT_STRING const char* msg, ...) SCP_FORMAT_STRING_ARGS(2, 3); - int internal_error(SCP_FORMAT_STRING const char* msg, ...) SCP_FORMAT_STRING_ARGS(2, 3); - - int fred_check_sexp(int sexp, int type, const char* location, ...); - - - int global_error_check_mixed_player_wing(int w); - - int global_error_check_player_wings(int multi); - - static const char* get_order_name(ai_goal_mode order); - void updateStartingWingLoadoutUseCounts(); }; diff --git a/qtfred/src/mission/EditorViewport.cpp b/qtfred/src/mission/EditorViewport.cpp index cc382babeca..5cd10840271 100644 --- a/qtfred/src/mission/EditorViewport.cpp +++ b/qtfred/src/mission/EditorViewport.cpp @@ -133,6 +133,7 @@ void EditorViewport::loadSettings() { Move_ships_when_undocking = settings.value("move_ships_when_undocking", Move_ships_when_undocking).toBool(); Always_save_display_names = settings.value("always_save_display_names", Always_save_display_names).toBool(); Error_checker_checks_potential_issues = settings.value("error_checker_checks_potential_issues", Error_checker_checks_potential_issues).toBool(); + Error_checker_apply_auto_corrections = settings.value("error_checker_apply_auto_corrections", Error_checker_apply_auto_corrections).toBool(); Show_sexp_help_mission_events = settings.value("show_sexp_help_mission_events", Show_sexp_help_mission_events).toBool(); Show_sexp_help_mission_goals = settings.value("show_sexp_help_mission_goals", Show_sexp_help_mission_goals).toBool(); Show_sexp_help_mission_cutscenes = settings.value("show_sexp_help_mission_cutscenes", Show_sexp_help_mission_cutscenes).toBool(); @@ -173,6 +174,7 @@ void EditorViewport::saveSettings() const { settings.setValue("move_ships_when_undocking", Move_ships_when_undocking); settings.setValue("always_save_display_names", Always_save_display_names); settings.setValue("error_checker_checks_potential_issues", Error_checker_checks_potential_issues); + settings.setValue("error_checker_apply_auto_corrections", Error_checker_apply_auto_corrections); settings.setValue("show_sexp_help_mission_events", Show_sexp_help_mission_events); settings.setValue("show_sexp_help_mission_goals", Show_sexp_help_mission_goals); settings.setValue("show_sexp_help_mission_cutscenes", Show_sexp_help_mission_cutscenes); diff --git a/qtfred/src/mission/EditorViewport.h b/qtfred/src/mission/EditorViewport.h index ce19e1a49b4..bd12eae0624 100644 --- a/qtfred/src/mission/EditorViewport.h +++ b/qtfred/src/mission/EditorViewport.h @@ -185,7 +185,11 @@ class EditorViewport { bool Move_ships_when_undocking = true; bool Always_save_display_names = false; bool Error_checker_checks_potential_issues = true; - bool Error_checker_checks_potential_issues_once = false; + bool Error_checker_apply_auto_corrections = true; + // One-shot override: when set, the next auto-run of the error checker shows + // the dialog and forces potential issues on regardless of the user's saved + // preference. Consumed (cleared) by autoRunErrorChecker. Not persisted. + bool Error_checker_force_display_potentials_once = false; bool Show_sexp_help_mission_events = true; bool Show_sexp_help_mission_goals = true; diff --git a/qtfred/src/mission/dialogs/ErrorCheckerDialogModel.cpp b/qtfred/src/mission/dialogs/ErrorCheckerDialogModel.cpp new file mode 100644 index 00000000000..5ff3f249b9f --- /dev/null +++ b/qtfred/src/mission/dialogs/ErrorCheckerDialogModel.cpp @@ -0,0 +1,60 @@ +#include "ErrorCheckerDialogModel.h" + +#include "mission/EditorViewport.h" + +namespace fso::fred::dialogs { + + +ErrorCheckerDialogModel::ErrorCheckerDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) + , _checker(std::make_unique(viewport)) +{ +} + +bool ErrorCheckerDialogModel::apply() { + return true; +} + +void ErrorCheckerDialogModel::reject() { +} + +bool ErrorCheckerDialogModel::runCheck() { + _checker = std::make_unique(_viewport); + bool errors = _checker->runFullCheck(); + _hasBeenRun = true; + modelChanged(); + return errors; +} + +void ErrorCheckerDialogModel::clearErrors() { + _hasBeenRun = false; + modelChanged(); +} + +bool ErrorCheckerDialogModel::hasBeenRun() const { + return _hasBeenRun; +} + +const SCP_vector& ErrorCheckerDialogModel::getErrors() const { + return _checker->getErrors(); +} + +bool ErrorCheckerDialogModel::getCheckPotentialIssues() const { + return _viewport->Error_checker_checks_potential_issues; +} + +void ErrorCheckerDialogModel::setCheckPotentialIssues(bool value) { + _viewport->Error_checker_checks_potential_issues = value; + _viewport->saveSettings(); +} + +bool ErrorCheckerDialogModel::getApplyAutoCorrections() const { + return _viewport->Error_checker_apply_auto_corrections; +} + +void ErrorCheckerDialogModel::setApplyAutoCorrections(bool value) { + _viewport->Error_checker_apply_auto_corrections = value; + _viewport->saveSettings(); +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/ErrorCheckerDialogModel.h b/qtfred/src/mission/dialogs/ErrorCheckerDialogModel.h new file mode 100644 index 00000000000..8a2b6758575 --- /dev/null +++ b/qtfred/src/mission/dialogs/ErrorCheckerDialogModel.h @@ -0,0 +1,37 @@ +#pragma once + +#include "AbstractDialogModel.h" +#include "ui/util/ErrorChecker.h" + +#include + +namespace fso::fred::dialogs { + +class ErrorCheckerDialogModel : public AbstractDialogModel { + Q_OBJECT + +public: + ErrorCheckerDialogModel(QObject* parent, EditorViewport* viewport); + ~ErrorCheckerDialogModel() override = default; + + bool apply() override; + void reject() override; + + bool runCheck(); + void clearErrors(); + + bool hasBeenRun() const; + const SCP_vector& getErrors() const; + + bool getCheckPotentialIssues() const; + void setCheckPotentialIssues(bool value); + + bool getApplyAutoCorrections() const; + void setApplyAutoCorrections(bool value); + +private: + std::unique_ptr _checker; + bool _hasBeenRun = false; +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/PreferencesDialogModel.cpp b/qtfred/src/mission/dialogs/PreferencesDialogModel.cpp index 3e4251339a9..49e581a9107 100644 --- a/qtfred/src/mission/dialogs/PreferencesDialogModel.cpp +++ b/qtfred/src/mission/dialogs/PreferencesDialogModel.cpp @@ -12,7 +12,8 @@ PreferencesDialogModel::PreferencesDialogModel(QObject* parent, EditorViewport* , _offerAutosaveRecovery(viewport->Offer_autosave_recovery) , _moveShipsWhenUndocking(viewport->Move_ships_when_undocking) , _alwaysSaveDisplayNames(viewport->Always_save_display_names) - , _errorCheckerChecksForPotentialIssues(viewport->Error_checker_checks_potential_issues) + , _checkPotentialIssues(viewport->Error_checker_checks_potential_issues) + , _applyAutoCorrections(viewport->Error_checker_apply_auto_corrections) , _showSexpHelpMissionEvents(viewport->Show_sexp_help_mission_events) , _showSexpHelpMissionGoals(viewport->Show_sexp_help_mission_goals) , _showSexpHelpMissionCutscenes(viewport->Show_sexp_help_mission_cutscenes) @@ -43,8 +44,9 @@ PreferencesDialogModel::PreferencesDialogModel(QObject* parent, EditorViewport* bool PreferencesDialogModel::apply() { _viewport->Offer_autosave_recovery = _offerAutosaveRecovery; _viewport->Move_ships_when_undocking = _moveShipsWhenUndocking; - _viewport->Always_save_display_names = _alwaysSaveDisplayNames; - _viewport->Error_checker_checks_potential_issues = _errorCheckerChecksForPotentialIssues; + _viewport->Always_save_display_names = _alwaysSaveDisplayNames; + _viewport->Error_checker_checks_potential_issues = _checkPotentialIssues; + _viewport->Error_checker_apply_auto_corrections = _applyAutoCorrections; _viewport->Show_sexp_help_mission_events = _showSexpHelpMissionEvents; _viewport->Show_sexp_help_mission_goals = _showSexpHelpMissionGoals; _viewport->Show_sexp_help_mission_cutscenes = _showSexpHelpMissionCutscenes; @@ -99,8 +101,11 @@ void PreferencesDialogModel::setMoveShipsWhenUndocking(bool value) { modify(_mov bool PreferencesDialogModel::getAlwaysSaveDisplayNames() const { return _alwaysSaveDisplayNames; } void PreferencesDialogModel::setAlwaysSaveDisplayNames(bool value) { modify(_alwaysSaveDisplayNames, value); } -bool PreferencesDialogModel::getErrorCheckerChecksForPotentialIssues() const { return _errorCheckerChecksForPotentialIssues; } -void PreferencesDialogModel::setErrorCheckerChecksForPotentialIssues(bool value) { modify(_errorCheckerChecksForPotentialIssues, value); } +bool PreferencesDialogModel::getCheckPotentialIssues() const { return _checkPotentialIssues; } +void PreferencesDialogModel::setCheckPotentialIssues(bool value) { modify(_checkPotentialIssues, value); } + +bool PreferencesDialogModel::getApplyAutoCorrections() const { return _applyAutoCorrections; } +void PreferencesDialogModel::setApplyAutoCorrections(bool value) { modify(_applyAutoCorrections, value); } bool PreferencesDialogModel::getShowSexpHelpMissionEvents() const { return _showSexpHelpMissionEvents; } void PreferencesDialogModel::setShowSexpHelpMissionEvents(bool value) { modify(_showSexpHelpMissionEvents, value); } diff --git a/qtfred/src/mission/dialogs/PreferencesDialogModel.h b/qtfred/src/mission/dialogs/PreferencesDialogModel.h index 2cad46f5ab4..cd522fcfd8f 100644 --- a/qtfred/src/mission/dialogs/PreferencesDialogModel.h +++ b/qtfred/src/mission/dialogs/PreferencesDialogModel.h @@ -27,8 +27,12 @@ class PreferencesDialogModel : public AbstractDialogModel { bool getAlwaysSaveDisplayNames() const; void setAlwaysSaveDisplayNames(bool value); - bool getErrorCheckerChecksForPotentialIssues() const; - void setErrorCheckerChecksForPotentialIssues(bool value); + // Error Checker + bool getCheckPotentialIssues() const; + void setCheckPotentialIssues(bool value); + + bool getApplyAutoCorrections() const; + void setApplyAutoCorrections(bool value); bool getShowSexpHelpMissionEvents() const; void setShowSexpHelpMissionEvents(bool value); @@ -69,7 +73,8 @@ class PreferencesDialogModel : public AbstractDialogModel { bool _offerAutosaveRecovery; bool _moveShipsWhenUndocking; bool _alwaysSaveDisplayNames; - bool _errorCheckerChecksForPotentialIssues; + bool _checkPotentialIssues; + bool _applyAutoCorrections; bool _showSexpHelpMissionEvents; bool _showSexpHelpMissionGoals; bool _showSexpHelpMissionCutscenes; diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.cpp index d9c7b5f1759..608f2c4b5f9 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.cpp @@ -1,4 +1,5 @@ #include "ShipGoalsDialogModel.h" +#include "ui/util/ErrorChecker.h" #include #include #include @@ -97,7 +98,7 @@ namespace fso { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { self_ship = ptr->instance; goalp = Ai_info[Ships[self_ship].ai_index].goals; - verify_orders(self_ship); + verify_orders(); } ptr = GET_NEXT(ptr); @@ -108,29 +109,24 @@ namespace fso { return true; } - int ShipGoalsDialogModel::verify_orders(const int ship) + int ShipGoalsDialogModel::verify_orders() { - const char* str; - SCP_string error_message; - if ((str = _editor->error_check_initial_orders(goalp, self_ship, self_wing)) != nullptr) { - if (*str == '!') - return 1; - else if (*str == '*') - str++; - - if (ship >= 0) - sprintf(error_message, "Initial orders error for ship \"%s\"\n\n%s", Ships[ship].ship_name, str); - else - error_message = str; - auto button = _viewport->dialogProvider->showButtonDialog(DialogType::Error, - "Order Error", - error_message, - { DialogButton::Ok, DialogButton::Cancel }); - if (button != DialogButton::Ok) - return 1; + ErrorChecker checker(_viewport); + if (!checker.runCheck(ErrorCheckType::InitialOrders, {goalp, self_ship, self_wing})) + return 0; + + SCP_string message; + for (const auto& entry : checker.getErrors()) { + if (!message.empty()) + message += "\n\n"; + message += entry.message; } - return 0; + auto button = _viewport->dialogProvider->showButtonDialog(DialogType::Error, + "Order Error", + message, + { DialogButton::Ok, DialogButton::Cancel }); + return (button == DialogButton::Ok) ? 0 : 1; } void ShipGoalsDialogModel::update_item(const int item) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.h index fd4ae77a06d..53fad8633c8 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.h @@ -42,7 +42,7 @@ class ShipGoalsDialogModel : public AbstractDialogModel { bool m_multi_edit; ai_goal* goalp; - int verify_orders(const int ship = -1); + int verify_orders(); void update_item(const int item); diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 52b2291cf90..b4b190355e9 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -58,6 +58,8 @@ #include #include #include +#include +#include #include #include @@ -304,6 +306,7 @@ void FredView::loadMissionFile(const QString& pathName, int flags) { fred->loadMission(pathToLoad, flags); QApplication::restoreOverrideCursor(); + autoRunErrorChecker(); } catch (const fso::fred::mission_load_error&) { QApplication::restoreOverrideCursor(); @@ -337,7 +340,80 @@ void FredView::on_actionSave_As_triggered(bool) { saveMissionAs(); } +bool FredView::performPreSaveCheck(int* outFixCount) { + // Potentials are advisory and intentionally ignored at save time; fix counts + // report only actual problems (Error/Warning/InternalError). + auto countNonPotential = [](const SCP_vector& entries) { + int n = 0; + for (const auto& e : entries) + if (e.severity != ErrorSeverity::Potential) + ++n; + return n; + }; + + // Clamp flags for the pre-save scan: no mutations, no potential issues. + const bool savedPotential = _viewport->Error_checker_checks_potential_issues; + const bool savedCorrections = _viewport->Error_checker_apply_auto_corrections; + _viewport->Error_checker_checks_potential_issues = false; + _viewport->Error_checker_apply_auto_corrections = false; + + // Use the normal error checker dialog in PreSave mode so the error cards are + // rendered identically to what the designer sees when running it manually. + dialogs::ErrorCheckerDialog dlg(this, _viewport, dialogs::ErrorCheckerDialog::Mode::PreSave); + const bool errors = dlg.runCheck(); // returns true when errors were found + + // Restore flags before any further work (including the optional fix run below). + _viewport->Error_checker_checks_potential_issues = savedPotential; + _viewport->Error_checker_apply_auto_corrections = savedCorrections; + + if (!errors) + return true; + + dlg.exec(); // modal — blocks until the designer clicks a button + + switch (dlg.preSaveAction()) { + case dialogs::ErrorCheckerDialog::PreSaveAction::Cancel: + return false; + + case dialogs::ErrorCheckerDialog::PreSaveAction::FixAndSave: { + const int beforeCount = countNonPotential(dlg.getErrors()); + + // Apply auto-corrections to in-memory data before the file is written. + _viewport->Error_checker_checks_potential_issues = false; + _viewport->Error_checker_apply_auto_corrections = true; + { + ErrorChecker fixer(_viewport); + fixer.runFullCheck(); + } + _viewport->Error_checker_apply_auto_corrections = savedCorrections; + + // Run a second clean check (no mutations) to see how many issues remain, + // so we can report to the designer exactly how many were resolved. + if (outFixCount) { + _viewport->Error_checker_checks_potential_issues = false; + ErrorChecker verifier(_viewport); + verifier.runFullCheck(); + *outFixCount = beforeCount - countNonPotential(verifier.getErrors()); + } + + _viewport->Error_checker_checks_potential_issues = savedPotential; + return true; + } + + case dialogs::ErrorCheckerDialog::PreSaveAction::SaveAsIs: + default: + return true; + } +} + bool FredView::saveMissionToCurrentPath() { + if (saveName.isEmpty()) + return saveMissionAs(); + + int fixCount = -1; + if (!performPreSaveCheck(&fixCount)) + return false; + Fred_mission_save save; save.set_save_format(_missionSaveFormat); save.set_always_save_display_names(_viewport->Always_save_display_names); @@ -346,15 +422,37 @@ bool FredView::saveMissionToCurrentPath() { save.set_fred_alt_names(Fred_alt_names); save.set_fred_callsigns(Fred_callsigns); - if (saveName.isEmpty()) { - return saveMissionAs(); - } - save.save_mission_file(saveName.replace('/', DIR_SEPARATOR_CHAR).toUtf8().constData()); _missionModified = false; + + if (fixCount > 0) + QMessageBox::information(this, tr("Auto-corrections Applied"), + tr("%n issue(s) were automatically corrected before saving.", "", fixCount)); + else if (fixCount == 0) + QMessageBox::information(this, tr("No Auto-corrections Applied"), + tr("No issues could be automatically corrected. The mission was saved with existing errors.")); + + // Keep the persistent error checker in sync if it is already open. + if (_errorCheckerDialog && _errorCheckerDialog->isVisible()) + _errorCheckerDialog->runCheck(); + return true; } + bool FredView::saveMissionAs() { + // Run the pre-save check before the file dialog so that cancelling does not + // leave the designer with a half-chosen save path. + int fixCount = -1; + if (!performPreSaveCheck(&fixCount)) + return false; + + const QString lastDir = fso::fred::util::getLastDir("missions/saveMission", CF_TYPE_MISSIONS); + saveName = QFileDialog::getSaveFileName(this, tr("Save mission"), lastDir, tr("FS2 missions (*.fs2)")); + if (saveName.isEmpty()) + return false; + + fso::fred::util::saveLastDir("missions/saveMission", saveName); + Fred_mission_save save; save.set_save_format(_missionSaveFormat); save.set_always_save_display_names(_viewport->Always_save_display_names); @@ -363,19 +461,20 @@ bool FredView::saveMissionAs() { save.set_fred_alt_names(Fred_alt_names); save.set_fred_callsigns(Fred_callsigns); - { - const QString lastDir = fso::fred::util::getLastDir("missions/saveMission", CF_TYPE_MISSIONS); - saveName = QFileDialog::getSaveFileName(this, tr("Save mission"), lastDir, tr("FS2 missions (*.fs2)")); + save.save_mission_file(saveName.replace('/', DIR_SEPARATOR_CHAR).toUtf8().constData()); + _missionModified = false; - if (saveName.isEmpty()) { - return false; - } + if (fixCount > 0) + QMessageBox::information(this, tr("Auto-corrections Applied"), + tr("%n issue(s) were automatically corrected before saving.", "", fixCount)); + else if (fixCount == 0) + QMessageBox::information(this, tr("No Auto-corrections Applied"), + tr("No issues could be automatically corrected. The mission was saved with existing errors.")); - fso::fred::util::saveLastDir("missions/saveMission", saveName); - } + // Keep the persistent error checker in sync if it is already open. + if (_errorCheckerDialog && _errorCheckerDialog->isVisible()) + _errorCheckerDialog->runCheck(); - save.save_mission_file(saveName.replace('/',DIR_SEPARATOR_CHAR).toUtf8().constData()); - _missionModified = false; return true; } @@ -631,6 +730,10 @@ void FredView::on_mission_loaded(const std::string& filepath) { // Clear browsed head ANIs so the new mission's message scan starts fresh. fso::fred::dialogs::MissionEventsDialogModel::clearBrowsedHeadAnis(); + if (_errorCheckerDialog) { + _errorCheckerDialog->clearErrors(); + } + if (_viewport != nullptr) { _viewport->reloadLayersFromMission(); _tbLayerComboDirty = true; @@ -2755,7 +2858,78 @@ void FredView::on_actionMark_Wing_triggered(bool) { } } void FredView::on_actionError_Checker_triggered(bool) { - fred->global_error_check(); + openAndRunErrorChecker(); +} + +void FredView::openAndRunErrorChecker() { + if (!_errorCheckerDialog) { + _errorCheckerDialog = new dialogs::ErrorCheckerDialog(this, _viewport); + _errorCheckerDialog->setAttribute(Qt::WA_DeleteOnClose); + connect(_errorCheckerDialog, &QObject::destroyed, this, [this]() { + _errorCheckerDialog = nullptr; + }); + } + _errorCheckerDialog->show(); + _errorCheckerDialog->raise(); + _errorCheckerDialog->activateWindow(); + _errorCheckerDialog->runCheck(); +} + +void FredView::autoRunErrorChecker() { + if (!_errorCheckerDialog) { + _errorCheckerDialog = new dialogs::ErrorCheckerDialog(this, _viewport); + _errorCheckerDialog->setAttribute(Qt::WA_DeleteOnClose); + connect(_errorCheckerDialog, &QObject::destroyed, this, [this]() { + _errorCheckerDialog = nullptr; + }); + } + + // Consume the one-shot "force review" flag (set e.g. after a data migration + // when the designer asked to review now). When set, we force the dialog open + // and override the display filter to include potentials for this session. + const bool forceReview = _viewport->Error_checker_force_display_potentials_once; + _viewport->Error_checker_force_display_potentials_once = false; + _errorCheckerDialog->setForcePotentialsDisplay(forceReview); + + // Never silently mutate mission data on an automatic (load-triggered) check. + // The designer can apply auto-corrections explicitly via the error checker dialog. + const bool savedCorrections = _viewport->Error_checker_apply_auto_corrections; + _viewport->Error_checker_apply_auto_corrections = false; + bool errors = _errorCheckerDialog->runCheck(); + _viewport->Error_checker_apply_auto_corrections = savedCorrections; + + if (forceReview) { + _errorCheckerDialog->show(); + _errorCheckerDialog->raise(); + _errorCheckerDialog->activateWindow(); + return; + } + + if (_errorCheckerDialog->isVisible()) { + if (errors) { + _errorCheckerDialog->raise(); + _errorCheckerDialog->activateWindow(); + } + return; + } + + if (!errors) { + return; + } + + QMessageBox msgBox(this); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setWindowTitle(tr("Mission Errors Detected")); + msgBox.setText(tr("Errors were detected in the mission.")); + auto* showBtn = msgBox.addButton(tr("Show Errors"), QMessageBox::AcceptRole); + msgBox.addButton(tr("Dismiss"), QMessageBox::RejectRole); + msgBox.exec(); + + if (msgBox.clickedButton() == showBtn) { + _errorCheckerDialog->show(); + _errorCheckerDialog->raise(); + _errorCheckerDialog->activateWindow(); + } } void FredView::on_actionHelp_Topics_triggered(bool) { // Keep a single instance alive for the session. The help engine's contentWidget(), diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index ffc272c2c12..d6b3697c5da 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -29,6 +29,7 @@ class Editor; class RenderWidget; namespace dialogs { +class ErrorCheckerDialog; class ShipEditorDialog; class WingEditorDialog; class PropEditorDialog; @@ -200,6 +201,15 @@ class FredView: public QMainWindow, public IDialogProvider { private: bool saveMissionToCurrentPath(); bool saveMissionAs(); + void openAndRunErrorChecker(); + void autoRunErrorChecker(); + // Runs the error checker before a save (no mutations, no potential issues). + // If errors are found, shows the error checker in PreSave mode and applies + // auto-corrections if the designer chooses "Fix and Save". + // Returns false if the save should be cancelled. + // If outFixCount is provided it is set to the number of issues auto-corrected + // (0 if Fix and Save was chosen but nothing could be fixed, -1 if not attempted). + bool performPreSaveCheck(int* outFixCount = nullptr); void saveAsTemplate(); void loadTemplate(); bool maybePromptToSaveMissionChanges(const QString& actionDescription); @@ -271,6 +281,7 @@ class FredView: public QMainWindow, public IDialogProvider { Editor* fred = nullptr; EditorViewport* _viewport = nullptr; + fso::fred::dialogs::ErrorCheckerDialog* _errorCheckerDialog = nullptr; fso::fred::dialogs::ShipEditorDialog* _shipEditorDialog = nullptr; fso::fred::dialogs::WingEditorDialog* _wingEditorDialog = nullptr; fso::fred::dialogs::PropEditorDialog* _propEditorDialog = nullptr; diff --git a/qtfred/src/ui/dialogs/ErrorCheckerDialog.cpp b/qtfred/src/ui/dialogs/ErrorCheckerDialog.cpp new file mode 100644 index 00000000000..d38041639a4 --- /dev/null +++ b/qtfred/src/ui/dialogs/ErrorCheckerDialog.cpp @@ -0,0 +1,297 @@ +#include "ErrorCheckerDialog.h" +#include "ui_ErrorCheckerDialog.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace fso::fred::dialogs { + +ErrorCheckerDialog::ErrorCheckerDialog(QWidget* parent, EditorViewport* viewport, Mode mode) + : QDialog(parent) + , ui(new Ui::ErrorCheckerDialog()) + , _model(new ErrorCheckerDialogModel(this, viewport)) + , _mode(mode) +{ + ui->setupUi(this); + connect(_model.get(), &ErrorCheckerDialogModel::modelChanged, this, &ErrorCheckerDialog::updateUi); + + initializeUi(); + updateUi(); +} + +ErrorCheckerDialog::~ErrorCheckerDialog() = default; + +bool ErrorCheckerDialog::runCheck() { + return _model->runCheck(); +} + +void ErrorCheckerDialog::clearErrors() { + _model->clearErrors(); +} + +int ErrorCheckerDialog::getErrorCount() const { + return static_cast(_model->getErrors().size()); +} + +const SCP_vector& ErrorCheckerDialog::getErrors() const { + return _model->getErrors(); +} + +void ErrorCheckerDialog::setForcePotentialsDisplay(bool force) { + if (_forcePotentialsDisplay == force) + return; + _forcePotentialsDisplay = force; + updateUi(); +} + +void ErrorCheckerDialog::on_runButton_clicked() { + _model->runCheck(); +} + +void ErrorCheckerDialog::on_closeButton_clicked() { + close(); +} + +void ErrorCheckerDialog::on_checkPotentialIssues_toggled(bool checked) { + _model->setCheckPotentialIssues(checked); +} + +void ErrorCheckerDialog::on_checkApplyAutoCorrections_toggled(bool checked) { + _model->setApplyAutoCorrections(checked); +} + +void ErrorCheckerDialog::changeEvent(QEvent* event) { + QDialog::changeEvent(event); + if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) { + updateUi(); + } +} + +void ErrorCheckerDialog::initializeUi() { + // --- Color legend bar (common to both modes) --- + // Sits between the toolbar row and the scroll area so designers always know what + // the stripe colors mean without having to hover over anything. + { + auto* legend = new QWidget(this); + auto* legendLayout = new QHBoxLayout(legend); + legendLayout->setContentsMargins(4, 2, 4, 2); + legendLayout->setSpacing(10); + + for (const auto& info : fso::fred::severity_info) { + auto* swatch = new QWidget(legend); + swatch->setFixedSize(12, 12); + swatch->setAutoFillBackground(true); + QPalette p = swatch->palette(); + p.setColor(QPalette::Window, QColor(info.r, info.g, info.b)); + swatch->setPalette(p); + swatch->setToolTip(tr(info.tooltip)); + legendLayout->addWidget(swatch); + + auto* lbl = new QLabel(tr(info.label), legend); + lbl->setToolTip(tr(info.tooltip)); + legendLayout->addWidget(lbl); + } + legendLayout->addStretch(); + + // Insert at index 1: after the toolbar layout, before the scroll area. + ui->mainLayout->insertWidget(1, legend); + } + + // --- Auto-correction nudge (Normal mode only) --- + // Sits between the legend and the scroll area. Shown by updateUi() when the + // run produced warnings and "Apply auto-corrections" is currently off, so the + // designer knows the warnings can be resolved without manual editing. + if (_mode == Mode::Normal) { + _autoFixNudge = new QLabel(this); + _autoFixNudge->setWordWrap(true); + _autoFixNudge->setContentsMargins(6, 4, 6, 4); + _autoFixNudge->hide(); + ui->mainLayout->insertWidget(2, _autoFixNudge); + } + + // --- Scroll area content (common to both modes) --- + auto* scrollContent = new QWidget(ui->errorScrollArea); + _errorLayout = new QVBoxLayout(scrollContent); + _errorLayout->setAlignment(Qt::AlignTop); + _errorLayout->setSpacing(4); + _errorLayout->setContentsMargins(4, 4, 4, 4); + ui->errorScrollArea->setWidget(scrollContent); + ui->errorScrollArea->setWidgetResizable(true); + + if (_mode == Mode::Normal) { + ui->checkPotentialIssues->setChecked(_model->getCheckPotentialIssues()); + ui->checkApplyAutoCorrections->setChecked(_model->getApplyAutoCorrections()); + return; + } + + // --- PreSave mode --- + setWindowTitle(tr("Pre-save Error Check")); + setWindowModality(Qt::WindowModal); + + // Authoring controls are not relevant for a one-shot pre-save scan + ui->runButton->hide(); + ui->checkPotentialIssues->hide(); + ui->checkApplyAutoCorrections->hide(); + + // Note explaining Fix and Save's scope, inserted above the button row + auto* scopeNote = new QLabel( + tr("\"Fix and Save\" only applies automatic corrections for simple, well-defined issues " + "(such as missing loadout pool entries). Complex errors must be addressed manually."), + this); + scopeNote->setWordWrap(true); + // Insert just before the last item (bottomLayout) in the main layout + ui->mainLayout->insertWidget(ui->mainLayout->count() - 1, scopeNote); + + // Replace the Close button with the three pre-save decision buttons + ui->closeButton->hide(); + + _fixSaveButton = new QPushButton(tr("Fix and Save"), this); + auto* saveAnywayButton = new QPushButton(tr("Save Anyway"), this); + auto* cancelButton = new QPushButton(tr("Cancel"), this); + + _fixSaveButton->setToolTip( + tr("Apply automatic corrections to simple, well-defined errors, then save.\n" + "Issues that cannot be auto-corrected will remain and must be fixed manually.")); + + // Insert before the hidden Close button so visual order matches expected flow + const int closeIdx = ui->bottomLayout->indexOf(ui->closeButton); + ui->bottomLayout->insertWidget(closeIdx, _fixSaveButton); + ui->bottomLayout->insertWidget(closeIdx + 1, saveAnywayButton); + ui->bottomLayout->insertWidget(closeIdx + 2, cancelButton); + + connect(_fixSaveButton, &QPushButton::clicked, this, [this]() { _preSaveAction = PreSaveAction::FixAndSave; accept(); }); + connect(saveAnywayButton, &QPushButton::clicked, this, [this]() { _preSaveAction = PreSaveAction::SaveAsIs; accept(); }); + connect(cancelButton, &QPushButton::clicked, this, [this]() { _preSaveAction = PreSaveAction::Cancel; reject(); }); +} + +void ErrorCheckerDialog::updateUi() { + while (QLayoutItem* item = _errorLayout->takeAt(0)) { + if (QWidget* w = item->widget()) + w->deleteLater(); + delete item; + } + + if (_autoFixNudge) + _autoFixNudge->hide(); + + if (!_model->hasBeenRun()) { + ui->statusLabel->setText(tr("No check has been run yet.")); + if (_fixSaveButton) + _fixSaveButton->setEnabled(false); + return; + } + + // Build the display list: always-run checks collect everything; the preference + // only controls whether potential issues are visible in the UI. A transient + // override (setForcePotentialsDisplay) can force potentials on for a single + // session without changing the saved preference. + const bool showPotential = _forcePotentialsDisplay || _model->getCheckPotentialIssues(); + auto errors = _model->getErrors(); + if (!showPotential) { + errors.erase(std::remove_if(errors.begin(), errors.end(), + [](const ErrorEntry& e) { return e.severity == ErrorSeverity::Potential; }), + errors.end()); + } + + if (errors.empty()) { + ui->statusLabel->setText(tr("No errors found!")); + if (_fixSaveButton) + _fixSaveButton->setEnabled(false); + return; + } + + // Sort so the UI always shows Critical → Error → Warning → Potential, + // regardless of the order checks were run. The enum is ordered by severity so a + // plain less-than comparison gives the right result. + std::sort(errors.begin(), errors.end(), [](const ErrorEntry& a, const ErrorEntry& b) { + return a.severity < b.severity; + }); + + const QColor windowColor = ui->errorScrollArea->palette().color(QPalette::Window); + const QColor cardBg = windowColor.lightness() < 128 + ? windowColor.lighter(115) + : windowColor.darker(108); + + int errorCount = 0; + int warningCount = 0; + int potentialCount = 0; + bool hasAutoFixable = false; + + for (const auto& entry : errors) { + const fso::fred::SeverityInfo& info = fso::fred::infoFor(entry.severity); + + switch (entry.severity) { + case ErrorSeverity::Error: + case ErrorSeverity::InternalError: + ++errorCount; + break; + case ErrorSeverity::Warning: + ++warningCount; + hasAutoFixable = true; + break; + case ErrorSeverity::Potential: + ++potentialCount; + break; + } + + auto* card = new QFrame(); + card->setFrameShape(QFrame::StyledPanel); + card->setFrameShadow(QFrame::Plain); + card->setAutoFillBackground(true); + QPalette cardPalette = card->palette(); + cardPalette.setColor(QPalette::Window, cardBg); + card->setPalette(cardPalette); + + auto* cardLayout = new QHBoxLayout(card); + cardLayout->setContentsMargins(0, 0, 0, 0); + cardLayout->setSpacing(0); + + auto* stripe = new QWidget(card); + stripe->setFixedWidth(5); + stripe->setAutoFillBackground(true); + QPalette stripePalette = stripe->palette(); + stripePalette.setColor(QPalette::Window, QColor(info.r, info.g, info.b)); + stripe->setPalette(stripePalette); + // Tooltip on the stripe gives context without cluttering the message text + stripe->setToolTip(tr("%1 — %2").arg(tr(info.label), tr(info.tooltip))); + cardLayout->addWidget(stripe); + + auto* label = new QLabel(QString::fromStdString(entry.message), card); + label->setWordWrap(true); + label->setContentsMargins(8, 6, 8, 6); + cardLayout->addWidget(label); + + _errorLayout->addWidget(card); + } + + QStringList parts; + if (errorCount > 0) + parts << tr("%1 error(s)").arg(errorCount); + if (warningCount > 0) + parts << tr("%1 warning(s)").arg(warningCount); + if (potentialCount > 0) + parts << tr("%1 potential issue(s)").arg(potentialCount); + ui->statusLabel->setText(parts.join(tr(", ")) + tr(" found.")); + + // "Fix and Save" is only enabled when there are entries the auto-corrector can address. + // Under the current taxonomy, only Warnings have auto-fixes; Errors must be addressed manually. + if (_fixSaveButton) + _fixSaveButton->setEnabled(hasAutoFixable); + + // Surface the auto-correction nudge when the run produced warnings and the + // designer doesn't currently have auto-corrections enabled. PreSave mode has + // the dedicated "Fix and Save" button instead, so the nudge is suppressed there. + if (_autoFixNudge && warningCount > 0 && !_model->getApplyAutoCorrections()) { + _autoFixNudge->setText(tr("%1 warning(s) can be fixed automatically. Enable Apply auto-corrections and re-run.").arg(warningCount)); + _autoFixNudge->show(); + } +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ErrorCheckerDialog.h b/qtfred/src/ui/dialogs/ErrorCheckerDialog.h new file mode 100644 index 00000000000..77df4c1502d --- /dev/null +++ b/qtfred/src/ui/dialogs/ErrorCheckerDialog.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include + +#include "mission/dialogs/ErrorCheckerDialogModel.h" + +class QLabel; +class QVBoxLayout; + +namespace fso::fred::dialogs { + +namespace Ui { +class ErrorCheckerDialog; +} + +class ErrorCheckerDialog final : public QDialog { + Q_OBJECT + +public: + enum class Mode { Normal, PreSave }; + + // Action chosen by the designer in PreSave mode. + // Only meaningful after exec() returns in PreSave mode. + enum class PreSaveAction { Cancel, SaveAsIs, FixAndSave }; + + explicit ErrorCheckerDialog(QWidget* parent, EditorViewport* viewport, Mode mode = Mode::Normal); + ~ErrorCheckerDialog() override; + + PreSaveAction preSaveAction() const { return _preSaveAction; } + + // Number of entries in the most recent check result (0 if not yet run). + int getErrorCount() const; + const SCP_vector& getErrors() const; + + // Force potential issues to be displayed on the next updateUi pass even if + // the user's saved preference has them hidden. Does not persist and does + // not affect checkbox state. Intended for contexts that want to draw + // attention to potentials without overwriting user preferences (e.g. after + // a data migration). Caller can clear by passing false. + void setForcePotentialsDisplay(bool force); + +public slots: // NOLINT(readability-redundant-access-specifiers) + bool runCheck(); // returns true if errors were found + void clearErrors(); + +private slots: + void on_runButton_clicked(); + void on_closeButton_clicked(); + void on_checkPotentialIssues_toggled(bool checked); + void on_checkApplyAutoCorrections_toggled(bool checked); + +protected: + void changeEvent(QEvent* event) override; + +private: // NOLINT(readability-redundant-access-specifiers) + void initializeUi(); + void updateUi(); + + std::unique_ptr ui; + std::unique_ptr _model; + QVBoxLayout* _errorLayout = nullptr; + QLabel* _autoFixNudge = nullptr; // Normal mode only; populated/shown in updateUi + + Mode _mode = Mode::Normal; + PreSaveAction _preSaveAction = PreSaveAction::Cancel; + QPushButton* _fixSaveButton = nullptr; // PreSave mode only; used in updateUi + bool _forcePotentialsDisplay = false; +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/PreferencesDialog.cpp b/qtfred/src/ui/dialogs/PreferencesDialog.cpp index dbc9b47a3a7..328d074d352 100644 --- a/qtfred/src/ui/dialogs/PreferencesDialog.cpp +++ b/qtfred/src/ui/dialogs/PreferencesDialog.cpp @@ -87,7 +87,8 @@ void PreferencesDialog::updateUi() { ui->offerAutosaveRecovery->setChecked(_model->getOfferAutosaveRecovery()); ui->moveShipsWhenUndocking->setChecked(_model->getMoveShipsWhenUndocking()); ui->alwaysSaveDisplayNames->setChecked(_model->getAlwaysSaveDisplayNames()); - ui->errorCheckerChecksForPotentialIssues->setChecked(_model->getErrorCheckerChecksForPotentialIssues()); + ui->checkPotentialIssues->setChecked(_model->getCheckPotentialIssues()); + ui->applyAutoCorrections->setChecked(_model->getApplyAutoCorrections()); ui->themeCombo->setCurrentIndex(_model->getDarkMode() ? 1 : 0); const int iconSize = _model->getToolbarIconSize(); @@ -131,8 +132,12 @@ void PreferencesDialog::on_alwaysSaveDisplayNames_toggled(bool checked) { _model->setAlwaysSaveDisplayNames(checked); } -void PreferencesDialog::on_errorCheckerChecksForPotentialIssues_toggled(bool checked) { - _model->setErrorCheckerChecksForPotentialIssues(checked); +void PreferencesDialog::on_checkPotentialIssues_toggled(bool checked) { + _model->setCheckPotentialIssues(checked); +} + +void PreferencesDialog::on_applyAutoCorrections_toggled(bool checked) { + _model->setApplyAutoCorrections(checked); } void PreferencesDialog::on_toolbarIconSizeCombo_currentIndexChanged(int index) { diff --git a/qtfred/src/ui/dialogs/PreferencesDialog.h b/qtfred/src/ui/dialogs/PreferencesDialog.h index 26adc412a26..b51032c3cb6 100644 --- a/qtfred/src/ui/dialogs/PreferencesDialog.h +++ b/qtfred/src/ui/dialogs/PreferencesDialog.h @@ -26,7 +26,8 @@ private slots: void on_offerAutosaveRecovery_toggled(bool checked); void on_moveShipsWhenUndocking_toggled(bool checked); void on_alwaysSaveDisplayNames_toggled(bool checked); - void on_errorCheckerChecksForPotentialIssues_toggled(bool checked); + void on_checkPotentialIssues_toggled(bool checked); + void on_applyAutoCorrections_toggled(bool checked); void on_toolbarIconSizeCombo_currentIndexChanged(int index); void on_themeCombo_currentIndexChanged(int index); void on_showSexpHelpMissionEvents_toggled(bool checked); diff --git a/qtfred/src/ui/util/ErrorChecker.cpp b/qtfred/src/ui/util/ErrorChecker.cpp new file mode 100644 index 00000000000..695efd5f98c --- /dev/null +++ b/qtfred/src/ui/util/ErrorChecker.cpp @@ -0,0 +1,1517 @@ +#include "ui/util/ErrorChecker.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "mission/Editor.h" +#include "mission/object.h" + +namespace fso::fred { + +ErrorChecker::ErrorChecker(EditorViewport* viewport) : _viewport(viewport) {} + +bool ErrorChecker::runFullCheck() { + _collected_errors.clear(); + _anchors_checked.clear(); + _object_names.clear(); + g_err = 0; + + // Surface any auto-corrections the parser had to apply at load time. These would + // otherwise be invisible: parse-time mutates negative delays to 0, out-of-range + // persona indices to -1, etc., so by the time the checker runs the live data is + // already clean and the per-check predicates would never fire. + for (const auto& msg : Mission_parse_warnings) { + _collected_errors.push_back({msg, ErrorSeverity::Warning}); + g_err = 1; + } + Mission_parse_warnings.clear(); + + if (checkObjectList() != 0) return true; + if (checkShips() != 0) return true; + if (checkWings() != 0) return true; + if (checkWaypointPaths() != 0) return true; + if (checkPlayerStarts() != 0) return true; + if (checkReinforcements() != 0) return true; + if (checkPlayerWings() != 0) return true; + if (checkTeamLoadout() != 0) return true; + if (checkMissionEvents() != 0) return true; + if (checkMissionGoals() != 0) return true; + if (checkBriefings() != 0) return true; + if (checkDebriefings() != 0) return true; + if (checkWingOrders() != 0) return true; + if (checkAsteroidTargets() != 0) return true; + if (checkDockingGroupCues() != 0) return true; + + return g_err != 0; +} + +bool ErrorChecker::runCheck(ErrorCheckType type, const ErrorCheckContext& ctx) { + g_err = 0; + _collected_errors.clear(); + _anchors_checked.clear(); + _object_names.clear(); + + switch (type) { + case ErrorCheckType::InitialOrders: checkInitialOrders(ctx.goals, ctx.ship, ctx.wing); break; + case ErrorCheckType::ObjectList: checkObjectList(); break; + case ErrorCheckType::Ships: checkShips(); break; + case ErrorCheckType::Wings: checkWings(); break; // calls populateNames() internally + case ErrorCheckType::WaypointPaths: checkWaypointPaths(); break; // calls populateNames() internally + case ErrorCheckType::PlayerStarts: checkPlayerStarts(); break; + case ErrorCheckType::Reinforcements: checkReinforcements(); break; + case ErrorCheckType::PlayerWings: checkPlayerWings(); break; + case ErrorCheckType::MissionEvents: checkMissionEvents(); break; + case ErrorCheckType::MissionGoals: checkMissionGoals(); break; + case ErrorCheckType::Briefings: checkBriefings(); break; + case ErrorCheckType::Debriefings: checkDebriefings(); break; + case ErrorCheckType::WingOrders: checkWingOrders(); break; + case ErrorCheckType::AsteroidTargets: checkAsteroidTargets(); break; + case ErrorCheckType::DockingGroupCues: checkDockingGroupCues(); break; + case ErrorCheckType::TeamLoadout: checkTeamLoadout(); break; + } + + return g_err != 0; +} + +const SCP_vector& ErrorChecker::getErrors() const { + return _collected_errors; +} + +void ErrorChecker::error(const char* msg, ...) { + char buf[2048]; + va_list args; + + va_start(args, msg); + vsnprintf(buf, sizeof(buf) - 1, msg, args); + va_end(args); + buf[sizeof(buf) - 1] = '\0'; + + g_err = 1; + _collected_errors.push_back({buf, ErrorSeverity::Error}); +} + +int ErrorChecker::internal_error(const char* msg, ...) { + SCP_string buf; + va_list args; + + va_start(args, msg); + vsprintf(buf, msg, args); + va_end(args); + + g_err = 1; + _collected_errors.push_back({buf, ErrorSeverity::InternalError}); + return -1; +} + +void ErrorChecker::warning(const char* msg, ...) { + char buf[2048]; + va_list args; + + va_start(args, msg); + vsnprintf(buf, sizeof(buf) - 1, msg, args); + va_end(args); + buf[sizeof(buf) - 1] = '\0'; + + g_err = 1; + _collected_errors.push_back({buf, ErrorSeverity::Warning}); +} + +void ErrorChecker::potential(const char* msg, ...) { + char buf[2048]; + va_list args; + + va_start(args, msg); + vsnprintf(buf, sizeof(buf) - 1, msg, args); + va_end(args); + buf[sizeof(buf) - 1] = '\0'; + + _collected_errors.push_back({buf, ErrorSeverity::Potential}); +} + +int ErrorChecker::fred_check_sexp(int sexp, int type, const char* location, ...) { + SCP_string location_buf, sexp_buf, error_buf, bad_node_str, issue_msg; + int err = 0, faulty_node; + va_list args; + + va_start(args, location); + vsprintf(location_buf, location, args); + va_end(args); + + if (sexp == -1) + return 0; + + int z = check_sexp_syntax(sexp, type, 1, &faulty_node); + if (z) { + convert_sexp_to_string(sexp_buf, sexp, SEXP_ERROR_CHECK_MODE); + truncate_message_lines(sexp_buf, 30); + + stuff_sexp_text_string(bad_node_str, faulty_node, SEXP_ERROR_CHECK_MODE); + if (!bad_node_str.empty()) + bad_node_str.pop_back(); + + sprintf(error_buf, + "Error in %s: %s\n\n%s\n\n(Bad node appears to be: %s)", + location_buf.c_str(), + sexp_error_message(z), + sexp_buf.c_str(), + bad_node_str.c_str()); + + if (z < 0 && z > -100) + err = 1; + + if (err) + return internal_error("%s", error_buf.c_str()); + + error("%s", error_buf.c_str()); + } + + { + int potential_z = check_sexp_potential_issues(sexp, &faulty_node, issue_msg); + if (potential_z) { + convert_sexp_to_string(sexp_buf, sexp, SEXP_ERROR_CHECK_MODE); + truncate_message_lines(sexp_buf, 30); + + stuff_sexp_text_string(bad_node_str, faulty_node, SEXP_ERROR_CHECK_MODE); + if (!bad_node_str.empty()) + bad_node_str.pop_back(); + + sprintf(error_buf, + "Potential issue detected in %s:\n\n%s\n\n%s\n\n(Suspect node appears to be: %s)", + location_buf.c_str(), + issue_msg.c_str(), + sexp_buf.c_str(), + bad_node_str.c_str()); + + _collected_errors.push_back({error_buf, ErrorSeverity::Potential}); + } + } + + return 0; +} + +void ErrorChecker::populateNames() { + _object_names.clear(); + object* ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + ObjectName entry; + int i = ptr->instance; + if (ptr->type == OBJ_SHIP || ptr->type == OBJ_START) { + if (i >= 0 && i < MAX_SHIPS) + entry.name = Ships[i].ship_name; + } else if (ptr->type == OBJ_WAYPOINT) { + int waypoint_num; + waypoint_list* wp_list = find_waypoint_list_with_instance(i, &waypoint_num); + if (wp_list != nullptr && waypoint_num >= 0 && (uint)waypoint_num < wp_list->get_waypoints().size()) { + char buf[256]; + waypoint_stuff_name(buf, i); + entry.name = buf; + } + } + _object_names.push_back(std::move(entry)); + ptr = GET_NEXT(ptr); + } +} + +int ErrorChecker::checkObjectList() { + + int t = 0; + _object_names.clear(); + object* ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + ObjectName entry; + int i = ptr->instance; + if ((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) { + if (i < 0 || i >= MAX_SHIPS) { + return internal_error("An object has an illegal ship index"); + } + + int z = Ships[i].ship_info_index; + if (!SCP_vector_inbounds(Ship_info, z)) { + return internal_error("A ship has an illegal class"); + } + + if (ptr->type == OBJ_START) { + t++; + if (!(Ship_info[z].flags[Ship::Info_Flags::Player_ship])) { + if (_viewport->Error_checker_apply_auto_corrections) { + ptr->type = OBJ_SHIP; + Player_starts--; + t--; + } + warning("Invalid ship type for a player.%s", + _viewport->Error_checker_apply_auto_corrections + ? " Ship has been reset to non-player ship." + : " Can be auto-corrected to non-player ship."); + } + + int count = 0; + for (int primary_bank_weapon : Ships[i].weapons.primary_bank_weapons) { + if (primary_bank_weapon >= 0) { + count++; + } + } + + if (!count) { + error("Player \"%s\" has no primary weapons. Should have at least 1", Ships[i].ship_name); + } + } + + if (Ships[i].objnum != OBJ_INDEX(ptr)) { + return internal_error("Object/ship references are corrupt"); + } + + entry.name = Ships[i].ship_name; + int w = Ships[i].wingnum; + if (w >= 0) { + if (w >= MAX_WINGS) { + return internal_error("A ship has an illegal wing index"); + } + + int j = Wings[w].wave_count; + if (!j) { + return internal_error("A ship is in a non-existent wing"); + } + + if (j < 0 || j > MAX_SHIPS_PER_WING) { + return internal_error("Invalid number of ships in wing \"%s\"", Wings[w].name); + } + + while (j--) { + if (_viewport->editor->wing_objects[w][j] == OBJ_INDEX(ptr)) { + break; + } + } + + if (j < 0) { + return internal_error("Ship/wing references are corrupt"); + } + + if (strlen(Wings[w].wing_squad_filename) > 0) //-V805 + { + if (The_mission.game_type & MISSION_TYPE_MULTI) { + potential("Wing squad logos are not displayed in multiplayer games."); + } else { + if (ptr->type == OBJ_START) { + potential("A squad logo was assigned to the player's wing. The player's squad logo will be displayed instead of the wing squad logo on ships in this wing."); + } + } + } + } + + if ((Ships[i].flags[Ship::Ship_Flags::Kill_before_mission]) && (Ships[i].hotkey >= 0)) { + potential("Ship flagged as \"destroy before mission start\" has a hotkey assignment"); + } + + if ((Ships[i].flags[Ship::Ship_Flags::Kill_before_mission]) && (ptr->type == OBJ_START)) { + error("Player start flagged as \"destroy before mission start\""); + } + + if ((Ships[i].flags[Ship::Ship_Flags::Kill_before_mission]) && (Ships[i].final_death_time < 0)) { + error("Ship \"%s\" is flagged as \"destroy before mission start\" but has a negative destroy time", + Ships[i].ship_name); + } + } else if (ptr->type == OBJ_WAYPOINT) { + int waypoint_num; + waypoint_list* wp_list = find_waypoint_list_with_instance(i, &waypoint_num); + + if (wp_list == nullptr) { + return internal_error("Object references an illegal waypoint path number"); + } + + if (waypoint_num < 0 || (uint)waypoint_num >= wp_list->get_waypoints().size()) { + return internal_error("Object references an illegal waypoint number in path"); + } + + char buf[256]; + waypoint_stuff_name(buf, i); + entry.name = buf; + } else if (ptr->type == OBJ_POINT) { + // Briefing icons are editor-only objects, not mission objects; nothing to validate here. + } else if (ptr->type == OBJ_JUMP_NODE || ptr->type == OBJ_PROP) { + // nothing needed + } else { + return internal_error("An unknown object type (%d) was detected", ptr->type); + } + + if (!entry.name.empty()) { + for (const auto& existing : _object_names) { + if (!existing.name.empty() && !stricmp(existing.name.c_str(), entry.name.c_str())) { + return internal_error("Duplicate object names (%s)", existing.name.c_str()); + } + } + } + + _object_names.push_back(std::move(entry)); + ptr = GET_NEXT(ptr); + } + + // t and Player_starts are decremented in lockstep when auto-correcting bad player + // starts above, so any mismatch here indicates a data-integrity problem regardless + // of the auto-correct setting. + if (t != Player_starts) { + return internal_error("Total number of player ships is incorrect"); + } + + if (static_cast(_object_names.size()) != Num_objects) { + return internal_error("Num_objects is incorrect"); + } + + return 0; +} + +int ErrorChecker::checkShips() { + int multi = (The_mission.game_type & MISSION_TYPE_MULTI) ? 1 : 0; + + int count = 0; + for (int i = 0; i < MAX_SHIPS; i++) { + if (Ships[i].objnum >= 0) { + count++; + if (!query_valid_object(Ships[i].objnum)) { + return internal_error("Ship uses an unused object"); + } + + int z = Objects[Ships[i].objnum].type; + if ((z != OBJ_SHIP) && (z != OBJ_START)) { + return internal_error("Object should be a ship, but isn't"); + } + + if (fred_check_sexp(Ships[i].arrival_cue, OPR_BOOL, "arrival cue of ship \"%s\"", Ships[i].ship_name)) { + return -1; + } + + if (fred_check_sexp(Ships[i].departure_cue, OPR_BOOL, "departure cue of ship \"%s\"", Ships[i].ship_name)) { + return -1; + } + + if (Ships[i].arrival_location != ArrivalLocation::AT_LOCATION) { + if (!Ships[i].arrival_anchor.isValid()) { + error("Ship \"%s\" requires a valid arrival target", Ships[i].ship_name); + } + + if (Ships[i].arrival_location == ArrivalLocation::FROM_DOCK_BAY) { + SCP_string anchor_message; + check_anchor_for_hangar_bay(anchor_message, _anchors_checked, Ships[i].arrival_anchor, Ships[i].ship_name, true, true); + if (!anchor_message.empty()) + error("%s", anchor_message.c_str()); + } + } + + if (Ships[i].departure_location != DepartureLocation::AT_LOCATION) { + if (!Ships[i].departure_anchor.isValid()) { + error("Ship \"%s\" requires a valid departure target", Ships[i].ship_name); + } + if (Ships[i].departure_location == DepartureLocation::TO_DOCK_BAY) { + SCP_string anchor_message; + check_anchor_for_hangar_bay(anchor_message, _anchors_checked, Ships[i].departure_anchor, Ships[i].ship_name, true, false); + if (!anchor_message.empty()) + error("%s", anchor_message.c_str()); + } + } + + if (Ships[i].arrival_delay < 0) { + error("Ship \"%s\" has a negative arrival delay", Ships[i].ship_name); + } + + if (Ships[i].departure_delay < 0) { + error("Ship \"%s\" has a negative departure delay", Ships[i].ship_name); + } + + if (Ships[i].arrival_location != ArrivalLocation::AT_LOCATION && Ships[i].arrival_distance <= 0) { + error("Arrival distance for ship \"%s\" must be greater than 0", Ships[i].ship_name); + } + + if (Ships[i].flags[Ship::Ship_Flags::Force_shields_on] && + Ship_info[Ships[i].ship_info_index].flags[Ship::Info_Flags::Intrinsic_no_shields]) { + potential("Ship \"%s\" has both \"force shields on\" and \"no shields\" set, which is inconsistent", + Ships[i].ship_name); + } + + if (Ships[i].persona_index >= 0 && Ships[i].persona_index >= (int)Personas.size()) { + potential("Ship \"%s\" has an invalid persona index", Ships[i].ship_name); + } + + for (const auto& alt : Ships[i].s_alt_classes) { + if (alt.ship_class >= 0 && !SCP_vector_inbounds(Ship_info, alt.ship_class)) { + error("Ship \"%s\" has an invalid alternate class index", Ships[i].ship_name); + } + } + + int ai = Ships[i].ai_index; + if (ai < 0 || ai >= MAX_AI_INFO) { + return internal_error("AI index out of range for ship \"%s\"", Ships[i].ship_name); + } + + if (Ai_info[ai].shipnum != i) { + return internal_error("AI/ship references are corrupt"); + } + + if (Ai_info[ai].ai_class < 0 || Ai_info[ai].ai_class >= Num_ai_classes) { + error("Ship \"%s\" has an invalid AI class", Ships[i].ship_name); + } + + if (checkInitialOrders(Ai_info[ai].goals, i, -1) != 0) + return -1; + + SCP_set used_dockpoints; + for (dock_instance* dock_ptr = Objects[Ships[i].objnum].dock_list; dock_ptr != nullptr; + dock_ptr = dock_ptr->next) { + int obj = OBJ_INDEX(dock_ptr->docked_objp); + + if (!query_valid_object(obj)) { + return internal_error("Ship \"%s\" initially docked with non-existent ship", Ships[i].ship_name); + } + + if (Objects[obj].type != OBJ_SHIP && Objects[obj].type != OBJ_START) { + return internal_error("Ship \"%s\" initially docked with non-ship object", Ships[i].ship_name); + } + + int sp = get_ship_from_obj(obj); + if (!ship_docking_valid(i, sp) && !ship_docking_valid(sp, i)) { + return internal_error("Docking illegal between \"%s\" and \"%s\" (initially docked)", + Ships[i].ship_name, + Ships[sp].ship_name); + } + + auto dock_list = Editor::get_docking_list(Ship_info[Ships[i].ship_info_index].model_num); + int point = dock_ptr->dockpoint_used; + if (point < 0 || point >= (int)dock_list.size()) { + return internal_error("Invalid docker point (\"%s\" initially docked with \"%s\")", + Ships[i].ship_name, + Ships[sp].ship_name); + } else if (!used_dockpoints.insert(point).second) { + return internal_error("Ship \"%s\" has the same dockpoint used in multiple initial dock pairings", + Ships[i].ship_name); + } + + dock_list = Editor::get_docking_list(Ship_info[Ships[sp].ship_info_index].model_num); + point = dock_find_dockpoint_used_by_object(dock_ptr->docked_objp, &Objects[Ships[i].objnum]); + if (point < 0 || point >= (int)dock_list.size()) { + return internal_error("Invalid dockee point (\"%s\" initially docked with \"%s\")", + Ships[i].ship_name, + Ships[sp].ship_name); + } + } + + int w = Ships[i].wingnum; + bool is_in_loadout_screen = (z == OBJ_START); + if (!is_in_loadout_screen && w >= 0) { + if (multi && The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) { + for (const char* tvt_wing_name : TVT_wing_names) { + if (!strcmp(Wings[w].name, tvt_wing_name)) { + is_in_loadout_screen = true; + break; + } + } + } else { + for (const char* starting_wing_name : Starting_wing_names) { + if (!strcmp(Wings[w].name, starting_wing_name)) { + is_in_loadout_screen = true; + break; + } + } + } + } + if (is_in_loadout_screen) { + int illegal = 0; + z = Ships[i].ship_info_index; + for (int primary_bank_weapon : Ships[i].weapons.primary_bank_weapons) { + if (primary_bank_weapon >= 0 + && !Ship_info[z].allowed_weapons[primary_bank_weapon]) { + illegal++; + } + } + + for (int secondary_bank_weapon : Ships[i].weapons.secondary_bank_weapons) { + if (secondary_bank_weapon >= 0 + && !Ship_info[z].allowed_weapons[secondary_bank_weapon]) { + illegal++; + } + } + + if (illegal) + error("%d illegal weapon(s) found on ship \"%s\"", illegal, Ships[i].ship_name); + } + + // Orders accepted must be a subset of the class's default orders — older missions + // sometimes saved orders from a different class. Auto-fix by discarding the extras. + { + const SCP_set& default_orders = + ship_get_default_orders_accepted(&Ship_info[Ships[i].ship_info_index]); + SCP_set extras; + std::set_difference(Ships[i].orders_accepted.begin(), Ships[i].orders_accepted.end(), + default_orders.begin(), default_orders.end(), + std::inserter(extras, extras.begin())); + if (!extras.empty()) { + if (_viewport->Error_checker_apply_auto_corrections) { + for (auto order : extras) + Ships[i].orders_accepted.erase(order); + } + warning("Ship \"%s\" accepts orders that are not part of its class's default orders.%s", + Ships[i].ship_name, + _viewport->Error_checker_apply_auto_corrections + ? " The extra orders have been removed." + : ""); + } + } + + } + } + + if (count != ship_get_num_ships()) { + return internal_error("num_ships is incorrect"); + } + + return 0; +} + +int ErrorChecker::checkWings() { + populateNames(); + + int count = 0; + for (int i = 0; i < MAX_WINGS; i++) { + int team = -1; + int j = Wings[i].wave_count; + if (j) { + count++; + if (j < 0 || j > MAX_SHIPS_PER_WING) { + return internal_error("Invalid number of ships in wing \"%s\"", Wings[i].name); + } + + while (j--) { + int obj = _viewport->editor->wing_objects[i][j]; + if (obj < 0 || obj >= MAX_OBJECTS) { + return internal_error("Wing_objects has an illegal object index"); + } + + if (!query_valid_object(obj)) { + return internal_error("Wing_objects references an unused object"); + } + + if (Objects[obj].type != OBJ_SHIP && Objects[obj].type != OBJ_START) { + return internal_error("Wing_objects of \"%s\" references an illegal object type", Wings[i].name); + } + int sp = Objects[obj].instance; + + char buf[256]; + wing_bash_ship_name(buf, Wings[i].name, j + 1); + if (stricmp(buf, Ships[sp].ship_name) != 0) { + return internal_error("Ship \"%s\" in wing should be called \"%s\"", + Ships[sp].ship_name, + buf); + } + + int ship_type = ship_query_general_type(sp); + if (ship_type < 0 || !(Ship_types[ship_type].flags[Ship::Type_Info_Flags::AI_can_form_wing])) { + potential("Ship \"%s\" is an illegal type to be in a wing", Ships[sp].ship_name); + } + + if (Ships[sp].wingnum != i) { + return internal_error("Wing/ship references are corrupt"); + } + + if (sp != Wings[i].ship_index[j]) { + return internal_error("Ship/wing references are corrupt"); + } + + if (team < 0) { + team = Ships[sp].team; + } else if (team != Ships[sp].team && team < 999) { + potential("Ship teams mixed within same wing (\"%s\")", Wings[i].name); + } + } + + if ((Wings[i].special_ship < 0) || (Wings[i].special_ship >= Wings[i].wave_count)) { + return internal_error("Special ship out of range for \"%s\"", Wings[i].name); + } + + if (Wings[i].num_waves < 0) { + return internal_error("Number of waves for \"%s\" is negative", Wings[i].name); + } + + if (Wings[i].threshold < 0) { + return internal_error("Threshold for \"%s\" is invalid", Wings[i].name); + } + + if (Wings[i].threshold + Wings[i].wave_count > MAX_SHIPS_PER_WING) { + if (_viewport->Error_checker_apply_auto_corrections) { + Wings[i].threshold = MAX_SHIPS_PER_WING - Wings[i].wave_count; + warning("Threshold for wing \"%s\" is higher than allowed. Reset to %d", + Wings[i].name, + Wings[i].threshold); + } else { + warning("Threshold for wing \"%s\" is higher than allowed.", Wings[i].name); + } + } + + for (const auto& entry : _object_names) { + if (!entry.name.empty() && !stricmp(entry.name.c_str(), Wings[i].name)) { + return internal_error("Wing name is also used by an object (%s)", entry.name.c_str()); + } + } + + if (fred_check_sexp(Wings[i].arrival_cue, OPR_BOOL, "arrival cue of wing \"%s\"", Wings[i].name)) { + return -1; + } + + if (fred_check_sexp(Wings[i].departure_cue, OPR_BOOL, "departure cue of wing \"%s\"", Wings[i].name)) { + return -1; + } + + if (Wings[i].arrival_location != ArrivalLocation::AT_LOCATION) { + if (!Wings[i].arrival_anchor.isValid()) { + error("Wing \"%s\" requires a valid arrival target", Wings[i].name); + } + if (Wings[i].arrival_location == ArrivalLocation::FROM_DOCK_BAY) { + SCP_string anchor_message; + check_anchor_for_hangar_bay(anchor_message, _anchors_checked, Wings[i].arrival_anchor, Wings[i].name, false, true); + if (!anchor_message.empty()) + error("%s", anchor_message.c_str()); + } + } + + if (Wings[i].departure_location != DepartureLocation::AT_LOCATION) { + if (!Wings[i].departure_anchor.isValid()) { + error("Wing \"%s\" requires a valid departure target", Wings[i].name); + } + if (Wings[i].departure_location == DepartureLocation::TO_DOCK_BAY) { + SCP_string anchor_message; + check_anchor_for_hangar_bay(anchor_message, _anchors_checked, Wings[i].departure_anchor, Wings[i].name, false, false); + if (!anchor_message.empty()) + error("%s", anchor_message.c_str()); + } + } + + if (Wings[i].arrival_delay < 0) { + error("Wing \"%s\" has a negative arrival delay", Wings[i].name); + } + + if (Wings[i].departure_delay < 0) { + error("Wing \"%s\" has a negative departure delay", Wings[i].name); + } + + if (Wings[i].arrival_location != ArrivalLocation::AT_LOCATION && Wings[i].arrival_distance <= 0) { + error("Arrival distance for wing \"%s\" must be greater than 0", Wings[i].name); + } + + if (Wings[i].formation >= 0 && Wings[i].formation >= (int)Wing_formations.size()) { + potential("Wing \"%s\" has an invalid formation", Wings[i].name); + } + + { + bool has_player = false; + for (int k = 0; k < Wings[i].wave_count; k++) { + if (Objects[Ships[Wings[i].ship_index[k]].objnum].type == OBJ_START) { + has_player = true; + break; + } + } + if (has_player && Wings[i].arrival_delay > 0) { + potential("Wing \"%s\" contains a player start but has a non-zero arrival delay; the delay will be ignored", + Wings[i].name); + } + } + + if (checkInitialOrders(Wings[i].ai_goals, -1, i) != 0) + return -1; + } + } + + if (count != Num_wings) { + return internal_error("Num_wings is incorrect"); + } + + return 0; +} + +int ErrorChecker::checkWaypointPaths() { + populateNames(); + + for (const auto& ii : Waypoint_lists) { + for (const auto& entry : _object_names) { + if (!entry.name.empty() && !stricmp(entry.name.c_str(), ii.get_name())) { + return internal_error("Waypoint path name is also used by an object (%s)", entry.name.c_str()); + } + } + + for (const auto& jj : ii.get_waypoints()) { + char buf[256]; + waypoint_stuff_name(buf, jj); + bool found = false; + for (const auto& entry : _object_names) { + if (!entry.name.empty() && !stricmp(entry.name.c_str(), buf)) { + found = true; + break; + } + } + + if (!found) { + return internal_error("Waypoint \"%s\" not linked to an object", buf); + } + } + } + + return 0; +} + +int ErrorChecker::checkPlayerStarts() { + int multi = (The_mission.game_type & MISSION_TYPE_MULTI) ? 1 : 0; + + if (Player_starts > MAX_PLAYERS) { + return internal_error("Number of player starts exceeds max limit"); + } + + if (!multi && (Player_starts > 1)) { + error("Multiple player starts exist, but this is a single player mission"); + } + + return 0; +} + +int ErrorChecker::checkReinforcements() { + + if (Num_reinforcements > MAX_REINFORCEMENTS) { + return internal_error("Number of reinforcements exceeds max limit"); + } + + for (int i = 0; i < Num_reinforcements; i++) { + if (Reinforcements[i].arrival_delay < 0) { + error("Reinforcement \"%s\" has a negative arrival delay", Reinforcements[i].name); + } + + int z = 0; + int ship_wingnum = -1; + for (const auto& ship : Ships) { + if ((ship.objnum >= 0) && !stricmp(ship.ship_name, Reinforcements[i].name)) { + z = 1; + ship_wingnum = ship.wingnum; + break; + } + } + + for (const auto& wing : Wings) { + if (wing.wave_count && !stricmp(wing.name, Reinforcements[i].name)) { + z = 1; + break; + } + } + + if (!z) { + return internal_error("Reinforcement name not found in ships or wings"); + } + + if (ship_wingnum >= 0) { + potential("Reinforcement \"%s\" is a ship that belongs to a wing; the reinforcement flag will be ignored", + Reinforcements[i].name); + } + } + + return 0; +} + +int ErrorChecker::checkPlayerWings() { + if (Player_start_shipnum < 0 || Player_start_shipnum >= MAX_SHIPS || Ships[Player_start_shipnum].objnum < 0) { + return internal_error("Mission has no valid player start ship"); + } + + const int multi = (The_mission.game_type & MISSION_TYPE_MULTI) ? 1 : 0; + + // The first starting wing and the first TVT wing must share a name — missionparse/post_process_ships_wings + // treats this as a fatal Error() in-game, so the mission will not run otherwise. + if (strcmp(Starting_wing_names[0], TVT_wing_names[0]) != 0) { + error("The first starting wing (\"%s\") and the first team-versus-team wing (\"%s\") must have the same name", + Starting_wing_names[0], TVT_wing_names[0]); + } + + auto checkMixedSpecies = [this](int w) { + int species = -1; + bool mixed = false; + for (int i = 0; i < Wings[w].wave_count; i++) { + int s = Wings[w].ship_index[i]; + if (species < 0) + species = Ship_info[Ships[s].ship_info_index].species; + else if (Ship_info[Ships[s].ship_info_index].species != species) + mixed = true; + } + if (mixed) + error("%s wing must all be of the same species", Wings[w].name); + }; + + // In non-TVT multiplayer, squadron wings must be contiguous from the front — if wing[i] exists + // but wing[i-1] doesn't, wings can disappear at runtime (missionparse.cpp line 5605). + if (multi && !(The_mission.game_type & MISSION_TYPE_MULTI_TEAMS)) { + bool found[MAX_STARTING_WINGS] = {}; + for (int i = 0; i < MAX_STARTING_WINGS; i++) { + for (const auto& wing : Wings) { + if (wing.wave_count && !strcmp(wing.name, Squadron_wing_names[i])) { + found[i] = true; + break; + } + } + } + for (int i = 1; i < MAX_STARTING_WINGS; i++) { + if (found[i] && !found[i - 1]) { + potential("Squadron wings are not in the correct order: wing \"%s\" exists but \"%s\" does not; this may cause wings to disappear in multiplayer", + Squadron_wing_names[i], Squadron_wing_names[i - 1]); + } + } + } + + if (multi && The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) { + for (const char* tvt_wing_name : TVT_wing_names) { + if (ship_tvt_wing_lookup(tvt_wing_name) == -1) { + error("%s wing is required for multiplayer team vs. team missions", tvt_wing_name); + } + } + } + + if (multi) { + if (The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) { + for (int i = 0; i < MAX_TVT_WINGS; i++) { + if (TVT_wings[i] >= 0 && Wings[TVT_wings[i]].num_waves > 1) { + if (_viewport->Error_checker_apply_auto_corrections) + Wings[TVT_wings[i]].num_waves = 1; + warning("%s wing must contain only 1 wave.%s", TVT_wing_names[i], + _viewport->Error_checker_apply_auto_corrections + ? " Reset to 1 wave." + : " Can be auto-corrected to 1 wave."); + } + } + } else { + for (int i = 0; i < MAX_STARTING_WINGS; i++) { + if (Starting_wings[i] >= 0 && Wings[Starting_wings[i]].num_waves > 1) { + if (_viewport->Error_checker_apply_auto_corrections) + Wings[Starting_wings[i]].num_waves = 1; + warning("%s wing must contain only 1 wave.%s", Starting_wing_names[i], + _viewport->Error_checker_apply_auto_corrections + ? " Reset to 1 wave." + : " Can be auto-corrected to 1 wave."); + } + } + } + } + + if (multi && The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) { + for (int i = 0; i < MAX_TVT_WINGS; i++) { + if (TVT_wings[i] >= 0 && Wings[TVT_wings[i]].wave_count > 4) { + error("%s wing has too many ships. Should only have 4 max.", TVT_wing_names[i]); + } + } + } else { + for (int i = 0; i < MAX_STARTING_WINGS; i++) { + if (Starting_wings[i] >= 0 && Wings[Starting_wings[i]].wave_count > 4) { + error("%s wing has too many ships. Should only have 4 max.", Starting_wing_names[i]); + } + } + } + + if (multi && The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) { + for (int i = 0; i < MAX_TVT_WINGS; i++) { + if (TVT_wings[i] >= 0 && Wings[TVT_wings[i]].arrival_delay > 0) { + potential("%s wing shouldn't have a non-zero arrival delay", TVT_wing_names[i]); + } + } + } + + if (multi) { + if (The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) { + for (int tvt_wing : TVT_wings) { + if (tvt_wing >= 0) { + checkMixedSpecies(tvt_wing); + } + } + } else { + for (int starting_wing : Starting_wings) { + if (starting_wing >= 0) { + checkMixedSpecies(starting_wing); + } + } + } + } + + int starting_wing_count[MAX_STARTING_WINGS]; + int tvt_wing_count[MAX_TVT_WINGS]; + SCP_string starting_wing_list; + SCP_string tvt_wing_list; + + for (int i = 0; i < MAX_STARTING_WINGS; i++) { + starting_wing_count[i] = 0; + + if (i < MAX_STARTING_WINGS - 1) { + starting_wing_list += Starting_wing_names[i]; + if (MAX_STARTING_WINGS > 2) + starting_wing_list += ","; + starting_wing_list += " "; + } else { + starting_wing_list += "or "; + starting_wing_list += Starting_wing_names[i]; + } + } + for (int i = 0; i < MAX_TVT_WINGS; i++) { + tvt_wing_count[i] = 0; + + if (i < MAX_TVT_WINGS - 1) { + tvt_wing_list += TVT_wing_names[i]; + if (MAX_TVT_WINGS > 2) + tvt_wing_list += ","; + tvt_wing_list += " "; + } else { + tvt_wing_list += "or "; + tvt_wing_list += TVT_wing_names[i]; + } + } + + object* ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + int ship_instance = ptr->instance; + int err = 0; + + if (ptr->type == OBJ_START) { + int z = Ships[ship_instance].wingnum; + if (z < 0) { + err = 1; + } else { + int in_starting_wing = 0; + int in_tvt_wing = 0; + + for (int i = 0; i < MAX_STARTING_WINGS; i++) { + if (Starting_wings[i] == z) { + in_starting_wing = 1; + starting_wing_count[i]++; + } + } + for (int i = 0; i < MAX_TVT_WINGS; i++) { + if (TVT_wings[i] == z) { + in_tvt_wing = 1; + tvt_wing_count[i]++; + } + } + + if (The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) { + if (!in_tvt_wing) + err = 1; + } else { + if (!in_starting_wing) + err = 1; + } + } + + if (err) { + if (The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) { + error("Player %s should be part of %s wing", Ships[ship_instance].ship_name, tvt_wing_list.c_str()); + } else { + error("Player %s should be part of %s wing", Ships[ship_instance].ship_name, starting_wing_list.c_str()); + } + } + } + + ptr = GET_NEXT(ptr); + } + + if (The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) { + for (int i = 0; i < MAX_TVT_WINGS; i++) { + if (!tvt_wing_count[i]) { + error("%s wing does not contain any players (which it should)", TVT_wing_names[i]); + } + } + } + + return 0; +} + +int ErrorChecker::checkMissionEvents() { + for (const auto& event : Mission_events) { + if (event.repeat_count == 0) { + error("Mission event \"%s\" has a repeat count of 0; must be at least 1 (or negative for unlimited)", + event.name.c_str()); + } + + if (event.trigger_count == 0) { + error("Mission event \"%s\" has a trigger count of 0; must be at least 1 (or negative for unlimited)", + event.name.c_str()); + } + + if (fred_check_sexp(event.formula, OPR_NULL, "mission event \"%s\"", event.name.c_str())) { + return -1; + } + } + return 0; +} + +int ErrorChecker::checkMissionGoals() { + for (const auto& goal : Mission_goals) { + if (fred_check_sexp(goal.formula, OPR_BOOL, "mission goal \"%s\"", goal.name.c_str())) { + return -1; + } + } + return 0; +} + +int ErrorChecker::checkBriefings() { + + for (int bs = 0; bs < Num_teams; bs++) { + for (int s = 0; s < Briefings[bs].num_stages; s++) { + brief_stage* sp = &Briefings[bs].stages[s]; + + if (fred_check_sexp(sp->formula, OPR_BOOL, "briefing stage %d (team %d)", s + 1, bs + 1)) { + return -1; + } + + int t = sp->num_icons; + for (int i = 0; i < t - 1; i++) { + for (int j = i + 1; j < t; j++) { + if ((sp->icons[i].id > 0) && (sp->icons[i].id == sp->icons[j].id)) { + potential("Duplicate icon IDs %d in briefing stage %d", sp->icons[i].id, s + 1); + } + } + } + } + } + return 0; +} + +int ErrorChecker::checkDebriefings() { + for (int j = 0; j < Num_teams; j++) { + for (int i = 0; i < Debriefings[j].num_stages; i++) { + if (fred_check_sexp(Debriefings[j].stages[i].formula, OPR_BOOL, "debriefing stage %d", i + 1)) { + return -1; + } + } + } + return 0; +} + +int ErrorChecker::checkWingOrders() { + + for (const auto& wing : Wings) { + if (!wing.wave_count) { + continue; + } + + int starting_wing = (ship_starting_wing_lookup(wing.name) != -1); + + if (starting_wing && (wing.flags[Ship::Wing_Flags::Reinforcement])) { + error("Starting Wing %s marked as reinforcement. This wing\nshould either be renamed, or unmarked as reinforcement.", + wing.name); + } + + std::set default_orders; + int default_orders_idx = -1; + for (int j = 0; j < wing.wave_count; j++) { + if (Objects[Ships[wing.ship_index[j]].objnum].type == OBJ_START) { + continue; + } + + const std::set& orders = Ships[wing.ship_index[j]].orders_accepted; + + if (default_orders_idx < 0) { + default_orders_idx = j; + default_orders = orders; + } else if (default_orders != orders) { + potential("%s and %s will accept different orders. All ships in a wing must accept the same Player Orders.", + Ships[wing.ship_index[j]].ship_name, + Ships[wing.ship_index[default_orders_idx]].ship_name); + } + } + } + + return 0; +} + +int ErrorChecker::checkAsteroidTargets() { + for (const auto& name : Asteroid_field.target_names) { + if (ship_name_lookup(name.c_str(), 1) < 0) { + error("Asteroid target '%s' is not a valid ship", name.c_str()); + } + } + return 0; +} + +int ErrorChecker::checkDockingGroupCues() { + SCP_set visited; // ship indices already accounted for + + for (int i = 0; i < MAX_SHIPS; i++) { + if (Ships[i].objnum < 0) continue; + if (!query_valid_object(Ships[i].objnum)) continue; + if (!object_is_docked(&Objects[Ships[i].objnum])) continue; + if (visited.count(i)) continue; + + // BFS to collect all ships in this docking group + SCP_vector group; + SCP_vector queue = {i}; + while (!queue.empty()) { + int si = queue.back(); + queue.pop_back(); + if (visited.count(si)) continue; + visited.insert(si); + group.push_back(si); + + for (dock_instance* dp = Objects[Ships[si].objnum].dock_list; dp != nullptr; dp = dp->next) { + int docked_obj = OBJ_INDEX(dp->docked_objp); + if (Objects[docked_obj].type == OBJ_SHIP || Objects[docked_obj].type == OBJ_START) { + int docked_ship = Objects[docked_obj].instance; + if (!visited.count(docked_ship)) + queue.push_back(docked_ship); + } + } + } + + // Count non-false arrival cues; winged ships share the wing cue + SCP_set checked_wings; + int non_false_count = 0; + for (int si : group) { + int w = Ships[si].wingnum; + if (w >= 0 && w < MAX_WINGS) { + if (!checked_wings.insert(w).second) continue; + if (Wings[w].arrival_cue != Locked_sexp_false) + non_false_count++; + } else { + if (Ships[si].arrival_cue != Locked_sexp_false) + non_false_count++; + } + } + + if (non_false_count == 0) { + error("Docking group containing \"%s\" has no ship with a non-false arrival cue; the group will not appear in-mission", + Ships[i].ship_name); + } else if (non_false_count > 1) { + error("Docking group containing \"%s\" has more than one non-false arrival cue; only one is allowed", + Ships[i].ship_name); + } + } + + return 0; +} + +int ErrorChecker::checkTeamLoadout() { + for (int i = 0; i < Num_teams; i++) { + // Respect the per-team "Bypass Loadout Validation" flag (set in the loadout editor). + if (Team_data[i].do_not_validate) + continue; + + // Build a fresh usage list for this team's starting wings. + int usage[MAX_WEAPON_TYPES]; + _viewport->editor->generate_team_weaponry_usage_list(i, usage); + + // Zero out weapons that are accounted for in the loadout pool, so that + // only weapons missing from the pool remain non-zero. + for (int j = 0; j < Team_data[i].num_weapon_choices; j++) { + int wi = Team_data[i].weaponry_pool[j]; + if (wi >= 0 && wi < MAX_WEAPON_TYPES) + usage[wi] = 0; + } + + // Any non-zero entry is a weapon used in wings but absent from the loadout. + for (int j = 0; j < MAX_WEAPON_TYPES; j++) { + if (usage[j] <= 0) + continue; + + if (_viewport->Error_checker_apply_auto_corrections && Team_data[i].num_weapon_choices < MAX_WEAPON_TYPES) { + // Add the missing weapon to the pool so the mission is structurally valid. + int slot = Team_data[i].num_weapon_choices; + Team_data[i].weaponry_pool[slot] = j; + Team_data[i].weaponry_count[slot] = usage[j]; + strcpy_s(Team_data[i].weaponry_amount_variable[slot], ""); + strcpy_s(Team_data[i].weaponry_pool_variable[slot], ""); + Team_data[i].num_weapon_choices++; + warning("Weapon \"%s\" is used in wings of team %d but was not in the team loadout pool — added automatically.", + Weapon_info[j].name, i + 1); + } else { + warning("Weapon \"%s\" is used in wings of team %d but is not in the team loadout pool — can be auto-corrected by adding it.", + Weapon_info[j].name, i + 1); + } + } + } + return 0; +} + +int ErrorChecker::checkInitialOrders(ai_goal* goals, int ship, int wing) { + if (_viewport->editor == nullptr) { + return internal_error("checkInitialOrders requires a valid editor"); + } + + auto get_order_name = [](ai_goal_mode order) -> const char* { + if (order == AI_GOAL_NONE) + return "None"; + const ai_goal_list* list = Editor::getAi_goal_list(); + int size = Editor::getAigoal_list_size(); + for (int i = 0; i < size; i++) + if (list[i].def == order) + return list[i].name; + return "???"; + }; + + int team, team2; + char* source; + const char* entity = (ship >= 0) ? "ship" : "wing"; + + if (ship >= 0) { + source = Ships[ship].ship_name; + team = Ships[ship].team; + for (int i = 0; i < MAX_AI_GOALS; i++) { + if (!ai_query_goal_valid(ship, goals[i].ai_mode)) + potential("Order \"%s\" is not allowed for ship \"%s\"", get_order_name(goals[i].ai_mode), source); + } + } else { + Assert(wing >= 0); + Assert(Wings[wing].wave_count > 0); + source = Wings[wing].name; + team = Ships[Objects[_viewport->editor->wing_objects[wing][0]].instance].team; + for (int j = 0; j < Wings[wing].wave_count; j++) { + for (int i = 0; i < MAX_AI_GOALS; i++) { + if (!ai_query_goal_valid(Wings[wing].ship_index[j], goals[i].ai_mode)) + potential("Order \"%s\" is not allowed for ship \"%s\"", get_order_name(goals[i].ai_mode), + Ships[Wings[wing].ship_index[j]].ship_name); + } + } + } + + int flag, found; + for (int i = 0; i < MAX_AI_GOALS; i++) { + switch (goals[i].ai_mode) { + case AI_GOAL_NONE: + case AI_GOAL_CHASE_ANY: + case AI_GOAL_CHASE_SHIP_CLASS: + case AI_GOAL_UNDOCK: + case AI_GOAL_KEEP_SAFE_DISTANCE: + case AI_GOAL_PLAY_DEAD: + case AI_GOAL_PLAY_DEAD_PERSISTENT: + case AI_GOAL_WARP: + flag = 0; + break; + + case AI_GOAL_WAYPOINTS: + case AI_GOAL_WAYPOINTS_ONCE: + flag = 1; + break; + + case AI_GOAL_DOCK: + if (ship < 0) { + error("Initial orders error for wing \"%s\"\n\nWings cannot dock", source); + continue; + } + FALLTHROUGH; + + case AI_GOAL_DESTROY_SUBSYSTEM: + case AI_GOAL_CHASE: + case AI_GOAL_GUARD: + case AI_GOAL_DISARM_SHIP: + case AI_GOAL_DISARM_SHIP_TACTICAL: + case AI_GOAL_DISABLE_SHIP: + case AI_GOAL_DISABLE_SHIP_TACTICAL: + case AI_GOAL_EVADE_SHIP: + case AI_GOAL_STAY_NEAR_SHIP: + case AI_GOAL_FORM_ON_WING: + case AI_GOAL_IGNORE: + case AI_GOAL_IGNORE_NEW: + flag = 2; + break; + + case AI_GOAL_CHASE_WING: + case AI_GOAL_GUARD_WING: + flag = 3; + break; + + case AI_GOAL_STAY_STILL: + flag = 4; + break; + + default: + return internal_error("Initial orders error for %s \"%s\"\n\nInvalid goal type", entity, source); + } + + found = 0; + if (flag > 0) { + if (*goals[i].target_name == '<') { + error("Initial orders error for %s \"%s\"\n\nInvalid target", entity, source); + continue; + } + + if (!stricmp(goals[i].target_name, source)) { + error("Initial orders error for %s \"%s\"\n\nTarget of %s's goal is itself", entity, source, entity); + continue; + } + } + + int inst = team2 = -1; + if (flag == 1) { + if (find_matching_waypoint_list(goals[i].target_name) == nullptr) + return internal_error("Initial orders error for %s \"%s\"\n\nInvalid target waypoint path name", entity, source); + + } else if (flag == 2) { + object* ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (ptr->type == OBJ_SHIP || ptr->type == OBJ_START) { + inst = ptr->instance; + if (!stricmp(goals[i].target_name, Ships[inst].ship_name)) { + found = 1; + break; + } + } + ptr = GET_NEXT(ptr); + } + + if (!found) + return internal_error("Initial orders error for %s \"%s\"\n\nInvalid target ship name", entity, source); + + if (wing >= 0) { + if (Ships[inst].wingnum == wing && Objects[Ships[inst].objnum].type != OBJ_START) { + error("Initial orders error for wing \"%s\"\n\nTarget ship of wing's goal is within said wing", source); + continue; + } + } + + team2 = Ships[inst].team; + + } else if (flag == 3) { + int j; + for (j = 0; j < MAX_WINGS; j++) + if (Wings[j].wave_count && !stricmp(Wings[j].name, goals[i].target_name)) + break; + + if (j >= MAX_WINGS) + return internal_error("Initial orders error for %s \"%s\"\n\nInvalid target wing name", entity, source); + + if (ship >= 0) { + if (Ships[ship].wingnum == j) { + error("Initial orders error for ship \"%s\"\n\nTarget wing of ship's goal is same wing said ship is part of", source); + continue; + } + } + + team2 = Ships[Objects[_viewport->editor->wing_objects[j][0]].instance].team; + + } else if (flag == 4) { + object* ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (ptr->type == OBJ_SHIP || ptr->type == OBJ_START) { + inst = ptr->instance; + if (!stricmp(goals[i].target_name, Ships[inst].ship_name)) { + found = 2; + break; + } + } else if (ptr->type == OBJ_WAYPOINT) { + if (!stricmp(goals[i].target_name, object_name(OBJ_INDEX(ptr)))) { + found = 1; + break; + } + } + ptr = GET_NEXT(ptr); + } + + if (!found) + return internal_error("Initial orders error for %s \"%s\"\n\nInvalid target ship or waypoint name", entity, source); + + if (found == 2) { + if (wing >= 0) { + if (Ships[inst].wingnum == wing && Objects[Ships[inst].objnum].type != OBJ_START) { + error("Initial orders error for wing \"%s\"\n\nTarget ship of wing's goal is within said wing", source); + continue; + } + } + team2 = Ships[inst].team; + } + } + + switch (goals[i].ai_mode) { + case AI_GOAL_DESTROY_SUBSYSTEM: + Assert(flag == 2 && inst >= 0); // NOLINT(readability-simplify-boolean-expr) + if (ship_find_subsys(&Ships[inst], goals[i].docker.name) < 0) { + potential("Initial orders error for ship \"%s\"\n\nUnknown subsystem type", source); + continue; + } + break; + + case AI_GOAL_DOCK: { + int dock1 = -1, dock2 = -1, model1, model2; + + Assert(flag == 2 && inst >= 0); // NOLINT(readability-simplify-boolean-expr) + if (!ship_docking_valid(ship, inst)) { + error("Initial orders error for ship \"%s\"\n\nDocking illegal between given ship types", source); + continue; + } + + model1 = Ship_info[Ships[ship].ship_info_index].model_num; + auto model1Docks = Editor::get_docking_list(model1); + for (int j = 0; j < (int)model1Docks.size(); ++j) { + if (!stricmp(goals[i].docker.name, model1Docks[j].c_str())) { + dock1 = j; + break; + } + } + + model2 = Ship_info[Ships[inst].ship_info_index].model_num; + auto model2Docks = Editor::get_docking_list(model2); + for (int j = 0; j < (int)model2Docks.size(); ++j) { + if (!stricmp(goals[i].dockee.name, model2Docks[j].c_str())) { + dock2 = j; + break; + } + } + + if (dock1 < 0) { + error("Initial orders error for ship \"%s\"\n\nInvalid docker point", source); + continue; + } + + if (dock2 < 0) { + error("Initial orders error for ship \"%s\"\n\nInvalid dockee point", source); + continue; + } + + if (!(model_get_dock_index_type(model1, dock1) & model_get_dock_index_type(model2, dock2))) { + error("Initial orders error for ship \"%s\"\n\nDock points are incompatible", source); + } + + break; + } + + default: + break; + } + + switch (goals[i].ai_mode) { + case AI_GOAL_GUARD: + case AI_GOAL_GUARD_WING: + if (team != team2) + potential("Initial orders error for %s \"%s\"\n\n%s assigned to guard a different team", + entity, source, ship >= 0 ? "Ship" : "Wing"); + break; + + case AI_GOAL_CHASE: + case AI_GOAL_CHASE_WING: + case AI_GOAL_DESTROY_SUBSYSTEM: + case AI_GOAL_DISARM_SHIP: + case AI_GOAL_DISARM_SHIP_TACTICAL: + case AI_GOAL_DISABLE_SHIP: + case AI_GOAL_DISABLE_SHIP_TACTICAL: + if (team == team2) + potential("Initial orders error for %s \"%s\"\n\n%s assigned to attack same team", + entity, source, ship >= 0 ? "Ship" : "Wing"); + break; + + default: + break; + } + } + + return 0; +} + +} // namespace fso::fred diff --git a/qtfred/src/ui/util/ErrorChecker.h b/qtfred/src/ui/util/ErrorChecker.h new file mode 100644 index 00000000000..b38ed822c6c --- /dev/null +++ b/qtfred/src/ui/util/ErrorChecker.h @@ -0,0 +1,147 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace fso::fred { + +class EditorViewport; + +enum class ErrorSeverity { InternalError, Error, Warning, Potential }; + +// Display metadata for one severity level. +// Colors are stored as plain RGB components so this header stays Qt-free; +// callers that need a QColor construct one from (r, g, b) at the use site. +struct SeverityInfo { + uint8_t r, g, b; + const char* label; + const char* tooltip; +}; + +// One entry per ErrorSeverity value, index-matched to the enum. +inline constexpr std::array severity_info = {{ + { 0xCC, 0x33, 0x33, "Critical Error", + "A serious structural problem FRED cannot automatically correct. " + "The mission may fail to load or behave unpredictably in-game." }, + { 0xE0, 0x78, 0x30, "Error", + "A mission design problem that must be manually fixed. The mission may not play correctly otherwise." }, + { 0xD4, 0xA0, 0x00, "Warning", + "A problem that can be (or has been) auto-corrected. Review the change before saving." }, + { 0x40, 0x80, 0xCC, "Potential Issue", + "A situation that may be intentional but is worth reviewing." }, +}}; + +inline const SeverityInfo& infoFor(ErrorSeverity sev) { + switch (sev) { + case ErrorSeverity::InternalError: return severity_info[0]; + case ErrorSeverity::Error: return severity_info[1]; + case ErrorSeverity::Warning: return severity_info[2]; + case ErrorSeverity::Potential: return severity_info[3]; + } + UNREACHABLE("Unhandled ErrorSeverity value"); + return severity_info[1]; +} + +struct ErrorEntry { + SCP_string message; + ErrorSeverity severity; +}; + +enum class ErrorCheckType { + InitialOrders, // standalone check (used by ShipGoalsDialogModel) + ObjectList, // object integrity + name collection + Ships, // ship SEXPs, anchors, AI goals, docking, loadout weapons + Wings, // wing structure, SEXPs, anchors, AI goals, thresholds + WaypointPaths, // waypoint path name conflicts with objects + PlayerStarts, // player start count validity + Reinforcements, // reinforcement name references + PlayerWings, // player wing membership and wave constraints + MissionEvents, // mission event SEXP validation + MissionGoals, // mission goal SEXP validation + Briefings, // briefing icon ID duplicates + Debriefings, // debriefing SEXP validation + WingOrders, // wing reinforcement flags and accepted orders consistency + AsteroidTargets, // asteroid field target ship name validity + DockingGroupCues, // initially-docked groups must have exactly one non-false arrival cue + TeamLoadout, // weapons used in starting wings but absent from the team loadout pool +}; + +struct ErrorCheckContext { + ai_goal* goals = nullptr; + int ship = -1; + int wing = -1; +}; + +class ErrorChecker { +public: + explicit ErrorChecker(EditorViewport* viewport); + + // Run all checks in collect mode; returns true if errors were found + bool runFullCheck(); + + // Run a specific check in collect mode; returns true if errors were found. + // Errors are available via getErrors() after the call. + bool runCheck(ErrorCheckType type, const ErrorCheckContext& ctx = {}); + + const SCP_vector& getErrors() const; + +private: + EditorViewport* _viewport; + + // Per-object identity entries built up during ObjectList / Wings / Waypoints checks. + // Empty name means an object type with no identifier (briefing icons, jump nodes, props). + struct ObjectName { + SCP_string name; + }; + SCP_vector _object_names; + // Accumulates whether any error() or warning() has fired during a check run. + // Those helpers are void (no return value to propagate), so g_err is the only + // way runFullCheck/runCheck can report non-critical issues that don't cause an + // early abort. internal_error() sets it too, but also returns -1 for callers + // that need to halt immediately. + int g_err = 0; + SCP_vector _collected_errors; + SCP_set _anchors_checked; + + // error() records a user-fixable problem and continues; return type is void so + // callers cannot short-circuit on it (use internal_error for abort-worthy issues). + void error(SCP_FORMAT_STRING const char* msg, ...) SCP_FORMAT_STRING_ARGS(2, 3); + // internal_error() records a data-integrity problem and returns -1 so callers + // can propagate an early abort when continuing would be unsafe. + int internal_error(SCP_FORMAT_STRING const char* msg, ...) SCP_FORMAT_STRING_ARGS(2, 3); + void warning(SCP_FORMAT_STRING const char* msg, ...) SCP_FORMAT_STRING_ARGS(2, 3); + void potential(SCP_FORMAT_STRING const char* msg, ...) SCP_FORMAT_STRING_ARGS(2, 3); + int fred_check_sexp(int sexp, int type, const char* location, ...); + + // Populates _object_names from the object list. Safe to call multiple times. + // Called internally by checkWings() and checkWaypointPaths() — no external call needed. + void populateNames(); + + // Individual check methods (called by runFullCheck in order). + // Return 0 on success or when only user-fixable errors were found. + // Return -1 on internal errors severe enough to warrant aborting further checks. + int checkObjectList(); + int checkShips(); + int checkWings(); + int checkWaypointPaths(); + int checkPlayerStarts(); + int checkReinforcements(); + int checkPlayerWings(); + int checkMissionEvents(); + int checkMissionGoals(); + int checkBriefings(); + int checkDebriefings(); + int checkWingOrders(); + int checkAsteroidTargets(); + int checkDockingGroupCues(); + int checkTeamLoadout(); + + // Helper methods + int checkInitialOrders(ai_goal* goals, int ship, int wing); +}; + +} // namespace fso::fred diff --git a/qtfred/ui/ErrorCheckerDialog.ui b/qtfred/ui/ErrorCheckerDialog.ui new file mode 100644 index 00000000000..30e86d746e0 --- /dev/null +++ b/qtfred/ui/ErrorCheckerDialog.ui @@ -0,0 +1,97 @@ + + + fso::fred::dialogs::ErrorCheckerDialog + + + + 0 + 0 + 600 + 480 + + + + Error Checker + + + + + + + + Rerun + + + + + + + Check Potential Issues + + + + + + + Apply Auto-Corrections + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + true + + + + + + + + + No check has been run yet. + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + + + + + + + + diff --git a/qtfred/ui/PreferencesDialog.ui b/qtfred/ui/PreferencesDialog.ui index c11c0989a21..e73b35564aa 100644 --- a/qtfred/ui/PreferencesDialog.ui +++ b/qtfred/ui/PreferencesDialog.ui @@ -107,10 +107,26 @@ + + + + + + + Error Checker + + + + + + Check potential issues + + + - + - Error checker checks for potential issues + Apply auto-corrections diff --git a/test/src/test_stubs.cpp b/test/src/test_stubs.cpp index 2970ae133db..de2b3971723 100644 --- a/test/src/test_stubs.cpp +++ b/test/src/test_stubs.cpp @@ -33,6 +33,7 @@ UI_TIMESTAMP Multi_ping_timestamp; int Sun_drew = 0; int Fred_running = 0; +int Qtfred_running = 0; float Sun_spot = 0.0f; char Fred_alt_names[MAX_SHIPS][NAME_LENGTH+1]; char Fred_callsigns[MAX_SHIPS][NAME_LENGTH+1]; From 975ce277a7a70dac3d612448629176a879e39125 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 12 May 2026 06:51:00 -0500 Subject: [PATCH 35/65] QtFRED large ship collision group mission flag (#7425) * large ship collision group mission flag * update help doc --- code/mission/mission_flags.h | 1 + code/mission/missionparse.cpp | 27 ++++++++++++++- code/mission/missionparse.h | 1 + code/missioneditor/missionsave.cpp | 4 +++ .../doc/dialogs/MissionSpecsDialog.html | 10 ++++++ .../dialogs/MissionSpecDialogModel.cpp | 18 ++++++++++ .../mission/dialogs/MissionSpecDialogModel.h | 3 ++ qtfred/src/ui/dialogs/MissionSpecDialog.cpp | 15 +++++++++ qtfred/src/ui/dialogs/MissionSpecDialog.h | 2 ++ qtfred/ui/MissionSpecDialog.ui | 33 +++++++++++++++++++ 10 files changed, 113 insertions(+), 1 deletion(-) diff --git a/code/mission/mission_flags.h b/code/mission/mission_flags.h index 178b1eddbfd..8189433525c 100644 --- a/code/mission/mission_flags.h +++ b/code/mission/mission_flags.h @@ -36,6 +36,7 @@ namespace Mission { Neb2_fog_color_override, // Whether to use explicit fog colors instead of checking the palette - Goober5000 Fullneb_background_bitmaps, // Show background bitmaps despite fullneb Preload_subspace, // Preload the subspace tunnel for both the sexp and specs checkbox (for scripts) - MjnMixael + Large_ships_no_collide_by_default, // Automatically puts all large ships in a shared collision group NUM_VALUES }; diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index d018a087f5c..d20ac0cb5c3 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -203,6 +203,8 @@ SCP_unordered_set Fred_migrated_immobile_ships; int Num_path_restrictions; path_restriction_t Path_restrictions[MAX_PATH_RESTRICTIONS]; +constexpr int DEFAULT_LARGE_SHIP_NO_COLLIDE_COLLISION_GROUP = 0; + extern int debrief_find_persona_index(); static bool mission_has_layer_name(const mission* pm, const SCP_string& layerName) { @@ -418,7 +420,8 @@ flag_def_list_new Parse_mission_flags[] = { {"Toggle Starting in Chase View", Mission::Mission_Flags::Toggle_start_chase_view, true, false}, {"Nebula Fog Color Override", Mission::Mission_Flags::Neb2_fog_color_override, true, true}, {"Full Nebula Background Bitmaps", Mission::Mission_Flags::Fullneb_background_bitmaps, true, true}, - {"Preload Subspace Tunnel", Mission::Mission_Flags::Preload_subspace, true, false} + {"Preload Subspace Tunnel", Mission::Mission_Flags::Preload_subspace, true, false}, + {"Large Ships Do Not Collide By Default", Mission::Mission_Flags::Large_ships_no_collide_by_default, true, false} }; parse_object_flag_description Parse_mission_flag_descriptions[] = { @@ -452,6 +455,7 @@ parse_object_flag_description Parse_mission_flag_descrip {Mission::Mission_Flags::Neb2_fog_color_override, "Whether to use explicit fog colors instead of checking the palette"}, {Mission::Mission_Flags::Fullneb_background_bitmaps, "Show background bitmaps despite full nebula"}, {Mission::Mission_Flags::Preload_subspace, "Preload the subspace tunnel for both the sexp and specs checkbox"}, + {Mission::Mission_Flags::Large_ships_no_collide_by_default, "Automatically places all large ships in the configured collision group, preventing large ships from colliding with each other"}, }; const size_t Num_parse_mission_flags = sizeof(Parse_mission_flags) / sizeof(flag_def_list_new); @@ -865,6 +869,18 @@ void parse_mission_info(mission *pm, bool basic = false) stuff_int(&pm->contrail_threshold); } + if (optional_string("+Large Ship Collision Group:")) { + stuff_int(&pm->large_ship_no_collide_collision_group); + + if (pm->large_ship_no_collide_collision_group < 0 || pm->large_ship_no_collide_collision_group > 31) { + WarningEx(LOCATION, + "Invalid large ship collision group id %d specified. Valid IDs range from 0 to 31. Using group %d instead.\n", + pm->large_ship_no_collide_collision_group, + DEFAULT_LARGE_SHIP_NO_COLLIDE_COLLISION_GROUP); + pm->large_ship_no_collide_collision_group = DEFAULT_LARGE_SHIP_NO_COLLIDE_COLLISION_GROUP; + } + } + if (optional_string("+Volumetric Nebula:")) { pm->volumetrics.emplace().parse_volumetric_nebula(); } @@ -2259,6 +2275,11 @@ int parse_create_object_sub(p_object *p_objp, bool standalone_ship) // Goober5000 - set the collision group if one was provided Objects[objnum].collision_group_id = p_objp->collision_group_id; + // Mission-level performance helper: large ships may be grouped so they skip mutual collision checks. + if (The_mission.flags[Mission::Mission_Flags::Large_ships_no_collide_by_default] && sip->is_big_or_huge()) { + Objects[objnum].collision_group_id |= (1 << The_mission.large_ship_no_collide_collision_group); + } + // Goober5000 - set some fields that the mission log might need (if logged via parse_bring_in_docked_wing just below) shipp->display_name = p_objp->display_name; shipp->alt_type_index = p_objp->alt_type_index; @@ -7210,6 +7231,7 @@ void mission::Reset() envmap_name[ 0 ] = '\0'; contrail_threshold = CONTRAIL_THRESHOLD_DEFAULT; + large_ship_no_collide_collision_group = DEFAULT_LARGE_SHIP_NO_COLLIDE_COLLISION_GROUP; ambient_light_level = DEFAULT_AMBIENT_LIGHT_LEVEL; sound_environment.id = -1; @@ -9591,6 +9613,9 @@ bool check_for_24_3_data() bool check_for_25_1_data() { + if (The_mission.flags[Mission::Mission_Flags::Large_ships_no_collide_by_default]) + return true; + if (count_items_with_value(Props) > 0) return true; diff --git a/code/mission/missionparse.h b/code/mission/missionparse.h index baf540616e6..8cca23f74e9 100644 --- a/code/mission/missionparse.h +++ b/code/mission/missionparse.h @@ -207,6 +207,7 @@ typedef struct mission { char envmap_name[MAX_FILENAME_LEN]; int skybox_flags; int contrail_threshold; + int large_ship_no_collide_collision_group; int ambient_light_level; std::optional volumetrics; sound_env sound_environment; diff --git a/code/missioneditor/missionsave.cpp b/code/missioneditor/missionsave.cpp index 505163dee6c..69f1c70b353 100644 --- a/code/missioneditor/missionsave.cpp +++ b/code/missioneditor/missionsave.cpp @@ -2568,6 +2568,10 @@ int Fred_mission_save::save_mission_info() if (The_mission.contrail_threshold != CONTRAIL_THRESHOLD_DEFAULT) { fout("\n$Contrail Speed Threshold: %d\n", The_mission.contrail_threshold); } + + if (The_mission.flags[Mission::Mission_Flags::Large_ships_no_collide_by_default]) { + fout("\n+Large Ship Collision Group: %d\n", The_mission.large_ship_no_collide_collision_group); + } } { diff --git a/qtfred/help-src/doc/dialogs/MissionSpecsDialog.html b/qtfred/help-src/doc/dialogs/MissionSpecsDialog.html index b05acd65eb1..399096ae618 100644 --- a/qtfred/help-src/doc/dialogs/MissionSpecsDialog.html +++ b/qtfred/help-src/doc/dialogs/MissionSpecsDialog.html @@ -140,5 +140,15 @@

Flags

box to find a specific flag by name, or use Select All / Select None to bulk-toggle. Hover over any flag label for a tooltip description.

+

Some flags reveal an additional control when enabled:

+ + + + + +
FlagControlDescription
Large Ships Do Not Collide By DefaultLarge Ship Collision GroupCollision group ID (0–31) that large and huge ships are automatically + placed into when the mission loads. Ships sharing the same group skip + collision checks against each other, reducing physics overhead in + missions with many capital ships. Default is 0.
diff --git a/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp b/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp index fa8101a3de4..ffd6453dd9e 100644 --- a/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp +++ b/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp @@ -70,6 +70,7 @@ void MissionSpecDialogModel::initializeData() { _m_contrail_threshold = The_mission.contrail_threshold; _m_contrail_threshold_flag = (_m_contrail_threshold != CONTRAIL_THRESHOLD_DEFAULT); + _m_large_ship_no_collide_collision_group = The_mission.large_ship_no_collide_collision_group; _m_custom_data = The_mission.custom_data; _m_custom_strings = The_mission.custom_strings; @@ -125,6 +126,7 @@ bool MissionSpecDialogModel::apply() { The_mission.support_ships.max_support_ships = (_m_disallow_support) ? 0 : -1; The_mission.support_ships.max_hull_repair_val = _m_max_hull_repair_val; The_mission.support_ships.max_subsys_repair_val = _m_max_subsys_repair_val; + The_mission.large_ship_no_collide_collision_group = _m_large_ship_no_collide_collision_group; // Copy mission flags The_mission.flags = _m_flags; @@ -418,6 +420,22 @@ void MissionSpecDialogModel::setMissionFlagDirect(Mission::Mission_Flags flag, b } } +void MissionSpecDialogModel::setLargeShipNoCollideCollisionGroup(int group) +{ + if (group < 0) { + group = 0; + } else if (group > 31) { + group = 31; + } + + modify(_m_large_ship_no_collide_collision_group, group); +} + +int MissionSpecDialogModel::getLargeShipNoCollideCollisionGroup() const +{ + return _m_large_ship_no_collide_collision_group; +} + bool MissionSpecDialogModel::getMissionFlag(Mission::Mission_Flags flag) const { return _m_flags[flag]; } diff --git a/qtfred/src/mission/dialogs/MissionSpecDialogModel.h b/qtfred/src/mission/dialogs/MissionSpecDialogModel.h index 0b623a3625a..636ac027b98 100644 --- a/qtfred/src/mission/dialogs/MissionSpecDialogModel.h +++ b/qtfred/src/mission/dialogs/MissionSpecDialogModel.h @@ -42,6 +42,7 @@ class MissionSpecDialogModel : public AbstractDialogModel { float _m_max_subsys_repair_val; bool _m_contrail_threshold_flag; int _m_contrail_threshold; + int _m_large_ship_no_collide_collision_group; SCP_map _m_custom_data; SCP_vector _m_custom_strings; sound_env _m_sound_env; @@ -119,6 +120,8 @@ class MissionSpecDialogModel : public AbstractDialogModel { void setMissionFlag(const SCP_string& flag_name, bool enabled); void setMissionFlagDirect(Mission::Mission_Flags flag, bool enabled); + void setLargeShipNoCollideCollisionGroup(int group); + int getLargeShipNoCollideCollisionGroup() const; bool getMissionFlag(Mission::Mission_Flags flag) const; const SCP_vector>& getMissionFlagsList(); static SCP_vector> getMissionFlagDescriptions(); diff --git a/qtfred/src/ui/dialogs/MissionSpecDialog.cpp b/qtfred/src/ui/dialogs/MissionSpecDialog.cpp index 339b9d60e19..a5ee42e72b8 100644 --- a/qtfred/src/ui/dialogs/MissionSpecDialog.cpp +++ b/qtfred/src/ui/dialogs/MissionSpecDialog.cpp @@ -134,6 +134,7 @@ void MissionSpecDialog::initFlagList() // per flag immediate apply to the model connect(ui->flagList, &fso::fred::FlagListWidget::flagToggled, this, [this](const QString& name, bool checked) { _model->setMissionFlag(name.toUtf8().constData(), checked); + updateLargeShipCollisionGroup(); }); } @@ -149,6 +150,15 @@ void MissionSpecDialog::updateFlags() } ui->flagList->setFlags(toWidget); + updateLargeShipCollisionGroup(); +} + +void MissionSpecDialog::updateLargeShipCollisionGroup() +{ + const auto enabled = _model->getMissionFlag(Mission::Mission_Flags::Large_ships_no_collide_by_default); + ui->largeShipCollisionGroupLabel->setVisible(enabled); + ui->largeShipCollisionGroup->setVisible(enabled); + ui->largeShipCollisionGroup->setValue(_model->getLargeShipNoCollideCollisionGroup()); } void MissionSpecDialog::updateMissionType() { @@ -433,6 +443,11 @@ void MissionSpecDialog::on_musicPackCombo_currentIndexChanged(int index) { _model->setSubEventMusic(subMusic); } +void MissionSpecDialog::on_largeShipCollisionGroup_valueChanged(int value) +{ + _model->setLargeShipNoCollideCollisionGroup(value); +} + void MissionSpecDialog::on_aiProfileCombo_currentIndexChanged(int index) { auto aipIndex = ui->aiProfileCombo->itemData(index).value(); diff --git a/qtfred/src/ui/dialogs/MissionSpecDialog.h b/qtfred/src/ui/dialogs/MissionSpecDialog.h index 64884aa02be..ed279f9acf0 100644 --- a/qtfred/src/ui/dialogs/MissionSpecDialog.h +++ b/qtfred/src/ui/dialogs/MissionSpecDialog.h @@ -64,6 +64,7 @@ private slots: // Right column // flags are dynamically generated and connected + void on_largeShipCollisionGroup_valueChanged(int value); void on_aiProfileCombo_currentIndexChanged(int index); // General @@ -84,6 +85,7 @@ private slots: void initFlagList(); void updateFlags(); + void updateLargeShipCollisionGroup(); void updateMissionType(); void updateCmdMessage(); diff --git a/qtfred/ui/MissionSpecDialog.ui b/qtfred/ui/MissionSpecDialog.ui index b3517fa6f48..ff6a6c9caa8 100644 --- a/qtfred/ui/MissionSpecDialog.ui +++ b/qtfred/ui/MissionSpecDialog.ui @@ -822,6 +822,39 @@
+ + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Large Ship Collision Group + + + + + + + 0 + + + 31 + + + + + From 1cbfb34a431b23f7fec562aabe776b488a54c9c5 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Tue, 12 May 2026 13:52:28 +0200 Subject: [PATCH 36/65] add findWorld / findObject for objects (#7443) --- code/scripting/api/objs/object.cpp | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/code/scripting/api/objs/object.cpp b/code/scripting/api/objs/object.cpp index 825e2045dc2..801692c179f 100644 --- a/code/scripting/api/objs/object.cpp +++ b/code/scripting/api/objs/object.cpp @@ -766,5 +766,37 @@ ADE_FUNC(getIFFColor, l_Object, "boolean ReturnType", } } +ADE_FUNC(findWorldPoint, l_Object, "vector", "Calculates the world coordinates of a point in the object's frame of reference", "vector", "Point, or empty vector if handle is not valid") +{ + object_h *objh; + vec3d pnt, outpnt; + if (!ade_get_args(L, "oo", l_Object.GetPtr(&objh), l_Vector.Get(&pnt))) + return ade_set_error(L, "o", l_Vector.Set(vmd_zero_vector)); + + if (!objh->isValid()) + return ade_set_error(L, "o", l_Vector.Set(vmd_zero_vector)); + + auto objp = objh->objp(); + vm_vec_unrotate(&outpnt, &pnt, &objp->orient); + outpnt += objp->pos; + return ade_set_args(L, "o", l_Vector.Set(outpnt)); +} + +ADE_FUNC(findObjectPoint, l_Object, "vector", "Calculates the coordinates in an object's frame of reference, of a point in world coordinates", "vector", "Point, or empty vector if handle is not valid") +{ + object_h *objh; + vec3d pnt, outpnt; + if (!ade_get_args(L, "oo", l_Object.GetPtr(&objh), l_Vector.Get(&pnt))) + return ade_set_error(L, "o", l_Vector.Set(vmd_zero_vector)); + + if (!objh->isValid()) + return ade_set_error(L, "o", l_Vector.Set(vmd_zero_vector)); + + auto objp = objh->objp(); + pnt -= objp->pos; + vm_vec_rotate(&outpnt, &pnt, &objp->orient); + return ade_set_args(L, "o", l_Vector.Set(outpnt)); +} + } // namespace api } // namespace scripting From ef73e2572df19f0fec10699f36383d2bcf727c4c Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Fri, 8 May 2026 01:14:09 -0400 Subject: [PATCH 37/65] unlimited mission titles Some code refactoring to allow mission titles to be strings of any length. Titles that are too long to actually display (based on string width) will be visibly truncated using `font::force_fit_string`, but the underlying title will not be affected. Multiplayer is still subject to the 31-character length limit, so titles sent over the network will still have characters truncated. Also removes some redundant calls to `gr_get_string_size`, since `font::force_fit_string` returns the fitted string width. --- code/controlconfig/controlsconfig.cpp | 3 +-- code/libs/discord/discord.cpp | 2 +- code/menuui/readyroom.cpp | 4 +-- code/mission/missionparse.cpp | 26 +++++++++---------- code/mission/missionparse.h | 2 +- code/missioneditor/missionsave.cpp | 2 +- code/missionui/missionbrief.cpp | 24 ++++++++++------- code/missionui/missiondebrief.cpp | 8 +++--- code/network/multi_dogfight.cpp | 26 +++++++++---------- code/network/multi_pxo.cpp | 11 ++++---- code/network/multiui.cpp | 3 ++- code/playerman/managepilot.cpp | 3 +-- fred2/dumpstats.cpp | 2 +- fred2/freddoc.cpp | 6 ++--- fred2/fredview.cpp | 4 --- fred2/management.cpp | 2 +- fred2/missionnotesdlg.cpp | 4 +-- fred2/voiceactingmanager.cpp | 2 +- qtfred/src/mission/Editor.cpp | 6 ++--- .../dialogs/MissionSpecDialogModel.cpp | 2 +- .../dialogs/VoiceActingManagerModel.cpp | 2 +- .../src/ui/dialogs/CampaignEditorDialog.cpp | 8 +++--- qtfred/src/ui/dialogs/MissionSpecDialog.cpp | 2 -- 23 files changed, 76 insertions(+), 78 deletions(-) diff --git a/code/controlconfig/controlsconfig.cpp b/code/controlconfig/controlsconfig.cpp index 5a01df130d7..1dbdc7b3efb 100644 --- a/code/controlconfig/controlsconfig.cpp +++ b/code/controlconfig/controlsconfig.cpp @@ -2724,8 +2724,7 @@ void control_config_do_frame(float frametime) strcpy_s(buf, Control_config[i].text.c_str()); } - font::force_fit_string(buf, 255, Conflict_wnd_coords[gr_screen.res][CONTROL_W_COORD]); - gr_get_string_size(&w, NULL, buf); + w = font::force_fit_string(buf, 255, Conflict_wnd_coords[gr_screen.res][CONTROL_W_COORD]); gr_printf_menu(x - w / 2, y, "%s", buf); } else if (*bound_string) { diff --git a/code/libs/discord/discord.cpp b/code/libs/discord/discord.cpp index fbbfa2554df..0d6eb5c9e6e 100644 --- a/code/libs/discord/discord.cpp +++ b/code/libs/discord/discord.cpp @@ -68,7 +68,7 @@ SCP_string get_details() } if (has_campaign && in_mission) { - sprintf(res, "%s: %s", get_current_campaign_name().c_str(), The_mission.name); + sprintf(res, "%s: %s", get_current_campaign_name().c_str(), The_mission.name.c_str()); } else if (has_campaign) { sprintf(res, "Campaign %s", get_current_campaign_name().c_str()); } else if (in_mission) { diff --git a/code/menuui/readyroom.cpp b/code/menuui/readyroom.cpp index bfecaee0f19..073759a5940 100644 --- a/code/menuui/readyroom.cpp +++ b/code/menuui/readyroom.cpp @@ -493,7 +493,7 @@ int build_standalone_mission_list_do_frame(bool API_Access) Lcl_unexpected_tstring_check = nullptr; if (condition) { - Standalone_mission_names[Num_standalone_missions_with_info] = vm_strdup(The_mission.name); + Standalone_mission_names[Num_standalone_missions_with_info] = vm_strdup(The_mission.name.c_str()); Standalone_mission_flags[Num_standalone_missions_with_info] = The_mission.game_type; int y = Num_lines * (font_height + 2); @@ -565,7 +565,7 @@ int build_campaign_mission_list_do_frame(bool API_Access) auto filename = Campaign.missions[Num_campaign_missions_with_info].name; // add to list - Campaign_mission_names[Num_campaign_missions_with_info] = vm_strdup(The_mission.name); + Campaign_mission_names[Num_campaign_missions_with_info] = vm_strdup(The_mission.name.c_str()); Campaign_mission_flags[Num_campaign_missions_with_info] = The_mission.game_type; int y = valid_missions_with_info * (font_height + 2); diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index d20ac0cb5c3..e89ec552e4d 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -787,7 +787,7 @@ void parse_mission_info(mission *pm, bool basic = false) throw parse::VersionException("Mission requires version " + gameversion::format_version(pm->required_fso_version), pm->required_fso_version); required_string("$Name:"); - stuff_string(pm->name, F_NAME, NAME_LENGTH); + stuff_string(pm->name, F_NAME); required_string("$Author:"); stuff_string(pm->author, F_NAME); @@ -1050,7 +1050,7 @@ void parse_mission_info(mission *pm, bool basic = false) if (index >= 0) The_mission.ai_profile = &Ai_profiles[index]; else - WarningEx(LOCATION, "Mission: %s\nUnknown AI profile %s!", pm->name, temp ); + WarningEx(LOCATION, "Mission: %s\nUnknown AI profile %s!", pm->name.c_str(), temp ); } if (optional_string("$Lighting Profile:")) @@ -1232,7 +1232,7 @@ void parse_player_info2(mission *pm) stuff_string(str, F_NAME, NAME_LENGTH); ptr->default_ship = ship_info_lookup(str); if (-1 == ptr->default_ship) { - WarningEx(LOCATION, "Mission: %s\nUnknown default ship %s! Defaulting to %s.", pm->name, str, Ship_info[ptr->ship_list[0]].name ); + WarningEx(LOCATION, "Mission: %s\nUnknown default ship %s! Defaulting to %s.", pm->name.c_str(), str, Ship_info[ptr->ship_list[0]].name ); ptr->default_ship = ptr->ship_list[0]; // default to 1st in list } // see if the player's default ship is an allowable ship (campaign only). If not, then what @@ -1245,7 +1245,7 @@ void parse_player_info2(mission *pm) break; } } - Assertion( i < ship_info_size(), "Mission: %s: Could not find a valid default ship.\n", pm->name ); + Assertion( i < ship_info_size(), "Mission: %s: Could not find a valid default ship.\n", pm->name.c_str() ); } } } @@ -3391,7 +3391,7 @@ int parse_object(mission *pm, int /*flag*/, p_object *p_objp) // try and find the alternate name p_objp->alt_type_index = mission_parse_lookup_alt(name); if(p_objp->alt_type_index < 0) - WarningEx(LOCATION, "Mission %s\nError looking up alternate ship type name %s!\n", pm->name, name); + WarningEx(LOCATION, "Mission %s\nError looking up alternate ship type name %s!\n", pm->name.c_str(), name); else mprintf(("Using alternate ship type name: %s\n", name)); } @@ -3405,7 +3405,7 @@ int parse_object(mission *pm, int /*flag*/, p_object *p_objp) // try and find the callsign p_objp->callsign_index = mission_parse_lookup_callsign(name); if(p_objp->callsign_index < 0) - WarningEx(LOCATION, "Mission %s\nError looking up callsign %s!\n", pm->name, name); + WarningEx(LOCATION, "Mission %s\nError looking up callsign %s!\n", pm->name.c_str(), name); else mprintf(("Using callsign: %s\n", name)); } @@ -6310,7 +6310,7 @@ void parse_bitmaps(mission *pm) } if (z == NUM_NEBULAS) - WarningEx(LOCATION, "Mission %s\nUnknown nebula %s!", pm->name, str); + WarningEx(LOCATION, "Mission %s\nUnknown nebula %s!", pm->name.c_str(), str); if (optional_string("+Color:")) { stuff_string(str, F_NAME, MAX_FILENAME_LEN); @@ -6323,7 +6323,7 @@ void parse_bitmaps(mission *pm) } if (z == NUM_NEBULA_COLORS) - WarningEx(LOCATION, "Mission %s\nUnknown nebula color %s!", pm->name, str); + WarningEx(LOCATION, "Mission %s\nUnknown nebula color %s!", pm->name.c_str(), str); if (optional_string("+Pitch:")){ stuff_int(&Nebula_pitch); @@ -6421,7 +6421,7 @@ void parse_asteroid_fields(mission *pm) if (subtype >= 0) { Asteroid_field.field_debris_type.push_back(subtype); } else { - WarningEx(LOCATION, "Mission %s\n Invalid asteroid debris %s!", pm->name, ast_name.c_str()); + WarningEx(LOCATION, "Mission %s\n Invalid asteroid debris %s!", pm->name.c_str(), ast_name.c_str()); } } @@ -6473,7 +6473,7 @@ void parse_asteroid_fields(mission *pm) if (valid){ Asteroid_field.field_asteroid_type.push_back(std::move(ast_name)); } else { - WarningEx(LOCATION, "Mission %s\n Invalid asteroid %s!", pm->name, ast_name.c_str()); + WarningEx(LOCATION, "Mission %s\n Invalid asteroid %s!", pm->name.c_str(), ast_name.c_str()); } } } @@ -6853,7 +6853,7 @@ bool parse_mission(mission *pm, int flags) popup(PF_TITLE_BIG | PF_TITLE_RED | PF_USE_AFFIRMATIVE_ICON | PF_NO_NETWORKING, 1, POPUP_OK, text); } - log_printf(LOGFILE_EVENT_LOG, "Mission %s loaded.\n", pm->name); + log_printf(LOGFILE_EVENT_LOG, "Mission %s loaded.\n", pm->name.c_str()); // success return true; @@ -7203,8 +7203,8 @@ int get_mission_info(const char *filename, mission *mission_p, bool basic, bool void mission::Reset() { - name[ 0 ] = '\0'; - author = ""; + name.clear(); + author.clear(); required_fso_version = LEGACY_MISSION_VERSION; created[ 0 ] = '\0'; modified[ 0 ] = '\0'; diff --git a/code/mission/missionparse.h b/code/mission/missionparse.h index 8cca23f74e9..967b903e5b1 100644 --- a/code/mission/missionparse.h +++ b/code/mission/missionparse.h @@ -185,7 +185,7 @@ struct parse_object_flag_description { }; typedef struct mission { - char name[NAME_LENGTH]; + SCP_string name; SCP_string author; gameversion::version required_fso_version; char created[DATE_TIME_LENGTH]; diff --git a/code/missioneditor/missionsave.cpp b/code/missioneditor/missionsave.cpp index 69f1c70b353..d1cbcb99225 100644 --- a/code/missioneditor/missionsave.cpp +++ b/code/missioneditor/missionsave.cpp @@ -2496,7 +2496,7 @@ int Fred_mission_save::save_mission_info() // XSTR required_string_fred("$Name:"); parse_comments(); - fout_ext(" ", "%s", The_mission.name); + fout_ext(" ", "%s", The_mission.name.c_str()); required_string_fred("$Author:"); parse_comments(); diff --git a/code/missionui/missionbrief.cpp b/code/missionui/missionbrief.cpp index ded3cf14f1a..b1112c5c87d 100644 --- a/code/missionui/missionbrief.cpp +++ b/code/missionui/missionbrief.cpp @@ -290,9 +290,10 @@ UI_XSTR Brief_select_text[GR_NUM_RESOLUTIONS][BRIEF_SELECT_NUM_TEXT] = { }; // coordinates for briefing title -- the x value is for the RIGHT side of the text -static int Title_coords[GR_NUM_RESOLUTIONS][2] = { - {575, 117}, // GR_640 - {918, 194} // GR_1024 +// third coord is max width of area for it to fit into (it is force fit there) +static int Title_coords[GR_NUM_RESOLUTIONS][3] = { + {575, 117, 370}, // GR_640 + {918, 194, 588} // GR_1024 }; // coordinates for briefing title in multiplayer briefings -- the x value is for the LEFT side of the text @@ -1158,7 +1159,7 @@ void brief_render(float frametime, bool api_access) gr_printf_menu((gr_screen.clip_width_unscaled - w) / 2,200,XSTR( "No Briefing exists for mission: %s", 430), Game_current_mission_filename); #ifndef NDEBUG - gr_get_string_size(&w, &h, The_mission.name); + gr_get_string_size(&w, &h, The_mission.name.c_str()); gr_set_color_fast(&Color_normal); SCP_string debugText; sprintf(debugText, NOX("[filename: %s, last mod: %s]"), Mission_filename, The_mission.modified); @@ -1211,17 +1212,20 @@ void brief_render(float frametime, bool api_access) gr_printf_menu(Brief_bmap_coords[gr_screen.res][0], Brief_bmap_coords[gr_screen.res][1]-title_y_offset, NOX("[name: %s, mod: %s]"), Mission_filename, The_mission.modified); #endif + // force-fit mission title if necessary + const size_t max_buf_len = 255; + char buf[max_buf_len + 1]; + strncpy(buf, The_mission.name.c_str(), max_buf_len); + buf[max_buf_len] = '\0'; + const int max_coords_width = (Game_mode & GM_MULTIPLAYER) ? Title_coords_multi[gr_screen.res][2] : Title_coords[gr_screen.res][2]; + w = font::force_fit_string(buf, max_buf_len, max_coords_width); + // output mission title gr_set_color_fast(&Color_bright_white); - if (Game_mode & GM_MULTIPLAYER) { - char buf[256]; - strncpy(buf, The_mission.name, 255); - font::force_fit_string(buf, 255, Title_coords_multi[gr_screen.res][2]); gr_string(Title_coords_multi[gr_screen.res][0], Title_coords_multi[gr_screen.res][1], buf, GR_RESIZE_MENU); } else { - gr_get_string_size(&w, nullptr, The_mission.name); - gr_string(Title_coords[gr_screen.res][0] - w, Title_coords[gr_screen.res][1], The_mission.name, GR_RESIZE_MENU); + gr_string(Title_coords[gr_screen.res][0] - w, Title_coords[gr_screen.res][1], buf, GR_RESIZE_MENU); } // maybe do objectives diff --git a/code/missionui/missiondebrief.cpp b/code/missionui/missiondebrief.cpp index 8a4870ddd5a..acd4a06937b 100644 --- a/code/missionui/missiondebrief.cpp +++ b/code/missionui/missiondebrief.cpp @@ -2277,7 +2277,8 @@ void debrief_do_frame(float frametime) { int k=0, new_k=0; const char *please_wait_str = XSTR("Please Wait", 1242); - char buf[256]; + const size_t max_buf_len = 255; + char buf[max_buf_len+1]; Assert(Debrief_inited); @@ -2458,8 +2459,9 @@ void debrief_do_frame(float frametime) // draw the title of the mission gr_set_color_fast(&Color_bright_white); - strcpy_s(buf, The_mission.name); - font::force_fit_string(buf, 255, Debrief_title_coords[gr_screen.res][2]); + strncpy(buf, The_mission.name.c_str(), max_buf_len); + buf[max_buf_len] = '\0'; + font::force_fit_string(buf, max_buf_len, Debrief_title_coords[gr_screen.res][2]); gr_string(Debrief_title_coords[gr_screen.res][0], Debrief_title_coords[gr_screen.res][1], buf, GR_RESIZE_MENU); #if !defined(NDEBUG) diff --git a/code/network/multi_dogfight.cpp b/code/network/multi_dogfight.cpp index 5dd40dd404d..8f17b4b76b0 100644 --- a/code/network/multi_dogfight.cpp +++ b/code/network/multi_dogfight.cpp @@ -245,7 +245,8 @@ void multi_df_debrief_init(bool API_Access) void multi_df_debrief_do(bool API_Access) { int k, new_k; - char buf[256]; + const size_t max_buf_len = 255; + char buf[max_buf_len+1]; k = chatbox_process(); if (!API_Access) { @@ -285,8 +286,9 @@ void multi_df_debrief_do(bool API_Access) chatbox_render(); // draw the mission title - strcpy_s(buf, The_mission.name); - font::force_fit_string(buf, 255, Kill_matrix_title_coords[gr_screen.res][2]); + strncpy(buf, The_mission.name.c_str(), max_buf_len); + buf[max_buf_len] = '\0'; + font::force_fit_string(buf, max_buf_len, Kill_matrix_title_coords[gr_screen.res][2]); gr_set_color_fast(&Color_bright_white); gr_string(Kill_matrix_title_coords[gr_screen.res][0], Kill_matrix_title_coords[gr_screen.res][1], @@ -392,7 +394,7 @@ void multi_df_setup_kill_matrix() // blit the kill matrix void multi_df_blit_kill_matrix() { - int idx, s_idx, str_len; + int idx, s_idx; int cx, cy; char squashed_string[CALLSIGN_LEN+1] = ""; int dy = gr_get_font_height() + 1; @@ -415,15 +417,14 @@ void multi_df_blit_kill_matrix() for(idx=0; idx= 0); if(Multi_df_score[idx].np_index >= 0){ gr_set_color_fast(Color_netplayer[Multi_df_score[idx].np_index]); } - gr_string(cx + (int)((max_item_width - (float)str_len)/2.0f), cy, squashed_string, GR_RESIZE_MENU); + gr_string(cx + (int)((max_item_width - (float)w)/2.0f), cy, squashed_string, GR_RESIZE_MENU); // next spot cx += (int)max_item_width; @@ -446,7 +447,6 @@ void multi_df_blit_kill_matrix() cx = Multi_df_display_coords[gr_screen.res][0]; strcpy_s(squashed_string, Multi_df_score[idx].callsign); font::force_fit_string(squashed_string, CALLSIGN_LEN, (int)max_text_width); - gr_get_string_size(&str_len, NULL, squashed_string); Assert(Multi_df_score[idx].np_index >= 0); if(Multi_df_score[idx].np_index >= 0){ gr_set_color_fast(Color_netplayer[Multi_df_score[idx].np_index]); @@ -469,9 +469,8 @@ void multi_df_blit_kill_matrix() } // draw the string - font::force_fit_string(squashed_string, CALLSIGN_LEN, (int)max_text_width); - gr_get_string_size(&str_len, NULL, squashed_string); - gr_string(cx + (int)((max_item_width - (float)str_len)/2.0f), cy, squashed_string, GR_RESIZE_MENU); + int w = font::force_fit_string(squashed_string, CALLSIGN_LEN, (int)max_text_width); + gr_string(cx + (int)((max_item_width - (float)w)/2.0f), cy, squashed_string, GR_RESIZE_MENU); // next spot cx += (int)max_item_width; @@ -480,8 +479,9 @@ void multi_df_blit_kill_matrix() // draw the row total gr_set_color_fast(Color_netplayer[Multi_df_score[idx].np_index]); sprintf(squashed_string, "(%d)", row_total); - gr_get_string_size(&str_len, NULL, squashed_string); - gr_string(Multi_df_display_coords[gr_screen.res][0] + Multi_df_display_coords[gr_screen.res][2] - (MULTI_DF_TOTAL_ADJUST + str_len), cy, squashed_string, GR_RESIZE_MENU); + int w; + gr_get_string_size(&w, nullptr, squashed_string); + gr_string(Multi_df_display_coords[gr_screen.res][0] + Multi_df_display_coords[gr_screen.res][2] - (MULTI_DF_TOTAL_ADJUST + w), cy, squashed_string, GR_RESIZE_MENU); cy += dy; } diff --git a/code/network/multi_pxo.cpp b/code/network/multi_pxo.cpp index 6cbc8326c60..58ab5187984 100644 --- a/code/network/multi_pxo.cpp +++ b/code/network/multi_pxo.cpp @@ -3029,8 +3029,6 @@ void multi_pxo_chat_process_incoming(const char *txt,int mode) */ void multi_pxo_chat_blit() { - int token_width; - // blit the title line char title[MAX_PXO_TEXT_LEN]; memset(title,0,MAX_PXO_TEXT_LEN); @@ -3043,10 +3041,9 @@ void multi_pxo_chat_blit() } else { strcpy_s(title,XSTR("Parallax Online - No Channel", 956)); } - font::force_fit_string(title, MAX_PXO_TEXT_LEN-1, Multi_pxo_chat_coords[gr_screen.res][2] - 10); - gr_get_string_size(&token_width,nullptr,title); + int title_width = font::force_fit_string(title, MAX_PXO_TEXT_LEN-1, Multi_pxo_chat_coords[gr_screen.res][2] - 10); gr_set_color_fast(&Color_normal); - gr_string(Multi_pxo_chat_coords[gr_screen.res][0] + ((Multi_pxo_chat_coords[gr_screen.res][2] - token_width)/2), Multi_pxo_chat_title_y[gr_screen.res], title, GR_RESIZE_MENU); + gr_string(Multi_pxo_chat_coords[gr_screen.res][0] + ((Multi_pxo_chat_coords[gr_screen.res][2] - title_width)/2), Multi_pxo_chat_title_y[gr_screen.res], title, GR_RESIZE_MENU); int disp_count, y_start; int line_height = gr_get_font_height() + 1; @@ -3078,12 +3075,13 @@ void multi_pxo_chat_blit() // normal mode, just highlight the server case CHAT_MODE_PRIVATE: - case CHAT_MODE_NORMAL: + case CHAT_MODE_NORMAL: { char piece[MAX_CHAT_LINE_LEN + 1]; strcpy_s(piece, line->text); tok = strtok(piece, " "); if (tok != nullptr) { // get the width of just the first "piece" + int token_width; gr_get_string_size(&token_width, nullptr, tok); // draw it brightly @@ -3101,6 +3099,7 @@ void multi_pxo_chat_blit() } } break; + } // carry mode, display with no highlight case CHAT_MODE_CARRY: diff --git a/code/network/multiui.cpp b/code/network/multiui.cpp index 66d57eca132..a282a4a8e5d 100644 --- a/code/network/multiui.cpp +++ b/code/network/multiui.cpp @@ -4793,7 +4793,8 @@ void multi_create_list_set_item(int abs_index, int mode) { ng->max_players = mission_parse_get_multi_mission_info(ng->mission_name); Assert(ng->max_players > 0); - strcpy_s(ng->title, The_mission.name); + strncpy(ng->title, The_mission.name.c_str(), NAME_LENGTH); + ng->title[NAME_LENGTH] = '\0'; // set the information area text Multi_netgame_common_description = The_mission.mission_desc; diff --git a/code/playerman/managepilot.cpp b/code/playerman/managepilot.cpp index 656a409ca4f..fda8d8b4dfd 100644 --- a/code/playerman/managepilot.cpp +++ b/code/playerman/managepilot.cpp @@ -264,8 +264,7 @@ void pilot_set_short_callsign(player *p, int max_width) { strcpy_s(p->short_callsign, p->callsign); font::set_font(font::FONT1); - font::force_fit_string(p->short_callsign, CALLSIGN_LEN - 1, max_width); - gr_get_string_size( &(p->short_callsign_width), NULL, p->short_callsign ); + p->short_callsign_width = font::force_fit_string(p->short_callsign, CALLSIGN_LEN - 1, max_width); } // pick a random image for the passed player diff --git a/fred2/dumpstats.cpp b/fred2/dumpstats.cpp index 3959c9901ea..1f3cf4d8b30 100644 --- a/fred2/dumpstats.cpp +++ b/fred2/dumpstats.cpp @@ -132,7 +132,7 @@ void DumpStats::get_mission_stats(CString &buffer) // Mission info buffer += "\t MISSION INFO\r\n"; - temp.Format("Title: %s\r\n", The_mission.name); + temp.Format("Title: %s\r\n", The_mission.name.c_str()); buffer += temp; temp.Format("Filename: %s\r\n", Mission_filename); diff --git a/fred2/freddoc.cpp b/fred2/freddoc.cpp index 8c1deb346bf..37bb25fe83f 100644 --- a/fred2/freddoc.cpp +++ b/fred2/freddoc.cpp @@ -257,9 +257,9 @@ bool CFREDDoc::load_mission(const char *pathname, int flags) { // message 2: unknown classes if ((Num_unknown_ship_classes > 0) || (Num_unknown_prop_classes > 0) || (Num_unknown_weapon_classes > 0) || (Num_unknown_loadout_classes > 0)) { if (flags & MPF_IMPORT_FSM) { - char msg[256]; - sprintf(msg, "Fred encountered unknown ship/prop/weapon classes when importing \"%s\" (path \"%s\"). You will have to manually edit the converted mission to correct this.", The_mission.name, pathname); - Fred_view_wnd->MessageBox(msg); + SCP_string msg; + sprintf(msg, "Fred encountered unknown ship/prop/weapon classes when importing \"%s\" (path \"%s\"). You will have to manually edit the converted mission to correct this.", The_mission.name.c_str(), pathname); + Fred_view_wnd->MessageBox(msg.c_str()); } else { Fred_view_wnd->MessageBox("Fred encountered unknown ship/prop/weapon classes when parsing the mission file. This may be due to mission disk data you do not have."); } diff --git a/fred2/fredview.cpp b/fred2/fredview.cpp index 2281c2b290c..56945ec5a41 100644 --- a/fred2/fredview.cpp +++ b/fred2/fredview.cpp @@ -2475,10 +2475,6 @@ int CFREDView::global_error_check() if ( The_mission.game_type & MISSION_TYPE_MULTI ) multi = 1; -// if (!stricmp(The_mission.name, "Untitled")) -// if (error("You haven't given this mission a title yet.\nThis is done from the Mission Specs Editor (Shift-N).")) -// return 1; - // cycle though all the objects and verify every possible aspect of them obj_count = t = 0; ptr = GET_FIRST(&obj_used_list); diff --git a/fred2/management.cpp b/fred2/management.cpp index 70e07adcf75..b6187d3ad4f 100644 --- a/fred2/management.cpp +++ b/fred2/management.cpp @@ -909,7 +909,7 @@ void clear_mission(bool fast_reload) time(¤tTime); auto timeinfo = localtime(¤tTime); - strcpy_s(The_mission.name, "Untitled"); + The_mission.name = "Untitled"; The_mission.author = str; time_to_mission_info_string(timeinfo, The_mission.created, DATE_TIME_LENGTH - 1); strcpy_s(The_mission.modified, The_mission.created); diff --git a/fred2/missionnotesdlg.cpp b/fred2/missionnotesdlg.cpp index 4531c5ebfe3..4ede0e3aff4 100644 --- a/fred2/missionnotesdlg.cpp +++ b/fred2/missionnotesdlg.cpp @@ -320,7 +320,7 @@ void CMissionNotesDlg::OnOK() // puts "$End Notes:" on a different line to ensure it's not interpreted as part of a comment pad_with_newline(m_mission_notes, NOTES_LENGTH - 1); - string_copy(The_mission.name, m_mission_title, NAME_LENGTH - 1, 1); + string_copy(The_mission.name, m_mission_title, true); string_copy(The_mission.author, m_designer_name, true); string_copy(The_mission.loading_screen[GR_640], m_loading_640, NAME_LENGTH - 1, 1); string_copy(The_mission.loading_screen[GR_1024], m_loading_1024, NAME_LENGTH - 1, 1); @@ -388,7 +388,7 @@ BOOL CMissionNotesDlg::OnInitDialog() team = (CButton *)GetDlgItem(IDC_TEAMVTEAM); dogfight = (CButton *)GetDlgItem(IDC_DOGFIGHT); - m_mission_title_orig = m_mission_title = _T(The_mission.name); + m_mission_title_orig = m_mission_title = _T(The_mission.name.c_str()); m_designer_name_orig = m_designer_name = _T(The_mission.author.c_str()); m_created = _T(The_mission.created); m_modified = _T(The_mission.modified); diff --git a/fred2/voiceactingmanager.cpp b/fred2/voiceactingmanager.cpp index b2c144e963e..5d6340b177a 100644 --- a/fred2/voiceactingmanager.cpp +++ b/fred2/voiceactingmanager.cpp @@ -408,7 +408,7 @@ void VoiceActingManager::OnGenerateScript() } fout("%s\n", Mission_filename); - fout("%s\n\n", The_mission.name); + fout("%s\n\n", The_mission.name.c_str()); if (m_export_everything || m_export_command_briefings) { diff --git a/qtfred/src/mission/Editor.cpp b/qtfred/src/mission/Editor.cpp index 14275f9347c..87aa811a3a2 100644 --- a/qtfred/src/mission/Editor.cpp +++ b/qtfred/src/mission/Editor.cpp @@ -308,7 +308,7 @@ bool Editor::loadMission(const std::string& mission_name, int flags) { SCP_string msg; sprintf(msg, "Fred encountered unknown ship/prop/weapon classes when importing \"%s\" (path \"%s\"). You will have to manually edit the converted mission to correct this.", - The_mission.name, + The_mission.name.c_str(), filepath.c_str()); _lastActiveViewport->dialogProvider->showButtonDialog(DialogType::Warning, @@ -455,7 +455,7 @@ bool Editor::loadMission(const std::string& mission_name, int flags) { if (flags & MPF_IS_TEMPLATE) { // reset fields that should not carry over from the template source - strcpy_s(The_mission.name, "Untitled"); + The_mission.name = "Untitled"; The_mission.author = getUsername(); time_t currentTime; @@ -592,7 +592,7 @@ void Editor::clearMission(bool fast_reload) { time(¤tTime); auto timeinfo = localtime(¤tTime); - strcpy_s(The_mission.name, "Untitled"); + The_mission.name = "Untitled"; The_mission.author = userName; time_to_mission_info_string(timeinfo, The_mission.created, DATE_TIME_LENGTH - 1); strcpy_s(The_mission.modified, The_mission.created); diff --git a/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp b/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp index ffd6453dd9e..987097c853f 100644 --- a/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp +++ b/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp @@ -149,7 +149,7 @@ bool MissionSpecDialogModel::apply() { // puts "$End Notes:" on a different line to ensure it's not interpreted as part of a comment Editor::pad_with_newline(_m_mission_notes, NOTES_LENGTH - 1); - strncpy(The_mission.name, _m_mission_title.c_str(), NAME_LENGTH-1); + The_mission.name = _m_mission_title; The_mission.author = _m_designer_name; strncpy(The_mission.loading_screen[GR_640], _m_loading_640.c_str(), NAME_LENGTH-1); strncpy(The_mission.loading_screen[GR_1024], _m_loading_1024.c_str(), NAME_LENGTH-1); diff --git a/qtfred/src/mission/dialogs/VoiceActingManagerModel.cpp b/qtfred/src/mission/dialogs/VoiceActingManagerModel.cpp index 73b3432be7c..ec46d6deefa 100644 --- a/qtfred/src/mission/dialogs/VoiceActingManagerModel.cpp +++ b/qtfred/src/mission/dialogs/VoiceActingManagerModel.cpp @@ -353,7 +353,7 @@ bool VoiceActingManagerModel::generateScript(const SCP_string& absoluteFilePath) // Mission metadata fout(fp, "%s\n", Mission_filename); - fout(fp, "%s\n\n", The_mission.name); + fout(fp, "%s\n\n", The_mission.name.c_str()); auto writeMessageEntry = [&](const char* filename, const SCP_string& text, diff --git a/qtfred/src/ui/dialogs/CampaignEditorDialog.cpp b/qtfred/src/ui/dialogs/CampaignEditorDialog.cpp index d6132e23f63..24fd14d8d5c 100644 --- a/qtfred/src/ui/dialogs/CampaignEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/CampaignEditorDialog.cpp @@ -529,8 +529,8 @@ void CampaignEditorDialog::on_availableMissionsListWidget_itemSelectionChanged() return; } - if (mission_info.name[0] != '\0') { - ui->missionNameLineEdit->setText(QString::fromUtf8(mission_info.name)); + if (!mission_info.name.empty()) { + ui->missionNameLineEdit->setText(QString::fromStdString(mission_info.name)); } else { ui->missionNameLineEdit->clear(); } @@ -554,8 +554,8 @@ void CampaignEditorDialog::on_graphView_missionSelected(int missionIndex) { return; } - if (mission_info.name[0] != '\0') { - ui->missionNameLineEdit->setText(QString::fromUtf8(mission_info.name)); + if (!mission_info.name.empty()) { + ui->missionNameLineEdit->setText(QString::fromStdString(mission_info.name)); } else { ui->missionNameLineEdit->clear(); } diff --git a/qtfred/src/ui/dialogs/MissionSpecDialog.cpp b/qtfred/src/ui/dialogs/MissionSpecDialog.cpp index a5ee42e72b8..70f5d4fc417 100644 --- a/qtfred/src/ui/dialogs/MissionSpecDialog.cpp +++ b/qtfred/src/ui/dialogs/MissionSpecDialog.cpp @@ -23,8 +23,6 @@ MissionSpecDialog::MissionSpecDialog(FredView* parent, EditorViewport* viewport) _viewport(viewport) { ui->setupUi(this); - ui->missionTitle->setMaxLength(NAME_LENGTH - 1); - ui->missionDesigner->setMaxLength(NAME_LENGTH - 1); ui->squadronName->setMaxLength(NAME_LENGTH - 1); ui->squadronLogo->setMaxLength(MAX_FILENAME_LEN - 1); ui->lowResScreen->setMaxLength(MAX_FILENAME_LEN - 1); From ddb6c2eaeda34e0fb7d6dc0ea81355ab2830dd30 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 13 May 2026 06:40:44 -0500 Subject: [PATCH 38/65] QtFRED Waypoint Dialog Upgrade (#7419) * make waypoint multi select and direct edit * clang * update qtfred help docs * fixes * static * address feedback --- .../doc/dialogs/WaypointEditorDialog.html | 24 +- .../dialogs/WaypointEditorDialogModel.cpp | 471 ++++++++++++------ .../dialogs/WaypointEditorDialogModel.h | 47 +- qtfred/src/ui/FredView.cpp | 2 +- .../src/ui/dialogs/WaypointEditorDialog.cpp | 108 ++-- qtfred/src/ui/dialogs/WaypointEditorDialog.h | 9 +- qtfred/ui/WaypointEditorDialog.ui | 70 ++- 7 files changed, 450 insertions(+), 281 deletions(-) diff --git a/qtfred/help-src/doc/dialogs/WaypointEditorDialog.html b/qtfred/help-src/doc/dialogs/WaypointEditorDialog.html index 32ec880f2e5..5ba892c31b4 100644 --- a/qtfred/help-src/doc/dialogs/WaypointEditorDialog.html +++ b/qtfred/help-src/doc/dialogs/WaypointEditorDialog.html @@ -14,20 +14,32 @@

Waypoint Editor

Waypoint paths are simply ordered sequences of arbitrary positions in space and can be used for any purpose a mission designer can express through SEXPs and AI goals.

+

The dialog tracks the viewport selection. Select waypoint objects in the viewport +to edit their path. When multiple paths are selected, display property changes +(No Draw Lines, Custom Color, Layer) apply to all selected paths at once.

+ +

Navigation

+ + + +
ButtonDescription
Prev / NextCycles the viewport selection to the previous or next + waypoint path in the mission, allowing sequential editing without switching + back to the viewport.
+

Key fields

- - + unique. When multiple paths are selected the Name field is read-only. Rename + single paths individually. + + visible. Applied to all selected paths. + Does not affect in-game appearance. Applied to all selected paths.
FieldDescription
Waypoint pathSelects which path to edit. New paths are created by - placing waypoint objects in the viewport.
NameIdentifies the path in SEXPs and AI goal assignments. Must be - unique.
LayerThe layer the waypoint path is assigned to.
LayerThe layer the waypoint path is assigned to. Applied to all + selected paths.
No Draw LinesWhen checked, the connecting lines between waypoints are hidden in the viewport. The waypoint points themselves are still - visible.
Custom ColorWhen checked, enables the RGB fields below to set a custom color for rendering this path's points and lines in the viewport. - Does not affect in-game appearance.
Individual waypoint positions are moved by selecting the waypoint diff --git a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp index 48f4b95c30d..ec053d77c4b 100644 --- a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp @@ -2,10 +2,34 @@ #include #include #include +#include #include "mission/dialogs/WaypointEditorDialogModel.h" namespace fso::fred::dialogs { +namespace { + +// Writes one or more color channels to every selected path that already uses a custom color. +// Channels still flagged mixed retain their per-path value; non-mixed channels take the model value. +// Skips paths without custom color so a channel edit never silently turns custom-color on. +void applyChannelsToAllPaths(const SCP_vector& selected, + int r, int g, int b, + bool rMixed, bool gMixed, bool bMixed) +{ + for (auto idx : selected) { + auto& path = Waypoint_lists[idx]; + if (!path.get_has_custom_color()) { + continue; + } + int outR = rMixed ? path.get_color_r() : r; + int outG = gMixed ? path.get_color_g() : g; + int outB = bMixed ? path.get_color_b() : b; + path.set_color(outR, outG, outB); + } +} + +} // namespace + WaypointEditorDialogModel::WaypointEditorDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) { connect(viewport->editor, &Editor::currentObjectChanged, this, &WaypointEditorDialogModel::onSelectedObjectChanged); @@ -15,151 +39,138 @@ WaypointEditorDialogModel::WaypointEditorDialogModel(QObject* parent, EditorView initializeData(); } -bool WaypointEditorDialogModel::apply() -{ - if (!validateData()) { - return false; - } - - // apply name - char old_name[NAME_LENGTH]; - strcpy_s(old_name, _editor->cur_waypoint_list->get_name()); - _editor->cur_waypoint_list->set_name(_currentName.c_str()); - auto str = _editor->cur_waypoint_list->get_name(); - if (strcmp(old_name, str) != 0) { - _editor->missionChanged(); - update_sexp_references(old_name, str); - _editor->ai_update_goal_references(sexp_ref_type::WAYPOINT_PATH, old_name, str); - - for (auto &wpt : _editor->cur_waypoint_list->get_waypoints()) { - char old_buf[NAME_LENGTH]; - char new_buf[NAME_LENGTH]; - waypoint_stuff_name(old_buf, old_name, wpt.get_index() + 1); - waypoint_stuff_name(new_buf, str, wpt.get_index() + 1); - update_sexp_references(old_buf, new_buf); - _editor->ai_update_goal_references(sexp_ref_type::WAYPOINT, old_buf, new_buf); - } - } - - // apply display properties - _editor->cur_waypoint_list->set_no_draw_lines(_noDrawLines); - if (_hasCustomColor) - _editor->cur_waypoint_list->set_color((ubyte)_colorR, (ubyte)_colorG, (ubyte)_colorB); - else - _editor->cur_waypoint_list->clear_color(); - - _editor->missionChanged(); - +bool WaypointEditorDialogModel::apply() { return true; } -void WaypointEditorDialogModel::reject() -{ - // do nothing -} +void WaypointEditorDialogModel::reject() {} -void WaypointEditorDialogModel::initializeData() -{ - _enabled = true; +void WaypointEditorDialogModel::initializeData() { + _selectedWaypointPaths.clear(); + _noDrawLinesMixed = false; + _hasCustomColorMixed = false; + _redMixed = _greenMixed = _blueMixed = false; - if (query_valid_object(_editor->currentObject) && Objects[_editor->currentObject].type == OBJ_WAYPOINT) { - Assertion(_editor->cur_waypoint_list == find_waypoint_list_with_instance(Objects[_editor->currentObject].instance), "Waypoint no longer exists in the mission!"); + // Collect unique waypoint list indices from all marked OBJ_WAYPOINT objects + SCP_vector seen(Waypoint_lists.size(), false); + for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if (ptr->type == OBJ_WAYPOINT && ptr->flags[Object::Object_Flags::Marked]) { + int listIdx = calc_waypoint_list_index(ptr->instance); + if (listIdx >= 0 && listIdx < static_cast(Waypoint_lists.size()) && !seen[listIdx]) { + seen[listIdx] = true; + _selectedWaypointPaths.push_back(listIdx); + } + } } - updateWaypointPathList(); + // Fall back to cur_waypoint_list if nothing marked + if (_selectedWaypointPaths.empty() && _editor->cur_waypoint_list != nullptr) { + int idx = find_index_of_waypoint_list(_editor->cur_waypoint_list); + if (idx >= 0) { + _selectedWaypointPaths.push_back(idx); + } + } - if (_editor->cur_waypoint_list != nullptr) { - _currentName = _editor->cur_waypoint_list->get_name(); - _noDrawLines = _editor->cur_waypoint_list->get_no_draw_lines(); - _hasCustomColor = _editor->cur_waypoint_list->get_has_custom_color(); - _colorR = _editor->cur_waypoint_list->get_color_r(); - _colorG = _editor->cur_waypoint_list->get_color_g(); - _colorB = _editor->cur_waypoint_list->get_color_b(); + if (!_selectedWaypointPaths.empty()) { + const auto& path = Waypoint_lists[_selectedWaypointPaths.front()]; + _currentName = path.get_name(); + _noDrawLines = path.get_no_draw_lines(); + _hasCustomColor = path.get_has_custom_color(); + _colorR = path.get_color_r(); + _colorG = path.get_color_g(); + _colorB = path.get_color_b(); + + if (hasMultipleSelection()) { + for (size_t i = 1; i < _selectedWaypointPaths.size(); ++i) { + const auto& other = Waypoint_lists[_selectedWaypointPaths[i]]; + if (other.get_no_draw_lines() != _noDrawLines) + _noDrawLinesMixed = true; + if (other.get_has_custom_color() != _hasCustomColor) + _hasCustomColorMixed = true; + if (other.get_color_r() != _colorR) + _redMixed = true; + if (other.get_color_g() != _colorG) + _greenMixed = true; + if (other.get_color_b() != _colorB) + _blueMixed = true; + } + } } else { - _currentName = ""; + _currentName.clear(); _noDrawLines = false; _hasCustomColor = false; _colorR = _colorG = _colorB = 255; - _enabled = false; } Q_EMIT waypointPathMarkingChanged(); _modified = false; } -void WaypointEditorDialogModel::updateWaypointPathList() -{ - - _waypointPathList.clear(); - _currentWaypointPathSelected = -1; +bool WaypointEditorDialogModel::hasValidSelection() const { + return !_selectedWaypointPaths.empty(); +} - for (size_t i = 0; i < Waypoint_lists.size(); ++i) { - _waypointPathList.emplace_back(Waypoint_lists[i].get_name(), static_cast(i)); - } +bool WaypointEditorDialogModel::hasMultipleSelection() const { + return _selectedWaypointPaths.size() > 1; +} - if (_editor->cur_waypoint_list != nullptr) { - int index = find_index_of_waypoint_list(_editor->cur_waypoint_list); - Assertion(index >= 0, "Could not find waypoint path in waypoint path list!"); - _currentWaypointPathSelected = index; - } +bool WaypointEditorDialogModel::hasAnyPathsInMission() { + return !Waypoint_lists.empty(); } -bool WaypointEditorDialogModel::validateData() -{ - // Reset flag before applying - _bypass_errors = false; +int WaypointEditorDialogModel::getSelectionCount() const { + return static_cast(_selectedWaypointPaths.size()); +} - if (query_valid_object(_editor->currentObject) && Objects[_editor->currentObject].type == OBJ_WAYPOINT) { - Assertion(_editor->cur_waypoint_list == find_waypoint_list_with_instance(Objects[_editor->currentObject].instance), "Waypoint no longer exists in the mission!"); +bool WaypointEditorDialogModel::validateName(const SCP_string& name) { + if (name.empty()) { + showErrorDialogNoCancel("Waypoint path name cannot be empty."); + return false; } // wing name collision for (auto& wing : Wings) { - if (!stricmp(wing.name, _currentName.c_str())) { + if (!stricmp(wing.name, name.c_str())) { showErrorDialogNoCancel("This waypoint path name is already being used by a wing"); return false; } } // ship name collision - object* ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { + for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if ((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) { - if (!stricmp(_currentName.c_str(), Ships[ptr->instance].ship_name)) { + if (!stricmp(name.c_str(), Ships[ptr->instance].ship_name)) { showErrorDialogNoCancel("This waypoint path name is already being used by a ship"); return false; } } - - ptr = GET_NEXT(ptr); } - // We don't need to check teams. "Unknown" is a valid name and also an IFF. - // target priority group name collision for (auto& ai : Ai_tp_list) { - if (!stricmp(_currentName.c_str(), ai.name)) { + if (!stricmp(name.c_str(), ai.name)) { showErrorDialogNoCancel("This waypoint path name is already being used by a target priority group"); return false; } } // waypoint path name collision + const waypoint_list* current_path = &Waypoint_lists[_selectedWaypointPaths.front()]; for (const auto& ii : Waypoint_lists) { - if (!stricmp(ii.get_name(), _currentName.c_str()) && (&ii != _editor->cur_waypoint_list)) { + if (!stricmp(ii.get_name(), name.c_str()) && (&ii != current_path)) { showErrorDialogNoCancel("This waypoint path name is already being used by another waypoint path"); return false; } } // jump node name collision - if (jumpnode_get_by_name(_currentName.c_str()) != nullptr) { + if (jumpnode_get_by_name(name.c_str()) != nullptr) { showErrorDialogNoCancel("This waypoint path name is already being used by a jump node"); return false; } // formatting - if (!_currentName.empty() && _currentName[0] == '<') { + if (name[0] == '<') { showErrorDialogNoCancel("Waypoint names not allowed to begin with '<'"); return false; } @@ -167,132 +178,262 @@ bool WaypointEditorDialogModel::validateData() return true; } -void WaypointEditorDialogModel::showErrorDialogNoCancel(const SCP_string& message) -{ +void WaypointEditorDialogModel::showErrorDialogNoCancel(const SCP_string& message) { if (_bypass_errors) { return; } - _bypass_errors = true; _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Error", message, {DialogButton::Ok}); } -void WaypointEditorDialogModel::onSelectedObjectChanged(int) { - initializeData(); +const SCP_string& WaypointEditorDialogModel::getCurrentName() const { + return _currentName; } -void WaypointEditorDialogModel::onSelectedObjectMarkingChanged(int, bool) { - initializeData(); -} +bool WaypointEditorDialogModel::setCurrentName(const SCP_string& name) { + if (hasMultipleSelection() || _selectedWaypointPaths.empty()) { + // Name field should be disabled in these cases; signal failure so the dialog + // restores the displayed text from getCurrentName(). + return false; + } -void WaypointEditorDialogModel::onMissionChanged() -{ - // When the mission is changed we also need to update our data in case one of our elements changed - initializeData(); -} + _bypass_errors = false; -const SCP_string& WaypointEditorDialogModel::getCurrentName() const { - return _currentName; -} + SCP_string trimmed = name; + SCP_trim(trimmed); -void WaypointEditorDialogModel::setCurrentName(const SCP_string& name) -{ - modify(_currentName, name); -} + if (!validateName(trimmed)) { + return false; + } -int WaypointEditorDialogModel::getCurrentlySelectedPath() const { - return _currentWaypointPathSelected; -} + auto& path = Waypoint_lists[_selectedWaypointPaths.front()]; -void WaypointEditorDialogModel::setCurrentlySelectedPath(int id) -{ - if (_currentWaypointPathSelected == id) { - // Nothing to do here - return; + char old_name[NAME_LENGTH]; + strcpy_s(old_name, path.get_name()); + path.set_name(trimmed.c_str()); + const char* new_name = path.get_name(); + + if (strcmp(old_name, new_name) != 0) { + update_sexp_references(old_name, new_name); + _editor->ai_update_goal_references(sexp_ref_type::WAYPOINT_PATH, old_name, new_name); + + for (auto& wpt : path.get_waypoints()) { + char old_buf[NAME_LENGTH]; + char new_buf[NAME_LENGTH]; + waypoint_stuff_name(old_buf, old_name, wpt.get_index() + 1); + waypoint_stuff_name(new_buf, new_name, wpt.get_index() + 1); + update_sexp_references(old_buf, new_buf); + _editor->ai_update_goal_references(sexp_ref_type::WAYPOINT, old_buf, new_buf); + } } - if (id < 0 || id >= static_cast(Waypoint_lists.size())) { - return; // out of range; ignore + _currentName = new_name; + _suppressRefresh = true; + set_modified(); + _editor->missionChanged(); + _suppressRefresh = false; + return true; +} + +bool WaypointEditorDialogModel::getNoDrawLines() const { return _noDrawLines; } + +int WaypointEditorDialogModel::getNoDrawLinesState() const { + if (_noDrawLinesMixed) return Qt::PartiallyChecked; + return _noDrawLines ? Qt::Checked : Qt::Unchecked; +} + +void WaypointEditorDialogModel::setNoDrawLines(bool val) { + _noDrawLines = val; + _noDrawLinesMixed = false; + for (auto idx : _selectedWaypointPaths) { + Waypoint_lists[idx].set_no_draw_lines(val); } + _suppressRefresh = true; + set_modified(); + _editor->missionChanged(); + _suppressRefresh = false; +} - // Only apply if there is actually a current path to save changes to. - bool canProceed = (_editor->cur_waypoint_list == nullptr) || apply(); +bool WaypointEditorDialogModel::getHasCustomColor() const { return _hasCustomColor; } - if (canProceed) { - _editor->unmark_all(); +int WaypointEditorDialogModel::getHasCustomColorState() const { + if (_hasCustomColorMixed) return Qt::PartiallyChecked; + return _hasCustomColor ? Qt::Checked : Qt::Unchecked; +} - // mark all waypoints belonging to the selected list - int listIndex = id; - for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { - if (ptr->type == OBJ_WAYPOINT) { - if (calc_waypoint_list_index(ptr->instance) == listIndex) { - _editor->markObject(OBJ_INDEX(ptr)); - } - } +void WaypointEditorDialogModel::setHasCustomColor(bool val) { + _hasCustomColor = val; + _hasCustomColorMixed = false; + for (auto idx : _selectedWaypointPaths) { + auto& path = Waypoint_lists[idx]; + if (val) { + // Preserve per-path channel values for channels still flagged mixed; apply + // the model's resolved channel values otherwise. + int outR = _redMixed ? path.get_color_r() : _colorR; + int outG = _greenMixed ? path.get_color_g() : _colorG; + int outB = _blueMixed ? path.get_color_b() : _colorB; + path.set_color(outR, outG, outB); + } else { + path.clear_color(); } + } + _suppressRefresh = true; + set_modified(); + _editor->missionChanged(); + _suppressRefresh = false; +} + +int WaypointEditorDialogModel::getColorR() const { return _colorR; } - _currentWaypointPathSelected = id; +void WaypointEditorDialogModel::setColorR(int r) { + // The spinbox uses -1 as its "mixed" sentinel (minimum=-1, specialValueText=" "). + // A read-back of that sentinel must leave _redMixed untouched... don't "simplify" + // this guard by letting the value fall through to a clear. + if (r < 0) return; + CLAMP(r, 0, 255); + _colorR = r; + _redMixed = false; + if (_hasCustomColor && !_hasCustomColorMixed) { + applyChannelsToAllPaths(_selectedWaypointPaths, _colorR, _colorG, _colorB, + _redMixed, _greenMixed, _blueMixed); + _suppressRefresh = true; + set_modified(); + _editor->missionChanged(); + _suppressRefresh = false; } } -bool WaypointEditorDialogModel::isEnabled() const { - return _enabled; +int WaypointEditorDialogModel::getColorG() const { return _colorG; } + +void WaypointEditorDialogModel::setColorG(int g) { + // -1 = spinbox "mixed" sentinel; leave _greenMixed untouched. See setColorR. + if (g < 0) return; + CLAMP(g, 0, 255); + _colorG = g; + _greenMixed = false; + if (_hasCustomColor && !_hasCustomColorMixed) { + applyChannelsToAllPaths(_selectedWaypointPaths, _colorR, _colorG, _colorB, + _redMixed, _greenMixed, _blueMixed); + _suppressRefresh = true; + set_modified(); + _editor->missionChanged(); + _suppressRefresh = false; + } } -const SCP_vector>& WaypointEditorDialogModel::getWaypointPathList() const -{ - return _waypointPathList; +int WaypointEditorDialogModel::getColorB() const { return _colorB; } + +void WaypointEditorDialogModel::setColorB(int b) { + // -1 = spinbox "mixed" sentinel; leave _blueMixed untouched. See setColorR. + if (b < 0) return; + CLAMP(b, 0, 255); + _colorB = b; + _blueMixed = false; + if (_hasCustomColor && !_hasCustomColorMixed) { + applyChannelsToAllPaths(_selectedWaypointPaths, _colorR, _colorG, _colorB, + _redMixed, _greenMixed, _blueMixed); + _suppressRefresh = true; + set_modified(); + _editor->missionChanged(); + _suppressRefresh = false; + } } -SCP_string WaypointEditorDialogModel::getLayer() const -{ - if (_editor->cur_waypoint_list == nullptr) - return ""; +bool WaypointEditorDialogModel::isColorRMixed() const { return _redMixed; } +bool WaypointEditorDialogModel::isColorGMixed() const { return _greenMixed; } +bool WaypointEditorDialogModel::isColorBMixed() const { return _blueMixed; } +bool WaypointEditorDialogModel::hasAnyColorMixed() const { + return _redMixed || _greenMixed || _blueMixed; +} - int listIndex = find_index_of_waypoint_list(_editor->cur_waypoint_list); +SCP_string WaypointEditorDialogModel::getLayer() const { + std::unordered_set selected(_selectedWaypointPaths.begin(), _selectedWaypointPaths.end()); SCP_string result; bool first = true; - for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { - if (ptr->type == OBJ_WAYPOINT && calc_waypoint_list_index(ptr->instance) == listIndex) { - SCP_string layer = _viewport->getObjectLayerName(OBJ_INDEX(ptr)); - if (first) { - result = layer; - first = false; - } else if (result != layer) { - return ""; - } + if (ptr->type != OBJ_WAYPOINT) continue; + if (selected.find(calc_waypoint_list_index(ptr->instance)) == selected.end()) continue; + SCP_string layer = _viewport->getObjectLayerName(OBJ_INDEX(ptr)); + if (first) { + result = layer; + first = false; + } else if (result != layer) { + return ""; } } return result; } -void WaypointEditorDialogModel::setLayer(const SCP_string& layer) -{ - if (_editor->cur_waypoint_list == nullptr) +void WaypointEditorDialogModel::setLayer(const SCP_string& layer) { + // moveObjectToLayer may unmark objects (hidden target layer) and fires + // notifyLayerStructureChanged. Both reach back into our refresh slots and would + // otherwise rebuild the dialog once per affected waypoint object. + _suppressRefresh = true; + std::unordered_set selected(_selectedWaypointPaths.begin(), _selectedWaypointPaths.end()); + for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if (ptr->type != OBJ_WAYPOINT) continue; + if (selected.find(calc_waypoint_list_index(ptr->instance)) == selected.end()) continue; + _viewport->moveObjectToLayer(OBJ_INDEX(ptr), layer); + } + set_modified(); + _editor->missionChanged(); + _suppressRefresh = false; + // One refresh now that the dust has settled. + initializeData(); +} + +void WaypointEditorDialogModel::selectWaypointPathByIndex(int idx) { + if (idx < 0 || idx >= static_cast(Waypoint_lists.size())) return; - int listIndex = find_index_of_waypoint_list(_editor->cur_waypoint_list); + // Prevent rebuilding the dialog for each waypoint as we mark them + _suppressRefresh = true; + _editor->unmark_all(); for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { - if (ptr->type == OBJ_WAYPOINT && calc_waypoint_list_index(ptr->instance) == listIndex) { - _viewport->moveObjectToLayer(OBJ_INDEX(ptr), layer); + if (ptr->type == OBJ_WAYPOINT && calc_waypoint_list_index(ptr->instance) == idx) { + _editor->markObject(OBJ_INDEX(ptr)); } } - set_modified(); - _editor->missionChanged(); + _suppressRefresh = false; + initializeData(); } -bool WaypointEditorDialogModel::getNoDrawLines() const { return _noDrawLines; } -void WaypointEditorDialogModel::setNoDrawLines(bool val) { modify(_noDrawLines, val); } +void WaypointEditorDialogModel::selectNextPath() { + if (Waypoint_lists.empty()) + return; + if (_selectedWaypointPaths.empty()) { + selectWaypointPathByIndex(0); + return; + } + int next = (_selectedWaypointPaths.front() + 1) % static_cast(Waypoint_lists.size()); + selectWaypointPathByIndex(next); +} -bool WaypointEditorDialogModel::getHasCustomColor() const { return _hasCustomColor; } -void WaypointEditorDialogModel::setHasCustomColor(bool val) { modify(_hasCustomColor, val); } +void WaypointEditorDialogModel::selectPreviousPath() { + if (Waypoint_lists.empty()) + return; + if (_selectedWaypointPaths.empty()) { + selectWaypointPathByIndex(static_cast(Waypoint_lists.size()) - 1); + return; + } + int prev = (_selectedWaypointPaths.front() - 1 + static_cast(Waypoint_lists.size())) + % static_cast(Waypoint_lists.size()); + selectWaypointPathByIndex(prev); +} -int WaypointEditorDialogModel::getColorR() const { return _colorR; } -int WaypointEditorDialogModel::getColorG() const { return _colorG; } -int WaypointEditorDialogModel::getColorB() const { return _colorB; } -void WaypointEditorDialogModel::setColorR(int r) { modify(_colorR, r); } -void WaypointEditorDialogModel::setColorG(int g) { modify(_colorG, g); } -void WaypointEditorDialogModel::setColorB(int b) { modify(_colorB, b); } +void WaypointEditorDialogModel::onSelectedObjectChanged(int) { + if (_suppressRefresh) return; + initializeData(); +} + +void WaypointEditorDialogModel::onSelectedObjectMarkingChanged(int, bool) { + if (_suppressRefresh) return; + initializeData(); +} + +void WaypointEditorDialogModel::onMissionChanged() { + if (_suppressRefresh) return; + initializeData(); +} } // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h index 2d633b167f7..5bdb49ab1d7 100644 --- a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h @@ -3,24 +3,31 @@ namespace fso::fred::dialogs { -class WaypointEditorDialogModel: public AbstractDialogModel { - Q_OBJECT +class WaypointEditorDialogModel : public AbstractDialogModel { + Q_OBJECT - public: +public: WaypointEditorDialogModel(QObject* parent, EditorViewport* viewport); bool apply() override; void reject() override; + bool hasValidSelection() const; + bool hasMultipleSelection() const; + static bool hasAnyPathsInMission(); + int getSelectionCount() const; + const SCP_string& getCurrentName() const; - void setCurrentName(const SCP_string& name); - int getCurrentlySelectedPath() const; - void setCurrentlySelectedPath(int elementId); + bool setCurrentName(const SCP_string& name); bool getNoDrawLines() const; + int getNoDrawLinesState() const; // Qt::CheckState as int void setNoDrawLines(bool val); + bool getHasCustomColor() const; + int getHasCustomColorState() const; // Qt::CheckState as int void setHasCustomColor(bool val); + int getColorR() const; int getColorG() const; int getColorB() const; @@ -28,35 +35,45 @@ class WaypointEditorDialogModel: public AbstractDialogModel { void setColorG(int g); void setColorB(int b); - bool isEnabled() const; - const SCP_vector>& getWaypointPathList() const; + bool isColorRMixed() const; + bool isColorGMixed() const; + bool isColorBMixed() const; + bool hasAnyColorMixed() const; SCP_string getLayer() const; void setLayer(const SCP_string& layer); + void selectNextPath(); + void selectPreviousPath(); + signals: void waypointPathMarkingChanged(); - private slots: void onSelectedObjectChanged(int); void onSelectedObjectMarkingChanged(int, bool); void onMissionChanged(); - private: // NOLINT(readability-redundant-access-specifiers) +private: // NOLINT(readability-redundant-access-specifiers) void initializeData(); - void updateWaypointPathList(); - bool validateData(); void showErrorDialogNoCancel(const SCP_string& message); + bool validateName(const SCP_string& name); + void selectWaypointPathByIndex(int idx); + SCP_vector _selectedWaypointPaths; // indices into Waypoint_lists SCP_string _currentName; - int _currentWaypointPathSelected = -1; - bool _enabled = false; - SCP_vector> _waypointPathList; bool _bypass_errors = false; bool _noDrawLines = false; bool _hasCustomColor = false; int _colorR = 255, _colorG = 255, _colorB = 255; + + bool _noDrawLinesMixed = false; + bool _hasCustomColorMixed = false; + bool _redMixed = false, _greenMixed = false, _blueMixed = false; + + // Guards against re-entry into initializeData() from selection/marking/mission signals + // while we're already mutating mission state (e.g., setLayer fans out unmarks). + bool _suppressRefresh = false; }; } // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index b4b190355e9..03c02c2a4dc 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -1061,7 +1061,7 @@ void FredView::onUpdateContextToolbar() { }); _contextToolBar->addAction(selWingAct); } - } else if (effectiveType == OBJ_WAYPOINT && (numMarked <= 1 || multiSharedWaypointList != nullptr)) { + } else if (effectiveType == OBJ_WAYPOINT) { addBtn(tr("Edit Waypoint Path"), &FredView::on_actionWaypoint_Paths_triggered); } else if (numMarked <= 1 && effectiveType == OBJ_JUMP_NODE) { addBtn(tr("Edit Jump Node"), &FredView::on_actionJump_Nodes_triggered); diff --git a/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp b/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp index 3eaa0aa06f5..441496dc8f4 100644 --- a/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp @@ -20,6 +20,12 @@ WaypointEditorDialog::WaypointEditorDialog(FredView* parent, EditorViewport* vie ui->nameEdit->setMaxLength(NAME_LENGTH - 1); + // -1 is the "mixed selection" sentinel; shown as blank via specialValueText. + for (auto* sb : {ui->colorRSpinBox, ui->colorGSpinBox, ui->colorBSpinBox}) { + sb->setMinimum(-1); + sb->setSpecialValueText(" "); + } + initializeUi(); updateUi(); @@ -38,45 +44,49 @@ void WaypointEditorDialog::initializeUi() { util::SignalBlockers blockers(this); - updateWaypointListComboBox(); - ui->layerCombo->clear(); for (const auto& name : _viewport->getLayerNames()) { ui->layerCombo->addItem(QString::fromStdString(name), QString::fromStdString(name)); } - bool enabled = _model->isEnabled(); - ui->nameEdit->setEnabled(enabled); + const bool enabled = _model->hasValidSelection(); + const bool hasAny = _model->hasAnyPathsInMission(); + const bool multiSelect = _model->hasMultipleSelection(); + + ui->nameEdit->setEnabled(enabled && !multiSelect); ui->noDrawLinesCheck->setEnabled(enabled); ui->customColorCheck->setEnabled(enabled); ui->layerCombo->setEnabled(enabled); -} + ui->prevPathButton->setEnabled(hasAny); + ui->nextPathButton->setEnabled(hasAny); -void WaypointEditorDialog::updateWaypointListComboBox() -{ - ui->pathSelection->clear(); - - for (auto& wp : _model->getWaypointPathList()) { - ui->pathSelection->addItem(QString::fromStdString(wp.first), wp.second); + if (multiSelect) { + setWindowTitle(QString("Edit %1 Waypoint Paths").arg(_model->getSelectionCount())); + } else { + setWindowTitle("Waypoint Path Editor"); } - - ui->pathSelection->setEnabled(!_model->getWaypointPathList().empty()); } void WaypointEditorDialog::updateUi() { util::SignalBlockers blockers(this); ui->nameEdit->setText(QString::fromStdString(_model->getCurrentName())); - ui->pathSelection->setCurrentIndex(ui->pathSelection->findData(_model->getCurrentlySelectedPath())); ui->layerCombo->setCurrentIndex(ui->layerCombo->findData(QString::fromStdString(_model->getLayer()))); - ui->noDrawLinesCheck->setChecked(_model->getNoDrawLines()); - ui->customColorCheck->setChecked(_model->getHasCustomColor()); - ui->colorRSpinBox->setValue(_model->getColorR()); - ui->colorGSpinBox->setValue(_model->getColorG()); - ui->colorBSpinBox->setValue(_model->getColorB()); + const int noDrawState = _model->getNoDrawLinesState(); + ui->noDrawLinesCheck->setTristate(noDrawState == Qt::PartiallyChecked); + ui->noDrawLinesCheck->setCheckState(static_cast(noDrawState)); + + const int customColorState = _model->getHasCustomColorState(); + ui->customColorCheck->setTristate(customColorState == Qt::PartiallyChecked); + ui->customColorCheck->setCheckState(static_cast(customColorState)); - bool colorEnabled = _model->isEnabled() && _model->getHasCustomColor(); + ui->colorRSpinBox->setValue(_model->isColorRMixed() ? -1 : _model->getColorR()); + ui->colorGSpinBox->setValue(_model->isColorGMixed() ? -1 : _model->getColorG()); + ui->colorBSpinBox->setValue(_model->isColorBMixed() ? -1 : _model->getColorB()); + + const bool customResolved = customColorState == Qt::Checked; + const bool colorEnabled = _model->hasValidSelection() && customResolved; ui->colorRSpinBox->setEnabled(colorEnabled); ui->colorGSpinBox->setEnabled(colorEnabled); ui->colorBSpinBox->setEnabled(colorEnabled); @@ -86,6 +96,14 @@ void WaypointEditorDialog::updateUi() void WaypointEditorDialog::updateColorSwatch() { + if (_model->hasAnyColorMixed()) { + ui->colorSwatch->setText("?"); + ui->colorSwatch->setAlignment(Qt::AlignCenter); + ui->colorSwatch->setStyleSheet("background: #888; color: white;" + "border: 1px solid #444; border-radius: 3px;"); + return; + } + ui->colorSwatch->setText(""); ui->colorSwatch->setStyleSheet(QString("background: rgb(%1,%2,%3);" "border: 1px solid #444; border-radius: 3px;") .arg(_model->getColorR()) @@ -93,70 +111,54 @@ void WaypointEditorDialog::updateColorSwatch() .arg(_model->getColorB())); } -void WaypointEditorDialog::on_pathSelection_currentIndexChanged(int index) +void WaypointEditorDialog::on_prevPathButton_clicked() { - auto itemId = ui->pathSelection->itemData(index).value(); - _model->setCurrentlySelectedPath(itemId); + _model->selectPreviousPath(); } -// This will run any time an edit is finished which includes the entire window closing, losing focus, -// the user clicking elsewhere in the dialog, or pressing Enter in the edit box. -// This is ok here because this is literally the only field that can be edited but if this dialog -// ever expands then it would be wise to change the whole thing to an ok/cancel type dialog. -void WaypointEditorDialog::on_nameEdit_editingFinished() +void WaypointEditorDialog::on_nextPathButton_clicked() { - // Waypoint editor applies immediately when the name is changed - // so save the current, try to apply, if fails, restore the current - // and update the text in the edit box - - SCP_string current = _model->getCurrentName(); - - SCP_string newText = ui->nameEdit->text().toUtf8().constData(); - _model->setCurrentName(newText); + _model->selectNextPath(); +} - if (!_model->apply()) { +void WaypointEditorDialog::on_nameEdit_editingFinished() +{ + if (!_model->setCurrentName(ui->nameEdit->text().toUtf8().constData())) { util::SignalBlockers blockers(this); - // If apply failed, restore the old name - ui->nameEdit->setText(QString::fromStdString(current)); - _model->setCurrentName(current); // Restore the model's current name + ui->nameEdit->setText(QString::fromStdString(_model->getCurrentName())); } } -void WaypointEditorDialog::on_noDrawLinesCheck_toggled(bool checked) +void WaypointEditorDialog::on_noDrawLinesCheck_clicked() { - _model->setNoDrawLines(checked); - _model->apply(); + // clicked() (not toggled()) so a click on a tri-state PartiallyChecked box still routes here. + _model->setNoDrawLines(ui->noDrawLinesCheck->isChecked()); + // User has resolved any partial state; refresh so tristate(true) is cleared. + updateUi(); } -void WaypointEditorDialog::on_customColorCheck_toggled(bool checked) +void WaypointEditorDialog::on_customColorCheck_clicked() { - _model->setHasCustomColor(checked); - ui->colorRSpinBox->setEnabled(checked); - ui->colorGSpinBox->setEnabled(checked); - ui->colorBSpinBox->setEnabled(checked); - updateColorSwatch(); - _model->apply(); + _model->setHasCustomColor(ui->customColorCheck->isChecked()); + updateUi(); } void WaypointEditorDialog::on_colorRSpinBox_valueChanged(int value) { _model->setColorR(value); updateColorSwatch(); - _model->apply(); } void WaypointEditorDialog::on_colorGSpinBox_valueChanged(int value) { _model->setColorG(value); updateColorSwatch(); - _model->apply(); } void WaypointEditorDialog::on_colorBSpinBox_valueChanged(int value) { _model->setColorB(value); updateColorSwatch(); - _model->apply(); } void WaypointEditorDialog::on_layerCombo_currentIndexChanged(int index) diff --git a/qtfred/src/ui/dialogs/WaypointEditorDialog.h b/qtfred/src/ui/dialogs/WaypointEditorDialog.h index d7d9f9b49f9..d5241fdb282 100644 --- a/qtfred/src/ui/dialogs/WaypointEditorDialog.h +++ b/qtfred/src/ui/dialogs/WaypointEditorDialog.h @@ -16,10 +16,11 @@ class WaypointEditorDialog : public QDialog { ~WaypointEditorDialog() override; private slots: - void on_pathSelection_currentIndexChanged(int index); + void on_prevPathButton_clicked(); + void on_nextPathButton_clicked(); void on_nameEdit_editingFinished(); - void on_noDrawLinesCheck_toggled(bool checked); - void on_customColorCheck_toggled(bool checked); + void on_noDrawLinesCheck_clicked(); + void on_customColorCheck_clicked(); void on_colorRSpinBox_valueChanged(int value); void on_colorGSpinBox_valueChanged(int value); void on_colorBSpinBox_valueChanged(int value); @@ -31,10 +32,8 @@ private slots: std::unique_ptr _model; void initializeUi(); - void updateWaypointListComboBox(); void updateUi(); void updateColorSwatch(); }; } // namespace fso::fred::dialogs - diff --git a/qtfred/ui/WaypointEditorDialog.ui b/qtfred/ui/WaypointEditorDialog.ui index 703e8663d04..6476e65adae 100644 --- a/qtfred/ui/WaypointEditorDialog.ui +++ b/qtfred/ui/WaypointEditorDialog.ui @@ -24,25 +24,23 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - Wa&ypoint Path - - - pathSelection - - - - - - - - 0 - 0 - - - + + + + + + &Prev + + + + + + + &Next + + + + @@ -60,6 +58,23 @@ + + + + Layer + + + + + + + + 0 + 0 + + + + @@ -136,23 +151,6 @@ - - - - Layer - - - - - - - - 0 - 0 - - - - From 9b57cc7a263b621a12cb0d33570b4333a81ac336 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 13 May 2026 06:40:54 -0500 Subject: [PATCH 39/65] wing editor stylization pass (#7433) --- .../mission/dialogs/WingEditorDialogModel.cpp | 48 ++-- .../mission/dialogs/WingEditorDialogModel.h | 246 +++++++++--------- qtfred/src/ui/dialogs/WingEditorDialog.cpp | 14 +- qtfred/src/ui/dialogs/WingEditorDialog.h | 168 ++++++------ 4 files changed, 235 insertions(+), 241 deletions(-) diff --git a/qtfred/src/mission/dialogs/WingEditorDialogModel.cpp b/qtfred/src/mission/dialogs/WingEditorDialogModel.cpp index 459c9d46134..80d29e2f799 100644 --- a/qtfred/src/mission/dialogs/WingEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/WingEditorDialogModel.cpp @@ -70,9 +70,9 @@ wing* WingEditorDialogModel::getCurrentWing() const return &Wings[_currentWingIndex]; } -std::vector> WingEditorDialogModel::getDockBayPathsForWingMask(uint32_t mask, int anchorShipnum) +SCP_vector> WingEditorDialogModel::getDockBayPathsForWingMask(uint32_t mask, int anchorShipnum) { - std::vector> out; + SCP_vector> out; if (anchorShipnum < 0 || !ship_has_hangar_bay(anchorShipnum)) return out; @@ -104,7 +104,7 @@ void WingEditorDialogModel::prepareSquadLogoList() pilot_load_squad_pic_list(); for (int i = 0; i < Num_pilot_squad_images; i++) { - squadLogoList.emplace_back(Pilot_squad_image_names[i]); + _squadLogoList.emplace_back(Pilot_squad_image_names[i]); } } @@ -277,9 +277,9 @@ std::pair> WingEditorDialogModel::getLeaderList() co return items; } -std::vector> WingEditorDialogModel::getHotkeyList() +SCP_vector> WingEditorDialogModel::getHotkeyList() { - std::vector> items; + SCP_vector> items; items.emplace_back(-1, "None"); for (int i = 0; i < MAX_KEYED_TARGETS; ++i) { @@ -293,9 +293,9 @@ std::vector> WingEditorDialogModel::getHotkeyList() return items; } -std::vector> WingEditorDialogModel::getFormationList() +SCP_vector> WingEditorDialogModel::getFormationList() { - std::vector> items; + SCP_vector> items; items.emplace_back(-1, "Default"); for (int i = 0; i < static_cast(Wing_formations.size()); i++) { @@ -305,9 +305,9 @@ std::vector> WingEditorDialogModel::getFormationList return items; } -std::vector> WingEditorDialogModel::getArrivalLocationList() +SCP_vector> WingEditorDialogModel::getArrivalLocationList() { - std::vector> items; + SCP_vector> items; items.reserve(MAX_ARRIVAL_NAMES); for (int i = 0; i < MAX_ARRIVAL_NAMES; i++) { items.emplace_back(i, Arrival_location_names[i]); @@ -315,9 +315,9 @@ std::vector> WingEditorDialogModel::getArrivalLocati return items; } -std::vector> WingEditorDialogModel::getDepartureLocationList() +SCP_vector> WingEditorDialogModel::getDepartureLocationList() { - std::vector> items; + SCP_vector> items; items.reserve(MAX_DEPARTURE_NAMES); for (int i = 0; i < MAX_DEPARTURE_NAMES; i++) { items.emplace_back(i, Departure_location_names[i]); @@ -336,9 +336,9 @@ static bool shipHasDockBay(int ship_info_index) return pm && pm->ship_bay && pm->ship_bay->num_paths > 0; } -std::vector> WingEditorDialogModel::getArrivalTargetList() const +SCP_vector> WingEditorDialogModel::getArrivalTargetList() const { - std::vector> items; + SCP_vector> items; const auto* w = getCurrentWing(); if (!w) return items; @@ -378,9 +378,9 @@ std::vector> WingEditorDialogModel::getArrivalTarget return items; } -std::vector> WingEditorDialogModel::getDepartureTargetList() const +SCP_vector> WingEditorDialogModel::getDepartureTargetList() const { - std::vector> items; + SCP_vector> items; const auto* w = getCurrentWing(); if (!w) return items; @@ -688,9 +688,9 @@ void WingEditorDialogModel::disbandCurrentWing() reloadFromCurWing(); } -std::vector> WingEditorDialogModel::getWingFlags() const +SCP_vector> WingEditorDialogModel::getWingFlags() const { - std::vector> flags; + SCP_vector> flags; if (!wingIsValid()) return flags; @@ -713,10 +713,10 @@ std::vector> WingEditorDialogModel::getWingFlags() c return flags; } -std::vector> WingEditorDialogModel::getWingFlagDescriptions() +SCP_vector> WingEditorDialogModel::getWingFlagDescriptions() { const size_t num_descs = Num_parse_wing_flag_descriptions; - std::vector> descriptions; + SCP_vector> descriptions; descriptions.reserve(Num_parse_wing_flags); for (size_t i = 0; i < Num_parse_wing_flags; ++i) { const auto& flagDef = Parse_wing_flags[i]; @@ -736,7 +736,7 @@ std::vector> WingEditorDialogModel::getWingFla return descriptions; } -void WingEditorDialogModel::setWingFlags(const std::vector>& newFlags) +void WingEditorDialogModel::setWingFlags(const SCP_vector>& newFlags) { if (!wingIsValid()) return; @@ -980,7 +980,7 @@ void WingEditorDialogModel::setArrivalDistance(int newDistance) modify(w->arrival_distance, newDistance); } -std::vector> WingEditorDialogModel::getArrivalPaths() const +SCP_vector> WingEditorDialogModel::getArrivalPaths() const { if (!wingIsValid()) return {}; @@ -992,7 +992,7 @@ std::vector> WingEditorDialogModel::getArrivalPaths( return getDockBayPathsForWingMask(w->arrival_path_mask, anchor_to_target(w->arrival_anchor)); } -void WingEditorDialogModel::setArrivalPaths(const std::vector>& chosen) +void WingEditorDialogModel::setArrivalPaths(const SCP_vector>& chosen) { if (!wingIsValid()) return; @@ -1220,7 +1220,7 @@ void WingEditorDialogModel::setDepartureTarget(int targetIndex) modify(w->departure_path_mask, 0); } -std::vector> WingEditorDialogModel::getDeparturePaths() const +SCP_vector> WingEditorDialogModel::getDeparturePaths() const { if (!wingIsValid()) return {}; @@ -1232,7 +1232,7 @@ std::vector> WingEditorDialogModel::getDeparturePath return getDockBayPathsForWingMask(w->departure_path_mask, anchor_to_target(w->departure_anchor)); } -void WingEditorDialogModel::setDeparturePaths(const std::vector>& chosen) +void WingEditorDialogModel::setDeparturePaths(const SCP_vector>& chosen) { if (!wingIsValid()) return; diff --git a/qtfred/src/mission/dialogs/WingEditorDialogModel.h b/qtfred/src/mission/dialogs/WingEditorDialogModel.h index b25195be769..b34f4d36c1f 100644 --- a/qtfred/src/mission/dialogs/WingEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/WingEditorDialogModel.h @@ -18,130 +18,126 @@ namespace fso::fred::dialogs { //TODO: This dialog currently works on the wing data directly instead of model members // so it does not support temporary changes. This will need to be changed in a future PR -/** - * @brief QTFred's Wing Editor's Model - */ class WingEditorDialogModel : public AbstractDialogModel { - Q_OBJECT - - public: - WingEditorDialogModel(QObject* parent, EditorViewport* viewport); - - // The model in this dialog directly applies changes to the mission, so apply and reject are superfluous - bool apply() override { return true; } - void reject() override {} - - int getCurrentWingIndex() const { return _currentWingIndex; }; - - bool wingIsValid() const; - - bool isPlayerWing() const; - bool containsPlayerStart() const; - bool wingAllFighterBombers() const; - - bool arrivalIsDockBay() const; - bool arrivalNeedsTarget() const; - bool departureIsDockBay() const; - bool departureNeedsTarget() const; - int getMaxWaveThreshold() const; - int getMinArrivalDistance() const; - - std::pair> getLeaderList() const; - static std::vector> getHotkeyList(); - static std::vector> getFormationList(); - static std::vector> getArrivalLocationList(); - static std::vector> getDepartureLocationList(); - std::vector> getArrivalTargetList() const; - std::vector> getDepartureTargetList() const; - std::vector getSquadLogoList() const { return squadLogoList; }; - - // Top section, first column - SCP_string getWingName() const; - void setWingName(const SCP_string& name); - int getWingLeaderIndex() const; - void setWingLeaderIndex(int newLeaderIndex); - int getNumberOfWaves() const; - void setNumberOfWaves(int newTotalWaves); - int getWaveThreshold() const; - void setWaveThreshold(int newThreshhold); - int getHotkey() const; - void setHotkey(int newHotkeyIndex); - - // Top section, second column - int getFormationId() const; - void setFormationId(int newFormationId); - float getFormationScale() const; - void setFormationScale(float newScale); - void alignWingFormation(); - SCP_string getSquadLogo() const; - void setSquadLogo(const SCP_string& filename); - - // Top section, third column - void selectPreviousWing(); - void selectNextWing(); - void deleteCurrentWing(); - void disbandCurrentWing(); - // Initial orders is handled by its own dialog, so no model function here - std::vector> getWingFlags() const; - void setWingFlags(const std::vector>& newFlags); - static std::vector> getWingFlagDescriptions(); - - - // Arrival controls - ArrivalLocation getArrivalType() const; - void setArrivalType(ArrivalLocation arrivalType); - int getArrivalDelay() const; - void setArrivalDelay(int delayIn); - int getMinWaveDelay() const; - void setMinWaveDelay(int newMin); - int getMaxWaveDelay() const; - void setMaxWaveDelay(int newMax); - int getArrivalTarget() const; - void setArrivalTarget(int targetIndex); - int getArrivalDistance() const; - void setArrivalDistance(int newDistance); - std::vector> getArrivalPaths() const; - void setArrivalPaths(const std::vector>& newFlags); - int getArrivalTree() const; - void setArrivalTree(int newTree); - bool getNoArrivalWarpFlag() const; - void setNoArrivalWarpFlag(bool flagIn); - bool getNoArrivalWarpAdjustFlag() const; - void setNoArrivalWarpAdjustFlag(bool flagIn); - - // Departure controls - DepartureLocation getDepartureType() const; - void setDepartureType(DepartureLocation departureType); - int getDepartureDelay() const; - void setDepartureDelay(int delayIn); - int getDepartureTarget() const; - void setDepartureTarget(int targetIndex); - std::vector> getDeparturePaths() const; - void setDeparturePaths(const std::vector>& newFlags); - int getDepartureTree() const; - void setDepartureTree(int newTree); - bool getNoDepartureWarpFlag() const; - void setNoDepartureWarpFlag(bool flagIn); - bool getNoDepartureWarpAdjustFlag() const; - void setNoDepartureWarpAdjustFlag(bool flagIn); - - signals: - void wingChanged(); - - private slots: - void onEditorSelectionChanged(int); // currentObjectChanged - void onEditorMissionChanged(); // missionChanged - - private: // NOLINT(readability-redundant-access-specifiers) - void initializeData(); - void reloadFromCurWing(); - wing* getCurrentWing() const; - static std::vector> getDockBayPathsForWingMask(uint32_t mask, int anchorShipnum); - void prepareSquadLogoList(); - - int _currentWingIndex = -1; - SCP_string _currentWingName; - - SCP_vector squadLogoList; + Q_OBJECT + + public: + WingEditorDialogModel(QObject* parent, EditorViewport* viewport); + + // The model in this dialog directly applies changes to the mission, so apply and reject are superfluous + bool apply() override { return true; } + void reject() override {} + + int getCurrentWingIndex() const { return _currentWingIndex; }; + + bool wingIsValid() const; + + bool isPlayerWing() const; + bool containsPlayerStart() const; + bool wingAllFighterBombers() const; + + bool arrivalIsDockBay() const; + bool arrivalNeedsTarget() const; + bool departureIsDockBay() const; + bool departureNeedsTarget() const; + int getMaxWaveThreshold() const; + int getMinArrivalDistance() const; + + std::pair> getLeaderList() const; + static SCP_vector> getHotkeyList(); + static SCP_vector> getFormationList(); + static SCP_vector> getArrivalLocationList(); + static SCP_vector> getDepartureLocationList(); + SCP_vector> getArrivalTargetList() const; + SCP_vector> getDepartureTargetList() const; + SCP_vector getSquadLogoList() const { return _squadLogoList; }; + + // Top section, first column + SCP_string getWingName() const; + void setWingName(const SCP_string& name); + int getWingLeaderIndex() const; + void setWingLeaderIndex(int newLeaderIndex); + int getNumberOfWaves() const; + void setNumberOfWaves(int newTotalWaves); + int getWaveThreshold() const; + void setWaveThreshold(int newThreshhold); + int getHotkey() const; + void setHotkey(int newHotkeyIndex); + + // Top section, second column + int getFormationId() const; + void setFormationId(int newFormationId); + float getFormationScale() const; + void setFormationScale(float newScale); + void alignWingFormation(); + SCP_string getSquadLogo() const; + void setSquadLogo(const SCP_string& filename); + + // Top section, third column + void selectPreviousWing(); + void selectNextWing(); + void deleteCurrentWing(); + void disbandCurrentWing(); + // Initial orders is handled by its own dialog, so no model function here + SCP_vector> getWingFlags() const; + void setWingFlags(const SCP_vector>& newFlags); + static SCP_vector> getWingFlagDescriptions(); + + // Arrival controls + ArrivalLocation getArrivalType() const; + void setArrivalType(ArrivalLocation arrivalType); + int getArrivalDelay() const; + void setArrivalDelay(int delayIn); + int getMinWaveDelay() const; + void setMinWaveDelay(int newMin); + int getMaxWaveDelay() const; + void setMaxWaveDelay(int newMax); + int getArrivalTarget() const; + void setArrivalTarget(int targetIndex); + int getArrivalDistance() const; + void setArrivalDistance(int newDistance); + SCP_vector> getArrivalPaths() const; + void setArrivalPaths(const SCP_vector>& newFlags); + int getArrivalTree() const; + void setArrivalTree(int newTree); + bool getNoArrivalWarpFlag() const; + void setNoArrivalWarpFlag(bool flagIn); + bool getNoArrivalWarpAdjustFlag() const; + void setNoArrivalWarpAdjustFlag(bool flagIn); + + // Departure controls + DepartureLocation getDepartureType() const; + void setDepartureType(DepartureLocation departureType); + int getDepartureDelay() const; + void setDepartureDelay(int delayIn); + int getDepartureTarget() const; + void setDepartureTarget(int targetIndex); + SCP_vector> getDeparturePaths() const; + void setDeparturePaths(const SCP_vector>& newFlags); + int getDepartureTree() const; + void setDepartureTree(int newTree); + bool getNoDepartureWarpFlag() const; + void setNoDepartureWarpFlag(bool flagIn); + bool getNoDepartureWarpAdjustFlag() const; + void setNoDepartureWarpAdjustFlag(bool flagIn); + + signals: + void wingChanged(); + + private slots: + void onEditorSelectionChanged(int); // currentObjectChanged + void onEditorMissionChanged(); // missionChanged + + private: // NOLINT(readability-redundant-access-specifiers) + void initializeData(); + void reloadFromCurWing(); + wing* getCurrentWing() const; + static SCP_vector> getDockBayPathsForWingMask(uint32_t mask, int anchorShipnum); + void prepareSquadLogoList(); + + int _currentWingIndex = -1; + SCP_string _currentWingName; + + SCP_vector _squadLogoList; }; -} // namespace fso::fred::dialogs \ No newline at end of file +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/WingEditorDialog.cpp b/qtfred/src/ui/dialogs/WingEditorDialog.cpp index 060bd86def1..c831c755f41 100644 --- a/qtfred/src/ui/dialogs/WingEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/WingEditorDialog.cpp @@ -420,7 +420,7 @@ void WingEditorDialog::on_setSquadLogoButton_clicked() if (dlg.exec() != QDialog::Accepted) return; - const std::string chosen = dlg.selectedFile().toUtf8().constData(); + const SCP_string chosen = dlg.selectedFile().toUtf8().constData(); _model->setSquadLogo(chosen); updateLogoPreview(); } @@ -490,7 +490,7 @@ void WingEditorDialog::on_wingFlagsButton_clicked() dlg.setOptionDescriptions(qtDescs); if (dlg.exec() == QDialog::Accepted) { - std::vector> result; + SCP_vector> result; for (const auto& f : dlg.getFlags()) result.emplace_back(f.first.toUtf8().constData(), f.second == Qt::Checked); _model->setWingFlags(result); @@ -554,11 +554,10 @@ void WingEditorDialog::on_restrictArrivalPathsButton_clicked() if (dlg.exec() == QDialog::Accepted) { auto returned_values = dlg.getCheckedStates(); - std::vector> updatedFlags; + SCP_vector> updatedFlags; for (int i = 0; i < checkbox_list.size(); ++i) { - // Convert back to std::string - std::string name = checkbox_list[i].first.toUtf8().constData(); + SCP_string name = checkbox_list[i].first.toUtf8().constData(); updatedFlags.emplace_back(name, returned_values[i]); } @@ -632,11 +631,10 @@ void WingEditorDialog::on_restrictDeparturePathsButton_clicked() if (dlg.exec() == QDialog::Accepted) { auto returned_values = dlg.getCheckedStates(); - std::vector> updatedFlags; + SCP_vector> updatedFlags; for (int i = 0; i < checkbox_list.size(); ++i) { - // Convert back to std::string - std::string name = checkbox_list[i].first.toUtf8().constData(); + SCP_string name = checkbox_list[i].first.toUtf8().constData(); updatedFlags.emplace_back(name, returned_values[i]); } diff --git a/qtfred/src/ui/dialogs/WingEditorDialog.h b/qtfred/src/ui/dialogs/WingEditorDialog.h index 14a8a9527f2..fe7735d5a12 100644 --- a/qtfred/src/ui/dialogs/WingEditorDialog.h +++ b/qtfred/src/ui/dialogs/WingEditorDialog.h @@ -14,90 +14,90 @@ class WingEditorDialog; class WingEditorDialog : public QDialog, public SexpTreeEditorInterface { Q_OBJECT - public: - explicit WingEditorDialog(FredView* parent, EditorViewport* viewport); - ~WingEditorDialog() override; - - private slots: - void on_hideCuesButton_clicked(); - - // Top section, first column - void on_wingNameEdit_editingFinished(); - void on_wingLeaderCombo_currentIndexChanged(int index); - void on_numWavesSpinBox_valueChanged(int value); - void on_waveThresholdSpinBox_valueChanged(int value); - void on_hotkeyCombo_currentIndexChanged(int /*index*/); - - // Top section, second column - void on_formationCombo_currentIndexChanged(int /*index*/); - void on_formationScaleSpinBox_valueChanged(double value); - void on_alignFormationButton_clicked(); - void on_setSquadLogoButton_clicked(); - - // Top section, third column - void on_prevWingButton_clicked(); - void on_nextWingButton_clicked(); - void on_deleteWingButton_clicked(); - void on_disbandWingButton_clicked(); - void on_initialOrdersButton_clicked(); - void on_wingFlagsButton_clicked(); - - // Arrival controls - void on_arrivalLocationCombo_currentIndexChanged(int /*index*/); - void on_arrivalDelaySpinBox_valueChanged(int value); - void on_minDelaySpinBox_valueChanged(int value); - void on_maxDelaySpinBox_valueChanged(int value); - void on_arrivalTargetCombo_currentIndexChanged(int /*index*/); - void on_arrivalDistanceSpinBox_valueChanged(int value); - void on_restrictArrivalPathsButton_clicked(); - void on_customWarpinButton_clicked(); - void on_arrivalTree_nodeChanged(int newTree); - void on_noArrivalWarpCheckBox_toggled(bool checked); - void on_noArrivalWarpAdjustCheckbox_toggled(bool checked); - - // Departure controls - void on_departureLocationCombo_currentIndexChanged(int /*index*/); - void on_departureDelaySpinBox_valueChanged(int value); - void on_departureTargetCombo_currentIndexChanged(int /*index*/); - void on_restrictDeparturePathsButton_clicked(); - void on_customWarpoutButton_clicked(); - void on_departureTree_nodeChanged(int newTree); - void on_noDepartureWarpCheckBox_toggled(bool checked); - void on_noDepartureWarpAdjustCheckbox_toggled(bool checked); - - // Sexp help text - void on_arrivalTree_helpChanged(const QString& help); - void on_arrivalTree_miniHelpChanged(const QString& help); - void on_departureTree_helpChanged(const QString& help); - void on_departureTree_miniHelpChanged(const QString& help); - - protected: - void closeEvent(QCloseEvent* e) override; - - private: // NOLINT(readability-redundant-access-specifiers) - std::unique_ptr ui; - std::unique_ptr _model; - EditorViewport* _viewport; - - bool _cues_hidden = false; - - void updateUi(); - void enableOrDisableControls(); - - void clearArrivalFields(); - void clearDepartureFields(); - void clearGeneralFields(); - - void refreshLeaderCombo(); - void refreshHotkeyCombo(); - void refreshFormationCombo(); - void refreshArrivalLocationCombo(); - void refreshDepartureLocationCombo(); - void refreshArrivalTargetCombo(); - void refreshDepartureTargetCombo(); - void refreshAllDynamicCombos(); - - void updateLogoPreview(); + public: + explicit WingEditorDialog(FredView* parent, EditorViewport* viewport); + ~WingEditorDialog() override; + + private slots: + void on_hideCuesButton_clicked(); + + // Top section, first column + void on_wingNameEdit_editingFinished(); + void on_wingLeaderCombo_currentIndexChanged(int index); + void on_numWavesSpinBox_valueChanged(int value); + void on_waveThresholdSpinBox_valueChanged(int value); + void on_hotkeyCombo_currentIndexChanged(int /*index*/); + + // Top section, second column + void on_formationCombo_currentIndexChanged(int /*index*/); + void on_formationScaleSpinBox_valueChanged(double value); + void on_alignFormationButton_clicked(); + void on_setSquadLogoButton_clicked(); + + // Top section, third column + void on_prevWingButton_clicked(); + void on_nextWingButton_clicked(); + void on_deleteWingButton_clicked(); + void on_disbandWingButton_clicked(); + void on_initialOrdersButton_clicked(); + void on_wingFlagsButton_clicked(); + + // Arrival controls + void on_arrivalLocationCombo_currentIndexChanged(int /*index*/); + void on_arrivalDelaySpinBox_valueChanged(int value); + void on_minDelaySpinBox_valueChanged(int value); + void on_maxDelaySpinBox_valueChanged(int value); + void on_arrivalTargetCombo_currentIndexChanged(int /*index*/); + void on_arrivalDistanceSpinBox_valueChanged(int value); + void on_restrictArrivalPathsButton_clicked(); + void on_customWarpinButton_clicked(); + void on_arrivalTree_nodeChanged(int newTree); + void on_noArrivalWarpCheckBox_toggled(bool checked); + void on_noArrivalWarpAdjustCheckbox_toggled(bool checked); + + // Departure controls + void on_departureLocationCombo_currentIndexChanged(int /*index*/); + void on_departureDelaySpinBox_valueChanged(int value); + void on_departureTargetCombo_currentIndexChanged(int /*index*/); + void on_restrictDeparturePathsButton_clicked(); + void on_customWarpoutButton_clicked(); + void on_departureTree_nodeChanged(int newTree); + void on_noDepartureWarpCheckBox_toggled(bool checked); + void on_noDepartureWarpAdjustCheckbox_toggled(bool checked); + + // Sexp help text + void on_arrivalTree_helpChanged(const QString& help); + void on_arrivalTree_miniHelpChanged(const QString& help); + void on_departureTree_helpChanged(const QString& help); + void on_departureTree_miniHelpChanged(const QString& help); + + protected: + void closeEvent(QCloseEvent* e) override; + + private: // NOLINT(readability-redundant-access-specifiers) + std::unique_ptr ui; + std::unique_ptr _model; + EditorViewport* _viewport; + + bool _cues_hidden = false; + + void updateUi(); + void enableOrDisableControls(); + + void clearArrivalFields(); + void clearDepartureFields(); + void clearGeneralFields(); + + void refreshLeaderCombo(); + void refreshHotkeyCombo(); + void refreshFormationCombo(); + void refreshArrivalLocationCombo(); + void refreshDepartureLocationCombo(); + void refreshArrivalTargetCombo(); + void refreshDepartureTargetCombo(); + void refreshAllDynamicCombos(); + + void updateLogoPreview(); }; } // namespace fso::fred::dialogs From e6d8835b72846da954a6b5016af922a8cc3c6929 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 13 May 2026 06:41:03 -0500 Subject: [PATCH 40/65] Qtfred Ship editor stylization pass (#7434) * ship editor stylization pass * proper enable/disable fixes --- .../ShipEditor/ShipEditorDialogModel.cpp | 780 +++++++++--------- .../ShipEditor/ShipEditorDialogModel.h | 268 +++--- .../dialogs/ShipEditor/ShipEditorDialog.cpp | 42 +- .../ui/dialogs/ShipEditor/ShipEditorDialog.h | 30 +- 4 files changed, 519 insertions(+), 601 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp index ee598a313a9..9d75963081b 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp @@ -40,43 +40,43 @@ int ShipEditorDialogModel::tristate_set(int val, int cur_state) } int ShipEditorDialogModel::getSingleShip() const { - return single_ship; + return _singleShip; } bool ShipEditorDialogModel::getIfMultipleShips() const { - return multi_edit; + return _multiEdit; } int ShipEditorDialogModel::getNumSelectedPlayers() const { - return player_count; + return _playerCount; } int ShipEditorDialogModel::getNumUnmarkedPlayers() const { - return pship_count; + return _pshipCount; } bool ShipEditorDialogModel::getUIEnable() const { - return enable; + return _enable; } int ShipEditorDialogModel::getNumSelectedShips() const { - return ship_count; + return _shipCount; } int ShipEditorDialogModel::getUseCue() const { - return cue_init; + return _cueInit; } int ShipEditorDialogModel::getNumSelectedObjects() const { - return total_count; + return _totalCount; } int ShipEditorDialogModel::getNumValidPlayers() const { - return pvalid_count; + return _pvalidCount; } int ShipEditorDialogModel::getIfPlayerShip() const { - return player_ship; + return _playerShipIndex; } SCP_vector> ShipEditorDialogModel::getPlayerOrders() @@ -136,14 +136,14 @@ void ShipEditorDialogModel::applyPlayerOrders(const SCP_vectormissionChanged(); } -void ShipEditorDialogModel::setArrivalPaths(const std::vector>& newPaths) +void ShipEditorDialogModel::setArrivalPaths(const SCP_vector>& newPaths) { - arrivalPaths = newPaths; - if (!newPaths.empty() && !multi_edit && single_ship >= 0) { + _arrivalPaths = newPaths; + if (!newPaths.empty() && !_multiEdit && _singleShip >= 0) { int num_allowed = 0; int m_path_mask = 0; for (int i = 0; i < static_cast(newPaths.size()); i++) { @@ -155,16 +155,16 @@ void ShipEditorDialogModel::setArrivalPaths(const std::vector(newPaths.size())) { m_path_mask = 0; } - Ships[single_ship].arrival_path_mask = m_path_mask; - set_modified(); + Ships[_singleShip].arrival_path_mask = m_path_mask; + setModified(); _editor->missionChanged(); } } -void ShipEditorDialogModel::setDeparturePaths(const std::vector>& newPaths) +void ShipEditorDialogModel::setDeparturePaths(const SCP_vector>& newPaths) { - departurePaths = newPaths; - if (!newPaths.empty() && !multi_edit && single_ship >= 0) { + _departurePaths = newPaths; + if (!newPaths.empty() && !_multiEdit && _singleShip >= 0) { int num_allowed = 0; int m_path_mask = 0; for (int i = 0; i < static_cast(newPaths.size()); i++) { @@ -176,8 +176,8 @@ void ShipEditorDialogModel::setDeparturePaths(const std::vector(newPaths.size())) { m_path_mask = 0; } - Ships[single_ship].departure_path_mask = m_path_mask; - set_modified(); + Ships[_singleShip].departure_path_mask = m_path_mask; + setModified(); _editor->missionChanged(); } } @@ -186,30 +186,30 @@ void ShipEditorDialogModel::initializeData() int type, wing = -1; int cargo = 0, base_ship, base_player, pship = -1; int escort_count; - respawn_priority = 0; - texenable = true; - std::set current_orders; - pship_count = 0; // a total count of the player ships not marked - player_count = 0; - ship_count = 0; - cue_init = 0; - total_count = 0; - pvalid_count = 0; - player_ship = 0; - ship_orders.clear(); - arrivalPaths.clear(); - departurePaths.clear(); - _m_alt_name = ""; - _m_callsign = ""; + _respawnPriority = 0; + _texEnable = true; + SCP_set current_orders; + _pshipCount = 0; // a total count of the player ships not marked + _playerCount = 0; + _shipCount = 0; + _cueInit = 0; + _totalCount = 0; + _pvalidCount = 0; + _playerShipIndex = 0; + _shipOrders.clear(); + _arrivalPaths.clear(); + _departurePaths.clear(); + _altName = ""; + _callsign = ""; object* objp; if (The_mission.game_type & MISSION_TYPE_MULTI) { - mission_type = 0; // multi player mission + _missionType = 0; // multi player mission } else { - mission_type = 1; // non-multiplayer mission (implies single player mission I guess) + _missionType = 1; // non-multiplayer mission (implies single player mission I guess) } - ship_count = player_count = escort_count = pship_count = pvalid_count = 0; + _shipCount = _playerCount = escort_count = _pshipCount = _pvalidCount = 0; base_ship = base_player = -1; - enable = true; + _enable = true; objp = GET_FIRST(&obj_used_list); while (objp != END_OF_LIST(&obj_used_list)) { if ((objp->type == OBJ_SHIP) && (Ships[objp->instance].flags[Ship::Ship_Flags::Escort])) { @@ -217,30 +217,30 @@ void ShipEditorDialogModel::initializeData() } if (objp->type == OBJ_START) { - pship_count++; // count all player starts in mission + _pshipCount++; // count all player starts in mission } if (objp->flags[Object::Object_Flags::Marked]) { type = objp->type; - if ((type == OBJ_START) && !mission_type) { // in multiplayer missions, starts act like ships + if ((type == OBJ_START) && !_missionType) { // in multiplayer missions, starts act like ships type = OBJ_SHIP; } auto i = -1; if (type == OBJ_START) { - player_count++; - // if player_count is 1, base_player will be the one and only player + _playerCount++; + // if _playerCount is 1, base_player will be the one and only player i = base_player = objp->instance; } else if (type == OBJ_SHIP) { - ship_count++; - // if ship_count is 1, base_ship will be the one and only ship + _shipCount++; + // if _shipCount is 1, base_ship will be the one and only ship i = base_ship = objp->instance; } if (i >= 0) { if (Ship_info[Ships[i].ship_info_index].flags[Ship::Info_Flags::Player_ship]) { - pvalid_count++; + _pvalidCount++; } } } @@ -248,32 +248,32 @@ void ShipEditorDialogModel::initializeData() objp = GET_NEXT(objp); } - total_count = ship_count + player_count; // get total number of objects being edited. - multi_edit = (total_count > 1); + _totalCount = _shipCount + _playerCount; // get total number of objects being edited. + _multiEdit = (_totalCount > 1); - _m_arrival_tree_formula = _m_departure_tree_formula = -1; - _m_arrival_location = -1; - _m_arrival_dist = -1; - _m_arrival_target = -1; - _m_arrival_delay = -1; - _m_departure_location = -1; - _m_departure_target = -1; - _m_departure_delay = -1; + _arrivalTreeFormula = _departureTreeFormula = -1; + _arrivalLocation = -1; + _arrivalDist = -1; + _arrivalTarget = -1; + _arrivalDelay = -1; + _departureLocation = -1; + _departureTarget = -1; + _departureDelay = -1; - player_ship = single_ship = -1; + _playerShipIndex = _singleShip = -1; - ship_orders.clear(); // assume they are all the same type - if (ship_count) { - if (!multi_edit) { - Assert((ship_count == 1) && (base_ship >= 0)); // NOLINT - _m_ship_name = Ships[base_ship].ship_name; - _m_ship_display_name = Ships[base_ship].has_display_name() ? Ships[base_ship].get_display_name() : ""; + _shipOrders.clear(); // assume they are all the same type + if (_shipCount) { + if (!_multiEdit) { + Assert((_shipCount == 1) && (base_ship >= 0)); // NOLINT + _shipName = Ships[base_ship].ship_name; + _shipDisplayName = Ships[base_ship].has_display_name() ? Ships[base_ship].get_display_name() : ""; } else { - _m_ship_name = ""; - _m_ship_display_name = ""; + _shipName = ""; + _shipDisplayName = ""; } - _m_update_arrival = _m_update_departure = true; + _updateArrival = _updateDeparture = true; base_player = 0; objp = GET_FIRST(&obj_used_list); @@ -283,26 +283,26 @@ void ShipEditorDialogModel::initializeData() // do processing for both ships and players auto i = get_ship_from_obj(objp); if (base_player >= 0) { - _m_ship_class = Ships[i].ship_info_index; - _m_team = Ships[i].team; + _shipClass = Ships[i].ship_info_index; + _team = Ships[i].team; pship = (objp->type == OBJ_START) ? Qt::Checked : Qt::Unchecked; base_player = -1; } else { - if (Ships[i].ship_info_index != _m_ship_class) { - _m_ship_class = -1; - texenable = false; + if (Ships[i].ship_info_index != _shipClass) { + _shipClass = -1; + _texEnable = false; } - if (Ships[i].team != _m_team) { - _m_team = -1; + if (Ships[i].team != _team) { + _team = -1; } pship = tristate_set(Objects[Ships[i].objnum].type == OBJ_START, pship); } // 'and' in the ship type of this ship to our running bitfield current_orders = ship_get_default_orders_accepted(&Ship_info[Ships[i].ship_info_index]); - if (ship_orders.empty()) { - ship_orders = current_orders; - } else if (ship_orders != current_orders) { - ship_orders = {std::numeric_limits::max()}; + if (_shipOrders.empty()) { + _shipOrders = current_orders; + } else if (_shipOrders != current_orders) { + _shipOrders = {std::numeric_limits::max()}; } if (Ships[i].flags[Ship::Ship_Flags::Escort]) { @@ -310,124 +310,124 @@ void ShipEditorDialogModel::initializeData() } if (Objects[Ships[i].objnum].type == OBJ_START) { - pship_count--; // removed marked starts from count + _pshipCount--; // removed marked starts from count } - if ((objp->type == OBJ_SHIP) || ((objp->type == OBJ_START) && !mission_type)) { + if ((objp->type == OBJ_SHIP) || ((objp->type == OBJ_START) && !_missionType)) { // process this if ship not in a wing if (Ships[i].wingnum < 0) { - if (!cue_init) { - cue_init = 1; - _m_arrival_tree_formula = Ships[i].arrival_cue; - _m_departure_tree_formula = Ships[i].departure_cue; - _m_arrival_location = static_cast(Ships[i].arrival_location); - _m_arrival_dist = Ships[i].arrival_distance; - _m_arrival_target = anchor_to_target(Ships[i].arrival_anchor); - _m_arrival_delay = Ships[i].arrival_delay; - _m_departure_location = static_cast(Ships[i].departure_location); - _m_departure_delay = Ships[i].departure_delay; - _m_departure_target = anchor_to_target(Ships[i].departure_anchor); + if (!_cueInit) { + _cueInit = 1; + _arrivalTreeFormula = Ships[i].arrival_cue; + _departureTreeFormula = Ships[i].departure_cue; + _arrivalLocation = static_cast(Ships[i].arrival_location); + _arrivalDist = Ships[i].arrival_distance; + _arrivalTarget = anchor_to_target(Ships[i].arrival_anchor); + _arrivalDelay = Ships[i].arrival_delay; + _departureLocation = static_cast(Ships[i].departure_location); + _departureDelay = Ships[i].departure_delay; + _departureTarget = anchor_to_target(Ships[i].departure_anchor); } else { - cue_init++; - if (static_cast(Ships[i].arrival_location) != _m_arrival_location) { - _m_arrival_location = -1; + _cueInit++; + if (static_cast(Ships[i].arrival_location) != _arrivalLocation) { + _arrivalLocation = -1; } - if (static_cast(Ships[i].departure_location) != _m_departure_location) { - _m_departure_location = -1; + if (static_cast(Ships[i].departure_location) != _departureLocation) { + _departureLocation = -1; } - _m_arrival_dist = Ships[i].arrival_distance; - _m_arrival_delay = Ships[i].arrival_delay; - _m_departure_delay = Ships[i].departure_delay; + _arrivalDist = Ships[i].arrival_distance; + _arrivalDelay = Ships[i].arrival_delay; + _departureDelay = Ships[i].departure_delay; - if (Ships[i].arrival_anchor != target_to_anchor(_m_arrival_target)) { - _m_arrival_target = -1; + if (Ships[i].arrival_anchor != target_to_anchor(_arrivalTarget)) { + _arrivalTarget = -1; } - if (!cmp_sexp_chains(_m_arrival_tree_formula, Ships[i].arrival_cue)) { - _m_arrival_tree_formula = -1; - _m_update_arrival = false; + if (!cmp_sexp_chains(_arrivalTreeFormula, Ships[i].arrival_cue)) { + _arrivalTreeFormula = -1; + _updateArrival = false; } - if (!cmp_sexp_chains(_m_departure_tree_formula, Ships[i].departure_cue)) { - _m_departure_tree_formula = -1; - _m_update_departure = false; + if (!cmp_sexp_chains(_departureTreeFormula, Ships[i].departure_cue)) { + _departureTreeFormula = -1; + _updateDeparture = false; } - if (Ships[i].departure_anchor != target_to_anchor(_m_departure_target)) { - _m_departure_target = -1; + if (Ships[i].departure_anchor != target_to_anchor(_departureTarget)) { + _departureTarget = -1; } } } // process the first ship in group, else process the rest if (base_ship >= 0) { - _m_ai_class = Ships[i].weapons.ai_class; + _aiClass = Ships[i].weapons.ai_class; cargo = Ships[i].cargo1; - _m_cargo1 = Cargo_names[cargo]; - _m_cargo_title = Ships[i].cargo_title; - _m_hotkey = Ships[i].hotkey + 1; - _m_score = Ships[i].score; - _m_assist_score = static_cast(Ships[i].assist_score_pct * 100); - - _m_persona = Ships[i].persona_index; - _m_alt_name = Fred_alt_names[base_ship]; - _m_callsign = Fred_callsigns[base_ship]; + _cargo = Cargo_names[cargo]; + _cargoTitle = Ships[i].cargo_title; + _hotkey = Ships[i].hotkey + 1; + _score = Ships[i].score; + _assistScore = static_cast(Ships[i].assist_score_pct * 100); + + _persona = Ships[i].persona_index; + _altName = Fred_alt_names[base_ship]; + _callsign = Fred_callsigns[base_ship]; if (The_mission.game_type & MISSION_TYPE_MULTI) { - respawn_priority = Ships[i].respawn_priority; + _respawnPriority = Ships[i].respawn_priority; } // we use final_death_time member of ship structure for holding the amount of time before a // mission to destroy this ship wing = Ships[i].wingnum; if (wing < 0) { - m_wing = "None"; + _wing = "None"; } else { - m_wing = Wings[wing].name; + _wing = Wings[wing].name; if (!_editor->query_single_wing_marked()) - _m_update_arrival = _m_update_departure = false; + _updateArrival = _updateDeparture = false; } // set routine local varaiables for ship/object flags - _m_no_arrival_warp = (Ships[i].flags[Ship::Ship_Flags::No_arrival_warp]) ? 2 : 0; - _m_no_departure_warp = (Ships[i].flags[Ship::Ship_Flags::No_departure_warp]) ? 2 : 0; + _noArrivalWarp = (Ships[i].flags[Ship::Ship_Flags::No_arrival_warp]) ? 2 : 0; + _noDepartureWarp = (Ships[i].flags[Ship::Ship_Flags::No_departure_warp]) ? 2 : 0; base_ship = -1; - if (!multi_edit) - single_ship = i; + if (!_multiEdit) + _singleShip = i; } else { - if (Ships[i].weapons.ai_class != _m_ai_class) { - _m_ai_class = -1; + if (Ships[i].weapons.ai_class != _aiClass) { + _aiClass = -1; } if (Ships[i].cargo1 != cargo) { - _m_cargo1 = ""; + _cargo = ""; } - if (_m_cargo_title != Ships[i].cargo_title) { - _m_cargo_title = ""; + if (_cargoTitle != Ships[i].cargo_title) { + _cargoTitle = ""; } - _m_score = Ships[i].score; - _m_assist_score = static_cast(Ships[i].assist_score_pct * 100); + _score = Ships[i].score; + _assistScore = static_cast(Ships[i].assist_score_pct * 100); - if (Ships[i].hotkey != _m_hotkey - 1) { - _m_hotkey = -1; + if (Ships[i].hotkey != _hotkey - 1) { + _hotkey = -1; } - if (Ships[i].persona_index != _m_persona) { - _m_persona = -2; + if (Ships[i].persona_index != _persona) { + _persona = -2; } if (Ships[i].wingnum != wing) { - m_wing = ""; + _wing = ""; } - _m_no_arrival_warp = - tristate_set(Ships[i].flags[Ship::Ship_Flags::No_arrival_warp], _m_no_arrival_warp); - _m_no_departure_warp = - tristate_set(Ships[i].flags[Ship::Ship_Flags::No_departure_warp], _m_no_departure_warp); + _noArrivalWarp = + tristate_set(Ships[i].flags[Ship::Ship_Flags::No_arrival_warp], _noArrivalWarp); + _noDepartureWarp = + tristate_set(Ships[i].flags[Ship::Ship_Flags::No_departure_warp], _noDepartureWarp); } } } @@ -436,36 +436,36 @@ void ShipEditorDialogModel::initializeData() objp = GET_NEXT(objp); } - _m_player_ship = pship; + _isPlayerShip = pship; - if (_m_persona > 0) { + if (_persona > 0) { int persona_index = 0; - for (int i = 0; i < _m_persona; i++) { + for (int i = 0; i < _persona; i++) { if (Personas[i].flags & PERSONA_FLAG_WINGMAN) persona_index++; } - _m_persona = persona_index; + _persona = persona_index; } } else { // no ships selected, 0 or more player ships selected - if (player_count > 1) { // multiple player ships selected + if (_playerCount > 1) { // multiple player ships selected Assert(base_player >= 0); - _m_ship_name = ""; - _m_ship_display_name = ""; - _m_player_ship = true; + _shipName = ""; + _shipDisplayName = ""; + _isPlayerShip = true; objp = GET_FIRST(&obj_used_list); while (objp != END_OF_LIST(&obj_used_list)) { if ((objp->type == OBJ_START) && (objp->flags[Object::Object_Flags::Marked])) { auto i = objp->instance; if (base_player >= 0) { - _m_ship_class = Ships[i].ship_info_index; - _m_team = Ships[i].team; + _shipClass = Ships[i].ship_info_index; + _team = Ships[i].team; base_player = -1; } else { - if (Ships[i].ship_info_index != _m_ship_class) - _m_ship_class = -1; - if (Ships[i].team != _m_team) - _m_team = -1; + if (Ships[i].ship_info_index != _shipClass) + _shipClass = -1; + if (Ships[i].team != _team) + _team = -1; } } @@ -473,60 +473,60 @@ void ShipEditorDialogModel::initializeData() } // only 1 player selected.. } else if (query_valid_object(_editor->currentObject) && (Objects[_editor->currentObject].type == OBJ_START)) { - // Assert((player_count == 1) && !multi_edit); - player_ship = Objects[_editor->currentObject].instance; - _m_ship_name = Ships[player_ship].ship_name; - _m_ship_display_name = - Ships[player_ship].has_display_name() ? Ships[player_ship].get_display_name() : ""; - _m_ship_class = Ships[player_ship].ship_info_index; - _m_team = Ships[player_ship].team; - _m_player_ship = true; - _m_alt_name = Fred_alt_names[player_ship]; - _m_callsign = Fred_callsigns[player_ship]; + // Assert((_playerCount == 1) && !_multiEdit); + _playerShipIndex = Objects[_editor->currentObject].instance; + _shipName = Ships[_playerShipIndex].ship_name; + _shipDisplayName = + Ships[_playerShipIndex].has_display_name() ? Ships[_playerShipIndex].get_display_name() : ""; + _shipClass = Ships[_playerShipIndex].ship_info_index; + _team = Ships[_playerShipIndex].team; + _isPlayerShip = true; + _altName = Fred_alt_names[_playerShipIndex]; + _callsign = Fred_callsigns[_playerShipIndex]; } else { // no ships or players selected.. - _m_ship_name = ""; - _m_ship_display_name = ""; - _m_ship_class = -1; - _m_team = -1; - _m_persona = -1; - _m_player_ship = false; + _shipName = ""; + _shipDisplayName = ""; + _shipClass = -1; + _team = -1; + _persona = -1; + _isPlayerShip = false; } - _m_ai_class = -1; - _m_cargo1 = ""; - _m_cargo_title = ""; - _m_hotkey = 0; - _m_score = 0; // cause control to be blank - _m_assist_score = 0; - _m_arrival_location = -1; - _m_departure_location = -1; - _m_arrival_delay = 0; - _m_departure_delay = 0; - _m_arrival_dist = 0; - _m_arrival_target = -1; - _m_departure_target = -1; - _m_no_arrival_warp = false; - _m_no_departure_warp = false; - m_wing = "None"; - enable = false; - arrivalPaths.clear(); - departurePaths.clear(); + _aiClass = -1; + _cargo = ""; + _cargoTitle = ""; + _hotkey = 0; + _score = 0; // cause control to be blank + _assistScore = 0; + _arrivalLocation = -1; + _departureLocation = -1; + _arrivalDelay = 0; + _departureDelay = 0; + _arrivalDist = 0; + _arrivalTarget = -1; + _departureTarget = -1; + _noArrivalWarp = false; + _noDepartureWarp = false; + _wing = "None"; + _enable = false; + _arrivalPaths.clear(); + _departurePaths.clear(); } // Compute common layer across all marked ships/players { - _m_layer = ""; + _layer = ""; bool first = true; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && ptr->flags[Object::Object_Flags::Marked]) { SCP_string layer = _viewport->getObjectLayerName(OBJ_INDEX(ptr)); if (first) { - _m_layer = layer; + _layer = layer; first = false; - } else if (_m_layer != layer) { - _m_layer = ""; + } else if (_layer != layer) { + _layer = ""; break; } } @@ -537,17 +537,17 @@ void ShipEditorDialogModel::initializeData() _modified = false; } -std::vector> ShipEditorDialogModel::getArrivalPaths() const +SCP_vector> ShipEditorDialogModel::getArrivalPaths() const { - std::vector> m_path_list; - if (_m_arrival_target < 0 || _m_arrival_target >= MAX_SHIPS) + SCP_vector> m_path_list; + if (_arrivalTarget < 0 || _arrivalTarget >= MAX_SHIPS) return m_path_list; - int target_class = Ships[_m_arrival_target].ship_info_index; + int target_class = Ships[_arrivalTarget].ship_info_index; auto m_model = model_get(Ship_info[target_class].model_num); if (!m_model || !m_model->ship_bay || m_model->ship_bay->num_paths <= 0) return m_path_list; auto m_num_paths = m_model->ship_bay->num_paths; - auto m_path_mask = Ships[single_ship].arrival_path_mask; + auto m_path_mask = Ships[_singleShip].arrival_path_mask; for (int i = 0; i < m_num_paths; i++) { int path_id = m_model->ship_bay->path_indexes[i]; @@ -563,17 +563,17 @@ std::vector> ShipEditorDialogModel::getArrivalPaths( } return m_path_list; } -std::vector> ShipEditorDialogModel::getDeparturePaths() const +SCP_vector> ShipEditorDialogModel::getDeparturePaths() const { - std::vector> m_path_list; - if (_m_departure_target < 0 || _m_departure_target >= MAX_SHIPS) + SCP_vector> m_path_list; + if (_departureTarget < 0 || _departureTarget >= MAX_SHIPS) return m_path_list; - int target_class = Ships[_m_departure_target].ship_info_index; + int target_class = Ships[_departureTarget].ship_info_index; auto m_model = model_get(Ship_info[target_class].model_num); if (!m_model || !m_model->ship_bay || m_model->ship_bay->num_paths <= 0) return m_path_list; auto m_num_paths = m_model->ship_bay->num_paths; - auto m_path_mask = Ships[single_ship].departure_path_mask; + auto m_path_mask = Ships[_singleShip].departure_path_mask; for (int i = 0; i < m_num_paths; i++) { int path_id = m_model->ship_bay->path_indexes[i]; @@ -595,18 +595,18 @@ bool ShipEditorDialogModel::apply() } void ShipEditorDialogModel::reject() {} -void ShipEditorDialogModel::ship_alt_name_close(int ship) +void ShipEditorDialogModel::shipAltNameClose(int ship) { - if (multi_edit) { + if (_multiEdit) { return; } - drop_white_space(_m_alt_name); - if (_m_alt_name.empty()) { + drop_white_space(_altName); + if (_altName.empty()) { return; } - if (!_m_alt_name.empty() || !stricmp(_m_alt_name.c_str(), "")) { + if (!_altName.empty() || !stricmp(_altName.c_str(), "")) { if (*Fred_alt_names[ship]) { bool used = false; for (int i = 0; i < MAX_SHIPS; ++i) { @@ -623,13 +623,13 @@ void ShipEditorDialogModel::ship_alt_name_close(int ship) } } // otherwise see if it already exists - if (mission_parse_lookup_alt(_m_alt_name.c_str()) >= 0) { - strcpy_s(Fred_alt_names[ship], _m_alt_name.c_str()); + if (mission_parse_lookup_alt(_altName.c_str()) >= 0) { + strcpy_s(Fred_alt_names[ship], _altName.c_str()); return; } // otherwise try and add it - if (mission_parse_add_alt(_m_alt_name.c_str()) >= 0) { - strcpy_s(Fred_alt_names[ship], _m_alt_name.c_str()); + if (mission_parse_add_alt(_altName.c_str()) >= 0) { + strcpy_s(Fred_alt_names[ship], _altName.c_str()); return; } strcpy_s(Fred_alt_names[ship], ""); @@ -639,18 +639,18 @@ void ShipEditorDialogModel::ship_alt_name_close(int ship) {DialogButton::Ok}); } -void ShipEditorDialogModel::ship_callsign_close(int ship) +void ShipEditorDialogModel::shipCallsignClose(int ship) { - if (multi_edit) { + if (_multiEdit) { return; } - drop_white_space(_m_callsign); - if (_m_callsign.empty()) { + drop_white_space(_callsign); + if (_callsign.empty()) { return; } - if (!_m_callsign.empty() || !stricmp(_m_callsign.c_str(), "")) { + if (!_callsign.empty() || !stricmp(_callsign.c_str(), "")) { if (*Fred_callsigns[ship]) { bool used = false; for (int i = 0; i < MAX_SHIPS; ++i) { @@ -667,13 +667,13 @@ void ShipEditorDialogModel::ship_callsign_close(int ship) } } // otherwise see if it already exists - if (mission_parse_lookup_callsign(_m_callsign.c_str()) >= 0) { - strcpy_s(Fred_callsigns[ship], _m_callsign.c_str()); + if (mission_parse_lookup_callsign(_callsign.c_str()) >= 0) { + strcpy_s(Fred_callsigns[ship], _callsign.c_str()); return; } // otherwise try and add it - if (mission_parse_add_callsign(_m_callsign.c_str()) >= 0) { - strcpy_s(Fred_callsigns[ship], _m_callsign.c_str()); + if (mission_parse_add_callsign(_callsign.c_str()) >= 0) { + strcpy_s(Fred_callsigns[ship], _callsign.c_str()); return; } strcpy_s(Fred_callsigns[ship], ""); @@ -685,11 +685,11 @@ void ShipEditorDialogModel::ship_callsign_close(int ship) void ShipEditorDialogModel::setShipName(const SCP_string& m_ship_name) { - if (_m_ship_name == m_ship_name) + if (_shipName == m_ship_name) return; // Name changes only apply to single-ship editing - if (multi_edit || single_ship < 0) + if (_multiEdit || _singleShip < 0) return; SCP_string new_name = m_ship_name; @@ -700,7 +700,7 @@ void ShipEditorDialogModel::setShipName(const SCP_string& m_ship_name) "Ship Name Error", "A ship name cannot be empty.", {DialogButton::Ok}); - _m_ship_name = Ships[single_ship].ship_name; + _shipName = Ships[_singleShip].ship_name; modelChanged(); return; } @@ -710,20 +710,20 @@ void ShipEditorDialogModel::setShipName(const SCP_string& m_ship_name) "Ship Name Error", "Ship names not allowed to begin with <.", {DialogButton::Ok}); - _m_ship_name = Ships[single_ship].ship_name; + _shipName = Ships[_singleShip].ship_name; modelChanged(); return; } // Check for duplicate ship names for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { - if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->instance != single_ship)) { + if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->instance != _singleShip)) { if (!stricmp(new_name.c_str(), Ships[ptr->instance].ship_name)) { _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Ship Name Error", "This ship name is already being used by another ship.", {DialogButton::Ok}); - _m_ship_name = Ships[single_ship].ship_name; + _shipName = Ships[_singleShip].ship_name; modelChanged(); return; } @@ -737,7 +737,7 @@ void ShipEditorDialogModel::setShipName(const SCP_string& m_ship_name) "Ship Name Error", "This ship name is already being used by a wing.", {DialogButton::Ok}); - _m_ship_name = Ships[single_ship].ship_name; + _shipName = Ships[_singleShip].ship_name; modelChanged(); return; } @@ -750,7 +750,7 @@ void ShipEditorDialogModel::setShipName(const SCP_string& m_ship_name) "Ship Name Error", "This ship name is already being used by a target priority group.", {DialogButton::Ok}); - _m_ship_name = Ships[single_ship].ship_name; + _shipName = Ships[_singleShip].ship_name; modelChanged(); return; } @@ -762,7 +762,7 @@ void ShipEditorDialogModel::setShipName(const SCP_string& m_ship_name) "Ship Name Error", "This ship name is already being used by a waypoint path.", {DialogButton::Ok}); - _m_ship_name = Ships[single_ship].ship_name; + _shipName = Ships[_singleShip].ship_name; modelChanged(); return; } @@ -773,19 +773,19 @@ void ShipEditorDialogModel::setShipName(const SCP_string& m_ship_name) "Ship Name Error", "This ship name is already being used by a jump node.", {DialogButton::Ok}); - _m_ship_name = Ships[single_ship].ship_name; + _shipName = Ships[_singleShip].ship_name; modelChanged(); return; } // Wing member ships cannot be renamed independently - int wing = Ships[single_ship].wingnum; + int wing = Ships[_singleShip].wingnum; if (wing >= 0) { Assert((wing < MAX_WINGS) && Wings[wing].wave_count); // NOLINT char expected_name[255]; int j; for (j = 0; j < Wings[wing].wave_count; j++) - if (_editor->wing_objects[wing][j] == Ships[single_ship].objnum) + if (_editor->wing_objects[wing][j] == Ships[_singleShip].objnum) break; Assert(j < Wings[wing].wave_count); wing_bash_ship_name(expected_name, Wings[wing].name, static_cast(j + 1)); @@ -794,7 +794,7 @@ void ShipEditorDialogModel::setShipName(const SCP_string& m_ship_name) "Ship Name Error", "This ship is part of a wing, and its name cannot be changed", {DialogButton::Ok}); - _m_ship_name = Ships[single_ship].ship_name; + _shipName = Ships[_singleShip].ship_name; modelChanged(); return; } @@ -802,63 +802,63 @@ void ShipEditorDialogModel::setShipName(const SCP_string& m_ship_name) // All validation passed — write the new name char old_name[NAME_LENGTH]; - strcpy_s(old_name, Ships[single_ship].ship_name); - strcpy_s(Ships[single_ship].ship_name, new_name.c_str()); - _m_ship_name = new_name; - - if (strcmp(old_name, Ships[single_ship].ship_name)) { - update_sexp_references(old_name, Ships[single_ship].ship_name); - _editor->ai_update_goal_references(sexp_ref_type::SHIP, old_name, Ships[single_ship].ship_name); - _editor->update_texture_replacements(old_name, Ships[single_ship].ship_name); + strcpy_s(old_name, Ships[_singleShip].ship_name); + strcpy_s(Ships[_singleShip].ship_name, new_name.c_str()); + _shipName = new_name; + + if (strcmp(old_name, Ships[_singleShip].ship_name)) { + update_sexp_references(old_name, Ships[_singleShip].ship_name); + _editor->ai_update_goal_references(sexp_ref_type::SHIP, old_name, Ships[_singleShip].ship_name); + _editor->update_texture_replacements(old_name, Ships[_singleShip].ship_name); for (int j = 0; j < Num_reinforcements; j++) { if (!strcmp(old_name, Reinforcements[j].name)) { - Assert(strlen(Ships[single_ship].ship_name) < NAME_LENGTH); - strcpy_s(Reinforcements[j].name, Ships[single_ship].ship_name); + Assert(strlen(Ships[_singleShip].ship_name) < NAME_LENGTH); + strcpy_s(Reinforcements[j].name, Ships[_singleShip].ship_name); } } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } SCP_string ShipEditorDialogModel::getShipName() const { - return _m_ship_name; + return _shipName; } void ShipEditorDialogModel::setShipDisplayName(const SCP_string& m_ship_display_name) { - if (_m_ship_display_name == m_ship_display_name) + if (_shipDisplayName == m_ship_display_name) return; - _m_ship_display_name = m_ship_display_name; + _shipDisplayName = m_ship_display_name; // Display name only applies to single-ship editing - if (!multi_edit && single_ship >= 0) { + if (!_multiEdit && _singleShip >= 0) { SCP_string display = m_ship_display_name; lcl_fred_replace_stuff(display); - if (display == _m_ship_name || stricmp(display.c_str(), "") == 0) { - Ships[single_ship].display_name = ""; - Ships[single_ship].flags.remove(Ship::Ship_Flags::Has_display_name); + if (display == _shipName || stricmp(display.c_str(), "") == 0) { + Ships[_singleShip].display_name = ""; + Ships[_singleShip].flags.remove(Ship::Ship_Flags::Has_display_name); } else { - Ships[single_ship].display_name = display; - Ships[single_ship].flags.set(Ship::Ship_Flags::Has_display_name); + Ships[_singleShip].display_name = display; + Ships[_singleShip].flags.set(Ship::Ship_Flags::Has_display_name); } - set_modified(); + setModified(); _editor->missionChanged(); } modelChanged(); } SCP_string ShipEditorDialogModel::getShipDisplayName() const { - return _m_ship_display_name; + return _shipDisplayName; } void ShipEditorDialogModel::setShipClass(int m_ship_class) { - if (_m_ship_class == m_ship_class) + if (_shipClass == m_ship_class) return; - _m_ship_class = m_ship_class; + _shipClass = m_ship_class; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { if (Ships[ptr->instance].ship_info_index != m_ship_class) { @@ -866,66 +866,66 @@ void ShipEditorDialogModel::setShipClass(int m_ship_class) } } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getShipClass() const { - return _m_ship_class; + return _shipClass; } void ShipEditorDialogModel::setAIClass(const int m_ai_class) { - if (_m_ai_class == m_ai_class) + if (_aiClass == m_ai_class) return; - _m_ai_class = m_ai_class; + _aiClass = m_ai_class; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { Ships[ptr->instance].weapons.ai_class = m_ai_class; } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getAIClass() const { - return _m_ai_class; + return _aiClass; } void ShipEditorDialogModel::setTeam(const int m_team) { - if (_m_team == m_team) + if (_team == m_team) return; - _m_team = m_team; + _team = m_team; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { Ships[ptr->instance].team = m_team; } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getTeam() const { - return _m_team; + return _team; } SCP_string ShipEditorDialogModel::getLayer() const { - return _m_layer; + return _layer; } void ShipEditorDialogModel::setLayer(const SCP_string& layer) { - if (_m_layer == layer) + if (_layer == layer) return; - _m_layer = layer; + _layer = layer; for (auto objp = GET_FIRST(&obj_used_list); objp != END_OF_LIST(&obj_used_list); objp = GET_NEXT(objp)) { if (objp->flags[Object::Object_Flags::Marked]) { @@ -935,16 +935,16 @@ void ShipEditorDialogModel::setLayer(const SCP_string& layer) } } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } void ShipEditorDialogModel::setCargo(const SCP_string& m_cargo) { - if (_m_cargo1 == m_cargo) + if (_cargo == m_cargo) return; - _m_cargo1 = m_cargo; + _cargo = m_cargo; SCP_string cargo_name = m_cargo; lcl_fred_replace_stuff(cargo_name); @@ -958,7 +958,7 @@ void ShipEditorDialogModel::setCargo(const SCP_string& m_cargo) sprintf(str, "Maximum number of cargo names %d reached.\nIgnoring new name.\n", MAX_CARGO); _viewport->dialogProvider->showButtonDialog(DialogType::Warning, "Cargo Error", str, {DialogButton::Ok}); z = 0; - _m_cargo1 = Cargo_names[z]; + _cargo = Cargo_names[z]; } } for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { @@ -966,42 +966,42 @@ void ShipEditorDialogModel::setCargo(const SCP_string& m_cargo) Ships[ptr->instance].cargo1 = (char)z; } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } SCP_string ShipEditorDialogModel::getCargo() const { - return _m_cargo1; + return _cargo; } void ShipEditorDialogModel::setCargoTitle(const SCP_string& title) { - if (_m_cargo_title == title) + if (_cargoTitle == title) return; - _m_cargo_title = title; + _cargoTitle = title; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { strcpy_s(Ships[ptr->instance].cargo_title, title.c_str()); } } - set_modified(); + setModified(); } SCP_string ShipEditorDialogModel::getCargoTitle() const { - return _m_cargo_title; + return _cargoTitle; } void ShipEditorDialogModel::setAltName(const SCP_string& m_altName) { - if (_m_alt_name == m_altName) + if (_altName == m_altName) return; - _m_alt_name = m_altName; - if (!multi_edit && single_ship >= 0) { - ship_alt_name_close(single_ship); - set_modified(); + _altName = m_altName; + if (!_multiEdit && _singleShip >= 0) { + shipAltNameClose(_singleShip); + setModified(); _editor->missionChanged(); } modelChanged(); @@ -1009,17 +1009,17 @@ void ShipEditorDialogModel::setAltName(const SCP_string& m_altName) SCP_string ShipEditorDialogModel::getAltName() const { - return _m_alt_name; + return _altName; } void ShipEditorDialogModel::setCallsign(const SCP_string& m_callsign) { - if (_m_callsign == m_callsign) + if (_callsign == m_callsign) return; - _m_callsign = m_callsign; - if (!multi_edit && single_ship >= 0) { - ship_callsign_close(single_ship); - set_modified(); + _callsign = m_callsign; + if (!_multiEdit && _singleShip >= 0) { + shipCallsignClose(_singleShip); + setModified(); _editor->missionChanged(); } modelChanged(); @@ -1027,80 +1027,80 @@ void ShipEditorDialogModel::setCallsign(const SCP_string& m_callsign) SCP_string ShipEditorDialogModel::getCallsign() const { - return _m_callsign; + return _callsign; } SCP_string ShipEditorDialogModel::getWing() const { - return m_wing; + return _wing; } void ShipEditorDialogModel::setHotkey(const int m_hotkey) { - if (_m_hotkey == m_hotkey) + if (_hotkey == m_hotkey) return; - _m_hotkey = m_hotkey; + _hotkey = m_hotkey; // hotkey stored as 1-indexed in model (0 = none), Ships[] uses 0-indexed (-1 = none, hotkey-1 otherwise) for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { Ships[ptr->instance].hotkey = m_hotkey - 1; } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getHotkey() const { - return _m_hotkey; + return _hotkey; } void ShipEditorDialogModel::setPersona(const int m_persona) { - if (_m_persona == m_persona) + if (_persona == m_persona) return; - _m_persona = m_persona; + _persona = m_persona; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { Ships[ptr->instance].persona_index = m_persona; } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getPersona() const { - return _m_persona; + return _persona; } void ShipEditorDialogModel::setScore(const int m_score) { - if (_m_score == m_score) + if (_score == m_score) return; - _m_score = m_score; + _score = m_score; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { Ships[ptr->instance].score = m_score; } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getScore() const { - return _m_score; + return _score; } void ShipEditorDialogModel::setAssist(const int m_assist_score) { - if (_m_assist_score == m_assist_score) + if (_assistScore == m_assist_score) return; - _m_assist_score = m_assist_score; + _assistScore = m_assist_score; float pct = static_cast(m_assist_score) / 100.0f; pct = std::clamp(pct, 0.0f, 1.0f); for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { @@ -1108,21 +1108,21 @@ void ShipEditorDialogModel::setAssist(const int m_assist_score) Ships[ptr->instance].assist_score_pct = pct; } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getAssist() const { - return _m_assist_score; + return _assistScore; } void ShipEditorDialogModel::setPlayer(const bool m_player) { - if (_m_player_ship == m_player) + if (_isPlayerShip == m_player) return; - _m_player_ship = m_player; + _isPlayerShip = m_player; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { if (m_player) { @@ -1147,21 +1147,21 @@ void ShipEditorDialogModel::setPlayer(const bool m_player) } } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } bool ShipEditorDialogModel::getPlayer() const { - return _m_player_ship; + return _isPlayerShip; } void ShipEditorDialogModel::setRespawn(const int value) { - if (respawn_priority == value) + if (_respawnPriority == value) return; - respawn_priority = value; + _respawnPriority = value; if (The_mission.game_type & MISSION_TYPE_MULTI) { for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { @@ -1169,21 +1169,21 @@ void ShipEditorDialogModel::setRespawn(const int value) } } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getRespawn() const { - return respawn_priority; + return _respawnPriority; } void ShipEditorDialogModel::setArrivalLocationIndex(const int value) { - if (_m_arrival_location == value) + if (_arrivalLocation == value) return; - _m_arrival_location = value; + _arrivalLocation = value; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { if (Ships[ptr->instance].wingnum < 0) { @@ -1191,14 +1191,14 @@ void ShipEditorDialogModel::setArrivalLocationIndex(const int value) } } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getArrivalLocationIndex() const { - return _m_arrival_location; + return _arrivalLocation; } void ShipEditorDialogModel::setArrivalLocation(const ArrivalLocation value) @@ -1208,7 +1208,7 @@ void ShipEditorDialogModel::setArrivalLocation(const ArrivalLocation value) ArrivalLocation ShipEditorDialogModel::getArrivalLocation() const { - return static_cast(_m_arrival_location); + return static_cast(_arrivalLocation); } int ShipEditorDialogModel::computeArrivalMinDist() const @@ -1219,7 +1219,7 @@ int ShipEditorDialogModel::computeArrivalMinDist() const return 0; // Validation doesn't apply to special anchors (negative value or ANCHOR_SPECIAL_ARRIVAL flag set) - if (_m_arrival_target < 0 || (_m_arrival_target & ANCHOR_SPECIAL_ARRIVAL)) + if (_arrivalTarget < 0 || (_arrivalTarget & ANCHOR_SPECIAL_ARRIVAL)) return 0; // Compute the most restrictive minimum distance across all marked arriving ships @@ -1239,22 +1239,22 @@ int ShipEditorDialogModel::computeArrivalMinDist() const void ShipEditorDialogModel::setArrivalTarget(const int value) { - if (_m_arrival_target == value) + if (_arrivalTarget == value) return; - _m_arrival_target = value; + _arrivalTarget = value; // Re-validate the existing arrival distance now that the target has changed. // A target change from a special anchor to a real ship can make a previously // acceptable distance too close. const int min_dist = computeArrivalMinDist(); - if (min_dist > 0 && _m_arrival_dist > -min_dist && _m_arrival_dist < min_dist) { - const int clamped = (_m_arrival_dist < 0) ? -min_dist : min_dist; + if (min_dist > 0 && _arrivalDist > -min_dist && _arrivalDist < min_dist) { + const int clamped = (_arrivalDist < 0) ? -min_dist : min_dist; QMessageBox::warning(nullptr, tr("Arrival Distance"), tr("Ship must arrive at least %1 meters away from target.\n" "Value has been reset to this. Use with caution!") .arg(min_dist)); - _m_arrival_dist = clamped; + _arrivalDist = clamped; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && ptr->flags[Object::Object_Flags::Marked] && @@ -1271,19 +1271,19 @@ void ShipEditorDialogModel::setArrivalTarget(const int value) } } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getArrivalTarget() const { - return _m_arrival_target; + return _arrivalTarget; } void ShipEditorDialogModel::setArrivalDistance(const int value) { - if (_m_arrival_dist == value) + if (_arrivalDist == value) return; const int min_dist = computeArrivalMinDist(); @@ -1297,7 +1297,7 @@ void ShipEditorDialogModel::setArrivalDistance(const int value) .arg(min_dist)); } - _m_arrival_dist = effective_value; + _arrivalDist = effective_value; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { if (Ships[ptr->instance].wingnum < 0) { @@ -1305,21 +1305,21 @@ void ShipEditorDialogModel::setArrivalDistance(const int value) } } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getArrivalDistance() const { - return _m_arrival_dist; + return _arrivalDist; } void ShipEditorDialogModel::setArrivalDelay(const int value) { - if (_m_arrival_delay == value) + if (_arrivalDelay == value) return; - _m_arrival_delay = value; + _arrivalDelay = value; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { if (Ships[ptr->instance].wingnum < 0) { @@ -1327,42 +1327,42 @@ void ShipEditorDialogModel::setArrivalDelay(const int value) } } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getArrivalDelay() const { - return _m_arrival_delay; + return _arrivalDelay; } void ShipEditorDialogModel::setArrivalCue(const bool value) { - modify(_m_update_arrival, value); + modify(_updateArrival, value); } bool ShipEditorDialogModel::getArrivalCue() const { - return _m_update_arrival; + return _updateArrival; } void ShipEditorDialogModel::setArrivalFormula(const int old_form, const int new_form) { - if (old_form != _m_arrival_tree_formula) - modify(_m_arrival_tree_formula, new_form); + if (old_form != _arrivalTreeFormula) + modify(_arrivalTreeFormula, new_form); } int ShipEditorDialogModel::getArrivalFormula() const { - return _m_arrival_tree_formula; + return _arrivalTreeFormula; } void ShipEditorDialogModel::setNoArrivalWarp(const int value) { - if (_m_no_arrival_warp == value) + if (_noArrivalWarp == value) return; - _m_no_arrival_warp = value; + _noArrivalWarp = value; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { if (value) { @@ -1372,21 +1372,21 @@ void ShipEditorDialogModel::setNoArrivalWarp(const int value) } } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getNoArrivalWarp() const { - return _m_no_arrival_warp; + return _noArrivalWarp; } void ShipEditorDialogModel::setDepartureLocationIndex(const int value) { - if (_m_departure_location == value) + if (_departureLocation == value) return; - _m_departure_location = value; + _departureLocation = value; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { if (Ships[ptr->instance].wingnum < 0) { @@ -1394,14 +1394,14 @@ void ShipEditorDialogModel::setDepartureLocationIndex(const int value) } } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getDepartureLocationIndex() const { - return _m_departure_location; + return _departureLocation; } void ShipEditorDialogModel::setDepartureLocation(const DepartureLocation value) @@ -1411,14 +1411,14 @@ void ShipEditorDialogModel::setDepartureLocation(const DepartureLocation value) DepartureLocation ShipEditorDialogModel::getDepartureLocation() const { - return static_cast(_m_departure_location); + return static_cast(_departureLocation); } void ShipEditorDialogModel::setDepartureTarget(const int value) { - if (_m_departure_target == value) + if (_departureTarget == value) return; - _m_departure_target = value; + _departureTarget = value; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { if (Ships[ptr->instance].wingnum < 0 && value >= 0) { @@ -1426,21 +1426,21 @@ void ShipEditorDialogModel::setDepartureTarget(const int value) } } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getDepartureTarget() const { - return _m_departure_target; + return _departureTarget; } void ShipEditorDialogModel::setDepartureDelay(const int value) { - if (_m_departure_delay == value) + if (_departureDelay == value) return; - _m_departure_delay = value; + _departureDelay = value; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { if (Ships[ptr->instance].wingnum < 0) { @@ -1448,42 +1448,42 @@ void ShipEditorDialogModel::setDepartureDelay(const int value) } } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getDepartureDelay() const { - return _m_departure_delay; + return _departureDelay; } void ShipEditorDialogModel::setDepartureCue(const bool value) { - modify(_m_update_departure, value); + modify(_updateDeparture, value); } bool ShipEditorDialogModel::getDepartureCue() const { - return _m_update_departure; + return _updateDeparture; } void ShipEditorDialogModel::setDepartureFormula(const int old_form, const int new_form) { - if (old_form != _m_departure_tree_formula) - modify(_m_departure_tree_formula, new_form); + if (old_form != _departureTreeFormula) + modify(_departureTreeFormula, new_form); } int ShipEditorDialogModel::getDepartureFormula() const { - return _m_departure_tree_formula; + return _departureTreeFormula; } void ShipEditorDialogModel::setNoDepartureWarp(const int value) { - if (_m_no_departure_warp == value) + if (_noDepartureWarp == value) return; - _m_no_departure_warp = value; + _noDepartureWarp = value; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { if (value) { @@ -1493,20 +1493,20 @@ void ShipEditorDialogModel::setNoDepartureWarp(const int value) } } } - set_modified(); + setModified(); _editor->missionChanged(); modelChanged(); } int ShipEditorDialogModel::getNoDepartureWarp() const { - return _m_no_departure_warp; + return _noDepartureWarp; } -void ShipEditorDialogModel::OnPrevious() +void ShipEditorDialogModel::onPrevious() { int i, n, arr[MAX_SHIPS]; - n = make_ship_list(arr); + n = makeShipList(arr); if (!n) { return; } @@ -1530,10 +1530,10 @@ void ShipEditorDialogModel::OnPrevious() _editor->selectObject(arr[i]); } -void ShipEditorDialogModel::OnNext() +void ShipEditorDialogModel::onNext() { int i, n, arr[MAX_SHIPS]; - n = make_ship_list(arr); + n = makeShipList(arr); if (!n) return; @@ -1554,13 +1554,13 @@ void ShipEditorDialogModel::OnNext() _editor->selectObject(arr[i]); } -void ShipEditorDialogModel::OnDeleteShip() +void ShipEditorDialogModel::onDeleteShip() { _editor->delete_marked(); _editor->unmark_all(); } -void ShipEditorDialogModel::OnShipReset() +void ShipEditorDialogModel::onShipReset() { int i, j, index, ship; object* objp; @@ -1571,13 +1571,13 @@ void ShipEditorDialogModel::OnShipReset() setCargo("Nothing"); setAIClass(AI_DEFAULT_CLASS); - if (_m_ship_class >= 0) { - setTeam(Species_info[Ship_info[_m_ship_class].species].default_iff); + if (_shipClass >= 0) { + setTeam(Species_info[Ship_info[_shipClass].species].default_iff); } objp = GET_FIRST(&obj_used_list); while (objp != END_OF_LIST(&obj_used_list)) { - if (((objp->type == OBJ_SHIP) || ((objp->type == OBJ_START) && !mission_type)) && + if (((objp->type == OBJ_SHIP) || ((objp->type == OBJ_START) && !_missionType)) && (objp->flags[Object::Object_Flags::Marked])) { ship = objp->instance; @@ -1638,7 +1638,7 @@ void ShipEditorDialogModel::OnShipReset() } modelChanged(); _editor->missionChanged(); - if (multi_edit) { + if (_multiEdit) { _viewport->dialogProvider->showButtonDialog(DialogType::Information, "Reset", @@ -1653,22 +1653,22 @@ void ShipEditorDialogModel::OnShipReset() } } -bool ShipEditorDialogModel::wing_is_player_wing(const int wing) const +bool ShipEditorDialogModel::wingIsPlayerWing(int wingNum) const { - return _editor->wing_is_player_wing(wing); + return _editor->wing_is_player_wing(wingNum); } -const std::set& ShipEditorDialogModel::getShipOrders() const +const SCP_set& ShipEditorDialogModel::getShipOrders() const { - return ship_orders; + return _shipOrders; } bool ShipEditorDialogModel::getTexEditEnable() const { - return texenable; + return _texEnable; } -int ShipEditorDialogModel::make_ship_list(int* arr) +int ShipEditorDialogModel::makeShipList(int* arr) { int n = 0; object* ptr; @@ -1685,7 +1685,7 @@ int ShipEditorDialogModel::make_ship_list(int* arr) return n; } -void ShipEditorDialogModel::set_modified() +void ShipEditorDialogModel::setModified() { if (!_modified) { _modified = true; diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h index 97777e29b1b..f0079aadfd0 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h @@ -6,250 +6,192 @@ #include "ui/widgets/sexp_tree.h" namespace fso::fred::dialogs { -/** - * @brief QTFred's Ship Editor's Model - */ -class ShipEditorDialogModel : public AbstractDialogModel { - private: - int _m_no_departure_warp; - int _m_no_arrival_warp; - bool _m_player_ship; - int _m_departure_tree_formula; - int _m_arrival_tree_formula; - SCP_string _m_ship_name; - SCP_string _m_ship_display_name; - SCP_string _m_cargo1; - SCP_string _m_cargo_title; - SCP_string _m_alt_name; - SCP_string _m_callsign; - int _m_ship_class; - int _m_team; - SCP_string _m_layer; - int _m_arrival_location; - int _m_departure_location; - int _m_ai_class; - int _m_arrival_delay; - int _m_departure_delay; - int _m_hotkey; - bool _m_update_arrival; - bool _m_update_departure; - int _m_score; - int _m_assist_score; - int _m_arrival_dist; - int _m_arrival_target; - int _m_departure_target; - int _m_persona; - - SCP_string m_wing; - - int mission_type; - - void set_modified(); - - void ship_alt_name_close(int base_ship); - void ship_callsign_close(int base_ship); - - static int make_ship_list(int* arr); - - int computeArrivalMinDist() const; - - bool enable = true; - int player_count; - int ship_count; - bool multi_edit; - int cue_init; - int total_count; - int pvalid_count; - int pship_count; // a total count of the player ships not marked - int single_ship; - int player_ship; - - std::set ship_orders; - - bool texenable = true; - - int respawn_priority; - - std::vector> arrivalPaths; - std::vector> departurePaths; +class ShipEditorDialogModel : public AbstractDialogModel { + Q_OBJECT public: - ShipEditorDialogModel(QObject* parent, EditorViewport* viewport); - void initializeData(); + explicit ShipEditorDialogModel(QObject* parent, EditorViewport* viewport); bool apply() override; void reject() override; - void setShipName(const SCP_string& m_ship_name); + void setShipName(const SCP_string& name); SCP_string getShipName() const; - void setShipDisplayName(const SCP_string& m_ship_display_name); + void setShipDisplayName(const SCP_string& displayName); SCP_string getShipDisplayName() const; - void setShipClass(const int); + void setShipClass(int shipClass); int getShipClass() const; - void setAIClass(const int); + void setAIClass(int aiClass); int getAIClass() const; - void setTeam(const int); + void setTeam(int team); int getTeam() const; void setLayer(const SCP_string& layer); SCP_string getLayer() const; - void setCargo(const SCP_string&); + void setCargo(const SCP_string& cargo); SCP_string getCargo() const; - void setCargoTitle(const SCP_string&); + void setCargoTitle(const SCP_string& cargoTitle); SCP_string getCargoTitle() const; - void setAltName(const SCP_string&); + void setAltName(const SCP_string& altName); SCP_string getAltName() const; - void setCallsign(const SCP_string&); + void setCallsign(const SCP_string& callsign); SCP_string getCallsign() const; - /** - * @brief Gets the ships wing - * @return Name of the ships wing - */ SCP_string getWing() const; - void setHotkey(const int); + void setHotkey(int hotkey); int getHotkey() const; - void setPersona(const int); + void setPersona(int persona); int getPersona() const; - void setScore(const int); + void setScore(int score); int getScore() const; - void setAssist(const int); + void setAssist(int assist); int getAssist() const; - void setPlayer(const bool); + void setPlayer(bool isPlayer); bool getPlayer() const; - void setRespawn(const int); + void setRespawn(int respawn); int getRespawn() const; - void setArrivalLocationIndex(const int); + void setArrivalLocationIndex(int index); int getArrivalLocationIndex() const; - void setArrivalLocation(const ArrivalLocation); + void setArrivalLocation(ArrivalLocation location); ArrivalLocation getArrivalLocation() const; - void setArrivalTarget(const int); + void setArrivalTarget(int targetIndex); int getArrivalTarget() const; - void setArrivalDistance(const int); + void setArrivalDistance(int distance); int getArrivalDistance() const; - void setArrivalDelay(const int); + void setArrivalDelay(int delay); int getArrivalDelay() const; - void setArrivalCue(const bool); + void setArrivalCue(bool updateCue); bool getArrivalCue() const; - void setArrivalFormula(const int, const int); + void setArrivalFormula(int formula, int objNum); int getArrivalFormula() const; - void setNoArrivalWarp(const int); + void setNoArrivalWarp(int state); int getNoArrivalWarp() const; - void setDepartureLocationIndex(const int); + void setDepartureLocationIndex(int index); int getDepartureLocationIndex() const; - void setDepartureLocation(const DepartureLocation); + void setDepartureLocation(DepartureLocation location); DepartureLocation getDepartureLocation() const; - void setDepartureTarget(const int); + void setDepartureTarget(int targetIndex); int getDepartureTarget() const; - void setDepartureDelay(const int); + void setDepartureDelay(int delay); int getDepartureDelay() const; - void setDepartureCue(const bool); + void setDepartureCue(bool updateCue); bool getDepartureCue() const; - void setDepartureFormula(const int, const int); + void setDepartureFormula(int formula, int objNum); int getDepartureFormula() const; - void setNoDepartureWarp(const int); + void setNoDepartureWarp(int state); int getNoDepartureWarp() const; - /** - * @brief Selects previous ship on the list - */ - void OnPrevious(); - /** - * @brief Selects next ship on the list - */ - void OnNext(); - void OnDeleteShip(); - /** - * @brief Resets Ship/s to class default - * @note Does not cover data from all subdialogs could use expanding - */ - void OnShipReset(); - /** - * @brief Returns true if the wing is a player wing - * @param wing Takes an integer id of the wing - */ - bool wing_is_player_wing(const int) const; - const std::set& getShipOrders() const; + + void onPrevious(); + void onNext(); + void onDeleteShip(); + void onShipReset(); + + bool wingIsPlayerWing(int wingNum) const; + const SCP_set& getShipOrders() const; bool getTexEditEnable() const; - /** - * @brief Used for handling conflicts between ships having differing states of the same flag - * @param val Takes the new value - * @param cur_state Takes the current state of the flag - * @return integer state the flag should be set to - * @warning Contains QT code. Will need refactoring if migrated to non QT environment. - */ - static int tristate_set(const int val, const int cur_state); - - /** - * @brief Returns the ID of the primary marked ship - */ + + static int tristate_set(int val, int curState); + int getSingleShip() const; - /** - * @brief Returns true if multiple ships/players selected - */ bool getIfMultipleShips() const; - /** - * @brief Returns number of selected players - */ int getNumSelectedPlayers() const; - /** - * @brief Get number of player ships not selected - */ int getNumUnmarkedPlayers() const; - /** - * @brief Whether or not to enable the UI - */ bool getUIEnable() const; - /** - * @brief Get number of non player ships - */ int getNumSelectedShips() const; int getUseCue() const; - /** - * @brief Get number of Ships selected - */ int getNumSelectedObjects() const; - /** - * @brief Get number of player ships in mission - */ int getNumValidPlayers() const; - /** - * @brief Returns true if only a single ship is selected and it is a player ship - */ int getIfPlayerShip() const; static SCP_vector> getPlayerOrders(); void applyPlayerOrders(const SCP_vector>& orders); - std::vector> getArrivalPaths() const; - void setArrivalPaths(const std::vector>&); + SCP_vector> getArrivalPaths() const; + void setArrivalPaths(const SCP_vector>& paths); + + SCP_vector> getDeparturePaths() const; + void setDeparturePaths(const SCP_vector>& paths); - std::vector> getDeparturePaths() const; - void setDeparturePaths(const std::vector>&); + void initializeData(); + + private: // NOLINT(readability-redundant-access-specifiers) + void setModified(); + void shipAltNameClose(int baseShip); + void shipCallsignClose(int baseShip); + static int makeShipList(int* arr); + int computeArrivalMinDist() const; + + int _noDepartureWarp; + int _noArrivalWarp; + bool _isPlayerShip; + int _departureTreeFormula; + int _arrivalTreeFormula; + SCP_string _shipName; + SCP_string _shipDisplayName; + SCP_string _cargo; + SCP_string _cargoTitle; + SCP_string _altName; + SCP_string _callsign; + int _shipClass; + int _team; + SCP_string _layer; + int _arrivalLocation; + int _departureLocation; + int _aiClass; + int _arrivalDelay; + int _departureDelay; + int _hotkey; + bool _updateArrival; + bool _updateDeparture; + int _score; + int _assistScore; + int _arrivalDist; + int _arrivalTarget; + int _departureTarget; + int _persona; + SCP_string _wing; + int _missionType; + bool _enable = true; + int _playerCount; + int _shipCount; + bool _multiEdit; + int _cueInit; + int _totalCount; + int _pvalidCount; + int _pshipCount; + int _singleShip; + int _playerShipIndex; + SCP_set _shipOrders; + bool _texEnable = true; + int _respawnPriority; + SCP_vector> _arrivalPaths; + SCP_vector> _departurePaths; }; -} // namespace fso::fred::dialogs \ No newline at end of file + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp index 63181230927..15beeed4f40 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp @@ -32,7 +32,7 @@ ShipEditorDialog::ShipEditorDialog(FredView* parent, EditorViewport* viewport) ui->callsignCombo->lineEdit()->setMaxLength(CALLSIGN_LEN); ui->cargoTitleEdit->setMaxLength(NAME_LENGTH - 1); - connect(_model.get(), &AbstractDialogModel::modelChanged, this, [this] { updateUI(false); }); + connect(_model.get(), &AbstractDialogModel::modelChanged, this, [this] { updateUi(false); }); connect(viewport->editor, &Editor::currentObjectChanged, this, &ShipEditorDialog::update); connect(viewport->editor, &Editor::objectMarkingChanged, this, &ShipEditorDialog::update); @@ -45,7 +45,7 @@ ShipEditorDialog::ShipEditorDialog(FredView* parent, EditorViewport* viewport) // ui->cargoCombo->installEventFilter(this); - updateUI(true); + updateUi(true); // Resize the dialog to the minimum size resize(QDialog::sizeHint()); @@ -118,11 +118,11 @@ void ShipEditorDialog::update() { if (this->isVisible()) { _model->initializeData(); - updateUI(true); + updateUi(true); } } -void ShipEditorDialog::updateUI(bool overwrite) +void ShipEditorDialog::updateUi(bool overwrite) { util::SignalBlockers blockers(this); enableDisable(); @@ -491,8 +491,8 @@ void ShipEditorDialog::enableDisable() ui->specialStatsButton->setEnabled(false); } - // disable textures for multiple ships - ui->textureReplacementButton->setEnabled(_model->getTexEditEnable()); + // disable textures unless exactly one ship/player is selected + ui->textureReplacementButton->setEnabled(_model->getNumSelectedObjects() == 1); ui->AIClassCombo->setEnabled(_model->getUIEnable()); ui->cargoCombo->setEnabled(_model->getUIEnable()); @@ -529,7 +529,7 @@ void ShipEditorDialog::enableDisable() // enable the "set player" button only if single player, single edit, and ship is in player wing { int marked_ship = (_model->getIfPlayerShip() >= 0) ? _model->getIfPlayerShip() : _model->getSingleShip(); - const bool isPlayerWing = _model->wing_is_player_wing(Ships[marked_ship].wingnum); + const bool isPlayerWing = _model->wingIsPlayerWing(Ships[marked_ship].wingnum); if (!(The_mission.game_type & MISSION_TYPE_MULTI) && (_model->getNumSelectedObjects() > 0) && (_model->getIfMultipleShips() != true) && (isPlayerWing == true)) ui->playerShipButton->setEnabled(true); @@ -537,8 +537,9 @@ void ShipEditorDialog::enableDisable() ui->playerShipButton->setEnabled(false); } - ui->deleteButton->setEnabled(_model->getUIEnable()); - ui->resetButton->setEnabled(_model->getUIEnable()); + const bool noPlayerSelected = (_model->getNumSelectedPlayers() == 0); + ui->deleteButton->setEnabled(_model->getUIEnable() && noPlayerSelected); + ui->resetButton->setEnabled(_model->getUIEnable() && noPlayerSelected); ui->killScoreEdit->setEnabled(_model->getUIEnable()); ui->assistEdit->setEnabled(_model->getUIEnable()); @@ -552,7 +553,10 @@ void ShipEditorDialog::enableDisable() ui->updateDepartureCueCheckBox->setVisible(false); } - if (_model->getIfMultipleShips() || (_model->getNumSelectedObjects() > 1)) { + if (_model->getNumSelectedPlayers() > 0) { + // player ships don't take orders from the player + ui->playerOrdersButton->setEnabled(false); + } else if (_model->getIfMultipleShips() || (_model->getNumSelectedObjects() > 1)) { // we will allow the ignore orders dialog to be multi edit if all selected // ships are the same type. the ship_type variable holds the ship types // for all ships. Determine how may bits set and enable/diable window @@ -633,19 +637,19 @@ void ShipEditorDialog::on_altShipClassButton_clicked() } void ShipEditorDialog::on_prevButton_clicked() { - _model->OnPrevious(); + _model->onPrevious(); } void ShipEditorDialog::on_nextButton_clicked() { - _model->OnNext(); + _model->onNext(); } void ShipEditorDialog::on_resetButton_clicked() { - _model->OnShipReset(); + _model->onShipReset(); } void ShipEditorDialog::on_deleteButton_clicked() { - _model->OnDeleteShip(); + _model->onDeleteShip(); } void ShipEditorDialog::on_weaponsButton_clicked() { @@ -706,11 +710,10 @@ void ShipEditorDialog::on_restrictArrivalPathsButton_clicked() if (dlg.exec() == QDialog::Accepted) { auto returned_values = dlg.getCheckedStates(); - std::vector> updatedPaths; + SCP_vector> updatedPaths; for (int i = 0; i < checkbox_list.size(); ++i) { - // Convert back to std::string - std::string name = checkbox_list[i].first.toUtf8().constData(); + SCP_string name = checkbox_list[i].first.toUtf8().constData(); updatedPaths.emplace_back(name, returned_values[i]); } @@ -737,11 +740,10 @@ void ShipEditorDialog::on_restrictDeparturePathsButton_clicked() if (dlg.exec() == QDialog::Accepted) { auto returned_values = dlg.getCheckedStates(); - std::vector> updatedPaths; + SCP_vector> updatedPaths; for (int i = 0; i < checkbox_list.size(); ++i) { - // Convert back to std::string - std::string name = checkbox_list[i].first.toUtf8().constData(); + SCP_string name = checkbox_list[i].first.toUtf8().constData(); updatedPaths.emplace_back(name, returned_values[i]); } diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h index 2f35675794d..a40831585b3 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h @@ -1,5 +1,4 @@ -#ifndef SHIPDEDITORDIALOG_H -#define SHIPDEDITORDIALOG_H +#pragma once #include "ShipCustomWarpDialog.h" #include "ShipFlagsDialog.h" @@ -21,38 +20,15 @@ namespace Ui { class ShipEditorDialog; } -/** - * @brief QTFred's Ship Editor - */ class ShipEditorDialog : public QDialog, public SexpTreeEditorInterface { - Q_OBJECT public: - /** - * @brief Constructor - * @param parent The main fred window. Needed for triggering window updates. - * @param viewport The viewport this dialog is attacted to. - */ explicit ShipEditorDialog(FredView* parent, EditorViewport* viewport); ~ShipEditorDialog() override; - /** - * @brief Allows subdialogs to get the ships class - * @return Returns the ship_info_index of the current ship or -1 if multiple ships selected. - */ int getShipClass() const; - - /** - * @brief Allows subdialogs to get the ship the editor is currently working on. - * @return Returns the index in Ships if working on one or -1 if working on multiple. - */ int getSingleShip() const; - - /** - * @brief Allows subdialogs to know if we are working on multiple ships. - * @return true if multiple ships are selected. - */ bool getIfMultipleShips() const; protected: @@ -126,7 +102,7 @@ class ShipEditorDialog : public QDialog, public SexpTreeEditorInterface { void update(); - void updateUI(bool overwrite = false); + void updateUi(bool overwrite = false); void updateColumnOne(bool overwrite = false); void updateColumnTwo(bool ovewrite = false); void updateArrival(bool overwrite = false); @@ -140,5 +116,3 @@ class ShipEditorDialog : public QDialog, public SexpTreeEditorInterface { void callsignChanged(); }; } // namespace fso::fred::dialogs - -#endif // SHIPDEDITORDIALOG_H \ No newline at end of file From f8fa1e4775da1593a1d08a926ce99131e0b44d94 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 13 May 2026 23:29:31 -0400 Subject: [PATCH 41/65] a bit of cleanup Fix a few items flagged by Coverity and Claude: - properly check for an unassigned ship node, rather than just Asserting, since this could be affected by mission data - properly check for null containers for the same reason - rename `ai_goal_find_empty_slot` to `ai_goal_allocate_slot` and edit comments to reflect its not-quite-side-effect-free role --- code/ai/aigoals.cpp | 17 +++++++++-------- code/parse/sexp.cpp | 25 ++++++++++++++++--------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/code/ai/aigoals.cpp b/code/ai/aigoals.cpp index 9402b7f7b0b..9c0dbf50ddd 100644 --- a/code/ai/aigoals.cpp +++ b/code/ai/aigoals.cpp @@ -825,7 +825,8 @@ void ai_add_goal_sub_player(ai_goal_type type, ai_goal_mode mode, int submode, c // friendlies want to rearm at the same time. The support ship forgets what it's doing and flies // off to repair somebody while still docked. I reproduced this with retail, so it's not a bug in // my new docking code. :) -int ai_goal_find_empty_slot( ai_goal *goals, int active_goal ) +// Note that this function is now where Purge_when_new_goal_added is checked to set the Purge flag. +int ai_goal_allocate_slot( ai_goal *goals, int active_goal ) { int oldest_index = -1, first_empty_index = -1; @@ -915,7 +916,7 @@ void ai_add_ship_goal_scripting(ai_goal_mode mode, int submode, int priority, co int empty_index; ai_goal *aigp; - empty_index = ai_goal_find_empty_slot(aip->goals, aip->active_goal); + empty_index = ai_goal_allocate_slot(aip->goals, aip->active_goal); aigp = &aip->goals[empty_index]; ai_add_goal_sub_scripting(ai_goal_type::PLAYER_SHIP, mode, submode, priority, shipname, aigp, int_data, float_data); @@ -939,7 +940,7 @@ void ai_add_ship_goal_player(ai_goal_type type, ai_goal_mode mode, int submode, int empty_index; ai_goal *aigp; - empty_index = ai_goal_find_empty_slot( aip->goals, aip->active_goal ); + empty_index = ai_goal_allocate_slot( aip->goals, aip->active_goal ); aigp = &aip->goals[empty_index]; ai_add_goal_sub_player( type, mode, submode, shipname, aigp, int_data, float_data, lua_target ); @@ -973,7 +974,7 @@ void ai_add_wing_goal_player(ai_goal_type type, ai_goal_mode mode, int submode, // add the sexpression index into the wing's list of goal sexpressions if // there are more waves to come. We use the same method here as when adding a goal to // a ship -- find the first empty entry. If none exists, take the oldest entry and replace it. - empty_index = ai_goal_find_empty_slot( wingp->ai_goals, -1 ); + empty_index = ai_goal_allocate_slot( wingp->ai_goals, -1 ); ai_add_goal_sub_player( type, mode, submode, shipname, &wingp->ai_goals[empty_index], int_data, float_data, lua_target ); } @@ -1613,7 +1614,7 @@ void ai_add_ship_goal_sexp( int sexp, ai_goal_type type, ai_info *aip ) { int gindex; - gindex = ai_goal_find_empty_slot( aip->goals, aip->active_goal ); + gindex = ai_goal_allocate_slot( aip->goals, aip->active_goal ); ai_add_goal_sub_sexp( sexp, type, aip, &aip->goals[gindex], Ships[aip->shipnum].ship_name ); } @@ -1637,7 +1638,7 @@ void ai_add_wing_goal_sexp(int sexp, ai_goal_type type, wing *wingp) if ((wingp->num_waves - wingp->current_wave > 0) || Fred_running) { int gindex; - gindex = ai_goal_find_empty_slot( wingp->ai_goals, -1 ); + gindex = ai_goal_allocate_slot( wingp->ai_goals, -1 ); ai_add_goal_sub_sexp( sexp, type, nullptr, &wingp->ai_goals[gindex], wingp->name ); } } @@ -1660,7 +1661,7 @@ void ai_add_goal_ship_internal( ai_info *aip, int goal_type, char *name, int /* Assertion(strcmp(name, Ships[aip->shipnum].ship_name) != 0, "The goals apply to the actor in ai_add_goal_ship_internal for ship %s, please report to the SCP!", name); // find an empty slot to put this goal in. - gindex = ai_goal_find_empty_slot( aip->goals, aip->active_goal ); + gindex = ai_goal_allocate_slot( aip->goals, aip->active_goal ); aigp = &(aip->goals[gindex]); ai_goal_reset(aigp, true); @@ -1789,7 +1790,7 @@ ai_achievability ai_mission_goal_achievable( int objnum, ai_goal *aigp ) if (!target_ship_entry || !target_ship_entry->has_shipp()) return ai_achievability::NOT_ACHIEVABLE; - // the override flag is now set in the calling function, ai_mission_goal_achievable + // the override flag is now set in the calling function, validate_mission_goals return ai_achievability::ACHIEVABLE; } diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 3c3cda2c890..8982e41b8e9 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -2689,7 +2689,10 @@ int check_sexp_syntax(int node, int desired_return_type, int recursive, int *bad } break; } - Assert(ship_node >= 0); + if (ship_node < 0) { + Warning(LOCATION, "Could not find ship node for operator %s!", Operators[op_index].text.c_str()); + return SEXP_CHECK_INVALID_SHIP; + } if (is_node_value_dynamic(ship_node)) { const int dyn_val_check = check_dynamic_value_node_type(ship_node, true, false); @@ -4034,7 +4037,10 @@ int check_sexp_syntax(int node, int desired_return_type, int recursive, int *bad } p_container = get_sexp_container(Sexp_nodes[node].text); - Assertion(p_container, "Attempt to use unknown container %s. Please report!", Sexp_nodes[node].text); + if (!p_container) { + Warning(LOCATION, "Attempt to use unknown container %s. Please report!", Sexp_nodes[node].text); + return SEXP_CHECK_TYPE_MISMATCH; + } if ((desired_argument_type == OPF_LIST_CONTAINER_NAME && !p_container->is_list()) || (desired_argument_type == OPF_MAP_CONTAINER_NAME && !p_container->is_map())) { @@ -4044,10 +4050,10 @@ int check_sexp_syntax(int node, int desired_return_type, int recursive, int *bad } case OPF_CONTAINER_VALUE: - Assertion(p_container, - "Attempt to check value arg for null container for SEXP operator %d at arg %d. Please report!", - op_const, - argnum); + if (!p_container) { + Warning(LOCATION, "Attempt to check value arg for null container for SEXP operator %s at arg %d. Please report!", Operators[op_index].text.c_str(), argnum); + return SEXP_CHECK_TYPE_MISMATCH; + } z = check_container_value_data_type(op_const, argnum, p_container->type, @@ -4063,9 +4069,10 @@ int check_sexp_syntax(int node, int desired_return_type, int recursive, int *bad if (node_subtype == SEXP_ATOM_CONTAINER_NAME) { // only list containers of strings or map containers with string keys are allowed const auto *p_str_container = get_sexp_container(Sexp_nodes[node].text); - Assertion(p_str_container, - "Attempt to use unknown container %s. Please report!", - Sexp_nodes[node].text); + if (!p_str_container) { + Warning(LOCATION, "Attempt to use unknown container %s. Please report!", Sexp_nodes[node].text); + return SEXP_CHECK_TYPE_MISMATCH; + } const auto &str_container = *p_str_container; if (str_container.is_list() && none(str_container.type & ContainerType::STRING_DATA)) { From 246213a4bef19e2914a426e3a41fc5769ad5d858 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 13 May 2026 00:44:07 -0400 Subject: [PATCH 42/65] first part of comm node fix Fix #4089, part 1: - give the `my_replacement` and `i_replace` fields clearer names - for submodels with the `Is_damaged` flag (representing -destroyed variants or debris), initialize them to blown-off (the true fix for #3078, fixing a regression introduced in the model instance refactor) - roll back the de-parenting of live debris from #3089 (726ce953c73a20ba8ad6de2ea8e322e947cdd61a), because it was a wrong fix to the initialization bug and introduced its own subtle bug when positioning live debris - roll back the `Can_move` flag skipping from #4138 (063d7afcfe230ad790d4ea55d0a9667bd1659fda), because it was another wrong fix to the initialization bug - add some clarifying comments --- code/model/model.h | 8 ++--- code/model/modelread.cpp | 60 +++++++++++++++++++++---------------- code/model/modelreplace.cpp | 4 +-- 3 files changed, 41 insertions(+), 31 deletions(-) diff --git a/code/model/model.h b/code/model/model.h index 3cff8481624..fee1de9bb3b 100644 --- a/code/model/model.h +++ b/code/model/model.h @@ -136,7 +136,7 @@ struct submodel_instance float shift_accel = 0.0f; TIMESTAMP stepped_translation_started; - bool blown_off = false; // If set, this subobject is blown off + bool blown_off = false; // If set, this subobject is not rendered or used for collision // These fields are the true standard reference for submodel rotation. They should seldom be read directly // and should almost never be written directly. In most cases, coders should prefer cur_angle and prev_angle. @@ -433,7 +433,7 @@ class bsp_info public: bsp_info() : bsp_data_size(0), bsp_data(nullptr), collision_tree_index(-1), - rad(0.0f), my_replacement(-1), i_replace(-1), num_live_debris(0), + rad(0.0f), next_form(-1), prev_form(-1), num_live_debris(0), parent(-1), num_children(0), first_child(-1), next_sibling(-1), num_details(0), outline_buffer(nullptr), n_verts_outline(0), render_sphere_radius(0.0f), use_render_box(0), use_render_sphere(0) { @@ -484,8 +484,8 @@ class bsp_info vec3d max; // The max point of this object's geometry vec3d bounding_box[8]; // calculated fron min/max - int my_replacement; // If not -1 this subobject is what should get rendered instead of this one - int i_replace; // If this is not -1, then this subobject will replace i_replace when it is damaged + int next_form; // If not -1, this submodel can transform into it + int prev_form; // If not -1, another submodel that can transform into this one int num_live_debris; // num live debris models assocaiated with a submodel int live_debris[MAX_LIVE_DEBRIS]; // array of live debris submodels for a submodel diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index 44015e877d0..176ab4307eb 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -3333,22 +3333,20 @@ int model_load(const char* filename, ship_info* sip, ErrorType error_type, bool // Set up the default values for (i=0; in_models; i++ ) { - pm->submodel[i].my_replacement = -1; // assume nothing replaces this - pm->submodel[i].i_replace = -1; // assume this doesn't replaces anything + pm->submodel[i].next_form = -1; // assume nothing replaces this + pm->submodel[i].prev_form = -1; // assume this doesn't replace anything } // Search for models that have destroyed versions for (i=0; in_models; i++ ) { - int j; char destroyed_name[128]; - strcpy_s( destroyed_name, pm->submodel[i].name ); strcat_s( destroyed_name, "-destroyed" ); - for (j=0; jn_models; j++ ) { - if ( !stricmp( pm->submodel[j].name, destroyed_name )) { - pm->submodel[i].my_replacement = j; - pm->submodel[j].i_replace = i; - } + + int j = find_item_with_string(pm->submodel.get(), i2sz(pm->n_models), &bsp_info::name, destroyed_name); + if (j >= 0) { + pm->submodel[i].next_form = j; + pm->submodel[j].prev_form = i; } // Search for models with live debris @@ -3366,12 +3364,8 @@ int model_load(const char* filename, ship_info* sip, ErrorType error_type, bool Assert(pm->submodel[i].num_live_debris < MAX_LIVE_DEBRIS); pm->submodel[i].live_debris[pm->submodel[i].num_live_debris++] = j; pm->submodel[j].flags.set(Model::Submodel_flags::Is_live_debris); - - // make sure live debris doesn't have a parent - pm->submodel[j].parent = -1; } } - } // maybe generate vertex buffers @@ -3554,9 +3548,17 @@ int model_create_instance(int objnum, int model_num) } pmi->id = open_slot; - if (pm->n_models > 0) + if (pm->n_models > 0) { pmi->submodel = new submodel_instance[pm->n_models]; + // "damaged" submodels (like -destroyed variants, or debris) are blown-off by default + for (int i = 0; i < pm->n_models; i++) { + if (pm->submodel[i].flags[Model::Submodel_flags::Is_damaged]) { + pmi->submodel[i].blown_off = true; + } + } + } + // add intrinsic_motion instances if this model is intrinsic-moving if (pm->flags & PM_FLAG_HAS_INTRINSIC_MOTION) { intrinsic_motion motion(objnum >= 0, open_slot); @@ -4836,8 +4838,9 @@ void model_get_moving_submodel_list(SCP_vector &submodel_vector, const obje const auto& child_submodel = pm->submodel[submodel]; const auto& child_submodel_instance = pmi->submodel[submodel]; - // Don't check it or its children if it is destroyed or it is a replacement (non-moving) - if (child_submodel.flags[Model::Submodel_flags::No_collisions] || child_submodel_instance.blown_off || child_submodel.i_replace != -1) { + // Don't check it or its children if it is destroyed or it is a replacement + // (we currently assume replacements are -destroyed versions of submodels that might otherwise move) + if (child_submodel.flags[Model::Submodel_flags::No_collisions] || child_submodel_instance.blown_off || child_submodel.prev_form != -1) { skipChildren = true; return; } @@ -4964,7 +4967,10 @@ void model_set_up_techroom_instance(ship_info *sip, int model_instance_num) model_iterate_submodel_tree(pm, pm->detail[0], [&](int submodel, int /*level*/, bool /*isLeaf*/) { - model_replicate_submodel_instance(pm, pmi, submodel, empty); + auto sm = &pm->submodel[submodel]; + + if (sm->flags[Model::Submodel_flags::Can_move]) + model_replicate_submodel_instance(pm, pmi, submodel, empty); }); } @@ -4985,17 +4991,21 @@ void model_replicate_submodel_instance_sub(polymodel *pm, polymodel_instance *pm submodel_instance *smi = &pmi->submodel[submodel_num]; bsp_info *sm = &pm->submodel[submodel_num]; - - // Set the "blown out" flags. + + // Set the "blown off" flags if ( flags[Ship::Subsystem_Flags::No_disappear] ) { smi->blown_off = false; } else if ( copy_from ) { smi->blown_off = copy_from->blown_off; } + // In the future, we could expand the submodel instance to have a "blown_off_index" + // to indicate which form is currently visible, but for now, we'll follow the retail + // convention of having just two forms, the second of which is opposite from the first. + if ( smi->blown_off ) { - if ( sm->my_replacement >= 0 && !(flags[Ship::Subsystem_Flags::No_replace]) ) { - auto r_smi = &pmi->submodel[sm->my_replacement]; + if ( sm->next_form >= 0 && !(flags[Ship::Subsystem_Flags::No_replace]) ) { + auto r_smi = &pmi->submodel[sm->next_form]; r_smi->blown_off = false; if ( copy_from ) { r_smi->cur_angle = copy_from->cur_angle; @@ -5016,10 +5026,10 @@ void model_replicate_submodel_instance_sub(polymodel *pm, polymodel_instance *pm } } } else { - // If submodel isn't yet blown off and has a -destroyed replacement model, we prevent - // the replacement model from being drawn by marking it as having been blown off - if ( sm->my_replacement >= 0 && sm->my_replacement != submodel_num) { - auto r_smi = &pmi->submodel[sm->my_replacement]; + // If submodel isn't yet blown off and has a next form (like a -destroyed replacement model), + // we prevent the replacement model from being drawn by marking it as having been blown off + if ( sm->next_form >= 0 && sm->next_form != submodel_num) { + auto r_smi = &pmi->submodel[sm->next_form]; r_smi->blown_off = true; } } diff --git a/code/model/modelreplace.cpp b/code/model/modelreplace.cpp index f929b997337..d8c5c1e5fa3 100644 --- a/code/model/modelreplace.cpp +++ b/code/model/modelreplace.cpp @@ -267,11 +267,11 @@ CHANGE_HELPER(change_submodel_numbers, bsp_info, int) for (auto& detail : input.details) REPLACE_IF_EQ(detail); REPLACE_IF_EQ(input.first_child); - REPLACE_IF_EQ(input.i_replace); + REPLACE_IF_EQ(input.prev_form); for (auto& debris : input.live_debris) REPLACE_IF_EQ(debris); REPLACE_IF_EQ(input.look_at_submodel); - REPLACE_IF_EQ(input.my_replacement); + REPLACE_IF_EQ(input.next_form); REPLACE_IF_EQ(input.next_sibling); REPLACE_IF_EQ(input.parent); CHANGE_HELPER_END From bed223c5c7b1b2f0ae91c6e9885aa21488d8b4f9 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 13 May 2026 00:53:05 -0400 Subject: [PATCH 43/65] second part of comm node fix Fix #4089, part 2: - properly link special-point subsystems with -destroyed submodels, if such a relationship exists and a game_settings.tbl flag is set - add some helper functions for -destroyed variants of submodels (and now subsystems) --- code/mod_table/mod_table.cpp | 6 ++++++ code/mod_table/mod_table.h | 1 + code/model/model.h | 7 ++++++- code/model/modelread.cpp | 30 ++++++++++++++++++++++++------ code/parse/sexp.cpp | 3 +++ code/ship/shiphit.cpp | 28 ++++++++++++++++++++++++++++ code/ship/shiphit.h | 4 ++++ freespace2/freespace.cpp | 2 +- 8 files changed, 73 insertions(+), 8 deletions(-) diff --git a/code/mod_table/mod_table.cpp b/code/mod_table/mod_table.cpp index 2a7176fd829..dde809ec526 100644 --- a/code/mod_table/mod_table.cpp +++ b/code/mod_table/mod_table.cpp @@ -184,6 +184,7 @@ float Shield_percent_skips_damage; float Min_radius_for_persistent_debris; bool Zero_radius_explosions_skip_fireballs; bool Render_insignias_as_decals; +bool Link_subsystems_to_destroyed_submodels; #ifdef WITH_DISCORD @@ -1649,6 +1650,10 @@ void parse_mod_table(const char *filename) stuff_boolean(&Zero_radius_explosions_skip_fireballs); } + if (optional_string("$Link subsystems to -destroyed submodels:")) { + stuff_boolean(&Link_subsystems_to_destroyed_submodels); + } + // end of options ---------------------------------------- // if we've been through once already and are at the same place, force a move @@ -1897,6 +1902,7 @@ void mod_table_reset() Min_radius_for_persistent_debris = 50.0f; Zero_radius_explosions_skip_fireballs = false; Render_insignias_as_decals = false; + Link_subsystems_to_destroyed_submodels = false; } void mod_table_set_version_flags() diff --git a/code/mod_table/mod_table.h b/code/mod_table/mod_table.h index af0470548fd..433461d63d8 100644 --- a/code/mod_table/mod_table.h +++ b/code/mod_table/mod_table.h @@ -206,6 +206,7 @@ extern float Shield_percent_skips_damage; extern float Min_radius_for_persistent_debris; extern bool Zero_radius_explosions_skip_fireballs; extern bool Render_insignias_as_decals; +extern bool Link_subsystems_to_destroyed_submodels; void mod_table_init(); void mod_table_post_process(); diff --git a/code/model/model.h b/code/model/model.h index fee1de9bb3b..109db237da6 100644 --- a/code/model/model.h +++ b/code/model/model.h @@ -1137,7 +1137,6 @@ extern int model_find_2d_bound_min(int model_num,matrix *orient, vec3d * pos,int // rect. int submodel_find_2d_bound_min(int model_num,int submodel, matrix *orient, vec3d * pos,int *x1, int *y1, int *x2, int *y2); - // Returns zero is x1,y1,x2,y2 are valid // Returns 2 for point offscreen. // note that x1,y1,x2,y2 aren't clipped to 2d screen coordinates! @@ -1145,6 +1144,12 @@ int submodel_find_2d_bound_min(int model_num,int submodel, matrix *orient, vec3d // bounding box won't change depending on the obj's orient. int subobj_find_2d_bound(float radius, matrix *orient, vec3d * pos,int *x1, int *y1, int *x2, int *y2); +// Returns the index of the -destroyed version of a submodel name, if it exists +int submodel_find_destroyed_form(int model_num, const char *name_stem); + +// Returns whether this submodel name is a -destroyed version +bool submodel_is_destroyed_form(const char *name); + // stats variables #ifndef NDEBUG extern int modelstats_num_polys; diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index 176ab4307eb..c25752e9d69 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -2088,7 +2088,7 @@ modelread_status read_model_file_no_subsys(polymodel * pm, const char* filename, sm->flags.set(Model::Submodel_flags::Nocollide_this_only); } - sm->flags.set(Model::Submodel_flags::Is_damaged, in(sm->name, "-destroyed")); + sm->flags.set(Model::Submodel_flags::Is_damaged, submodel_is_destroyed_form(sm->name)); break; } @@ -3339,11 +3339,7 @@ int model_load(const char* filename, ship_info* sip, ErrorType error_type, bool // Search for models that have destroyed versions for (i=0; in_models; i++ ) { - char destroyed_name[128]; - strcpy_s( destroyed_name, pm->submodel[i].name ); - strcat_s( destroyed_name, "-destroyed" ); - - int j = find_item_with_string(pm->submodel.get(), i2sz(pm->n_models), &bsp_info::name, destroyed_name); + int j = submodel_find_destroyed_form(pm->id, pm->submodel[i].name); if (j >= 0) { pm->submodel[i].next_form = j; pm->submodel[j].prev_form = i; @@ -3979,6 +3975,28 @@ int subobj_find_2d_bound(float radius ,matrix * /*orient*/, vec3d * pos,int *x1, return 0; } +int submodel_find_destroyed_form(int model_num, const char *name_stem) +{ + const auto pm = model_get(model_num); + Assertion(pm, "model_num must be valid!"); + + SCP_string destroyed_name(name_stem); + destroyed_name += "-destroyed"; + + return find_item_with_string(pm->submodel.get(), i2sz(pm->n_models), &bsp_info::name, destroyed_name.c_str()); +} + +bool submodel_is_destroyed_form(const char *name) +{ + constexpr auto suffix = "-destroyed"; + constexpr auto suffix_len = std::char_traits::length(suffix); + + auto len = strlen(name); + if (len <= suffix_len) + return false; + + return stricmp(name + len - suffix_len, suffix) == 0; +} // Given a rotating submodel, find the local and world axes of rotation. void model_get_rotating_submodel_axis(vec3d *model_axis, vec3d *world_axis, const polymodel *pm, const polymodel_instance *pmi, int submodel_num, const matrix *objorient) diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 3c3cda2c890..7d97160754a 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -15755,6 +15755,9 @@ void set_subsys_strength_and_maybe_ancestors(ship *shipp, ship_subsys *ss, polym if (ss->submodel_instance_2) ss->submodel_instance_2->blown_off = false; + // special case for subsystems that don't correspond to a submodel + check_subsystem_submodel_link(shipp, ss, false); + // see if we are handling ancestors and if this subsystem has a submodel int subobj = ss->system_info->subobj_num; if (repair_ancestors && subobj >= 0) diff --git a/code/ship/shiphit.cpp b/code/ship/shiphit.cpp index 541a3b9c272..0c7d936f1ce 100755 --- a/code/ship/shiphit.cpp +++ b/code/ship/shiphit.cpp @@ -114,6 +114,31 @@ static bool is_subsys_destroyed(ship *shipp, int submodel) return false; } +void check_subsystem_submodel_link(const ship *shipp, const ship_subsys *subsys, bool was_destroyed) +{ + if (!Link_subsystems_to_destroyed_submodels) + return; + + Assertion(shipp && subsys, "the ship and subsystem must exist!"); + auto pmi = model_get_instance(shipp->model_instance_num); + Assertion(pmi, "the ship's model instance must exist!"); + + // check subsystem-submodel link, but only for special-point subsystems + // (not subsystems corresponding to a submodel) + auto psub = subsys->system_info; + if (psub->subobj_num >= 0) + return; + int j = submodel_find_destroyed_form(pmi->model_num, psub->subobj_name); + if (j < 0) + return; + + // show the submodel, or not, depending on what happened to the subsystem + if (was_destroyed) + pmi->submodel[j].blown_off = false; + else + pmi->submodel[j].blown_off = true; +} + // do_subobj_destroyed_stuff is called when a subobject for a ship is killed. Separated out // to separate function on 10/15/97 by MWA for easy multiplayer access. It does all of the // cool things like blowing off the model (if applicable, writing the logs, etc) @@ -346,6 +371,9 @@ void do_subobj_destroyed_stuff( ship *ship_p, ship_subsys *subsys, const vec3d* if ((psub->subobj_num != psub->turret_gun_sobj) && (psub->turret_gun_sobj >= 0)) { subsys->submodel_instance_2->blown_off = true; } + + // special case for subsystems that don't correspond to a submodel + check_subsystem_submodel_link(ship_p, subsys, true); } if (notify && !no_explosion) { diff --git a/code/ship/shiphit.h b/code/ship/shiphit.h index ba7b2f6292a..e2d6ee3542a 100644 --- a/code/ship/shiphit.h +++ b/code/ship/shiphit.h @@ -32,6 +32,10 @@ constexpr float DEATHROLL_ROTVEL_CAP = 6.3f; // maximum added deathroll rotve // of whoever is calling these functions. These functions are strictly // for damaging ship's hulls, shields, and subsystems. Nothing more. +// handle a -destroyed submodel in the special case where it is related to a special-point subsystem +// (the usual submodel-submodel case is handled through the normal code paths) +void check_subsystem_submodel_link(const ship *shipp, const ship_subsys *subsys, bool was_destroyed); + // function to destroy a subsystem. Called internally and from multiplayer messaging code extern void do_subobj_destroyed_stuff( ship *ship_p, ship_subsys *subsys, const vec3d *hitpos, bool no_explosion = false ); diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index 10de4806fae..b983df5bf5b 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -6732,7 +6732,7 @@ void game_spew_pof_info_sub(int model_num, polymodel *pm, int sm, CFILE *out, in // find the # of faces for this _individual_ object total = submodel_get_num_polys(model_num, sm); - if(strstr(pm->submodel[sm].name, "-destroyed")){ + if (submodel_is_destroyed_form(pm->submodel[sm].name)) { sub_total_destroyed = total; } From 8f916174bef3d2ce1d1bd7a884616a7b750dcac0 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Thu, 14 May 2026 03:30:53 -0400 Subject: [PATCH 44/65] clang --- code/model/modelread.cpp | 2 +- code/ship/shiphit.cpp | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index c25752e9d69..95de185af92 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -3983,7 +3983,7 @@ int submodel_find_destroyed_form(int model_num, const char *name_stem) SCP_string destroyed_name(name_stem); destroyed_name += "-destroyed"; - return find_item_with_string(pm->submodel.get(), i2sz(pm->n_models), &bsp_info::name, destroyed_name.c_str()); + return find_item_with_string(pm->submodel.get(), i2sz(pm->n_models), &bsp_info::name, destroyed_name); } bool submodel_is_destroyed_form(const char *name) diff --git a/code/ship/shiphit.cpp b/code/ship/shiphit.cpp index 0c7d936f1ce..96fb5ddac76 100755 --- a/code/ship/shiphit.cpp +++ b/code/ship/shiphit.cpp @@ -133,10 +133,7 @@ void check_subsystem_submodel_link(const ship *shipp, const ship_subsys *subsys, return; // show the submodel, or not, depending on what happened to the subsystem - if (was_destroyed) - pmi->submodel[j].blown_off = false; - else - pmi->submodel[j].blown_off = true; + pmi->submodel[j].blown_off = !was_destroyed; } // do_subobj_destroyed_stuff is called when a subobject for a ship is killed. Separated out From 2c62cb2089f51ca2d4e1f2815c1ed7574c506f93 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Thu, 14 May 2026 06:53:05 -0500 Subject: [PATCH 45/65] ship alt class stylization pass (#7435) --- .../ShipEditor/ShipAltShipClassModel.cpp | 66 ++++++++++--------- .../ShipEditor/ShipAltShipClassModel.h | 45 +++++-------- .../dialogs/ShipEditor/ShipAltShipClass.cpp | 65 ++++++++++-------- .../ui/dialogs/ShipEditor/ShipAltShipClass.h | 36 ++++------ 4 files changed, 103 insertions(+), 109 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.cpp index fb2f816bf1a..a7b2c6df357 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.cpp @@ -1,7 +1,9 @@ #include "ShipAltShipClassModel.h" #include "ship/ship.h" + namespace fso::fred::dialogs { + ShipAltShipClassModel::ShipAltShipClassModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) { @@ -11,7 +13,7 @@ ShipAltShipClassModel::ShipAltShipClassModel(QObject* parent, EditorViewport* vi bool ShipAltShipClassModel::apply() { // TODO: Add extra validation here - for (auto& pool_class : alt_class_pool) { + for (auto& pool_class : _altClassPool) { if (pool_class.ship_class == -1 && pool_class.variable_index == -1) { _viewport->dialogProvider->showButtonDialog(DialogType::Warning, "Warning", @@ -20,48 +22,48 @@ bool ShipAltShipClassModel::apply() return false; } } - for (int i = 0; i < _num_selected_ships; i++) { - Ships[_m_selected_ships[i]].s_alt_classes = alt_class_pool; + for (int i = 0; i < _numSelectedShips; i++) { + Ships[_selectedShips[i]].s_alt_classes = _altClassPool; } return true; } void ShipAltShipClassModel::reject() {} -SCP_vector ShipAltShipClassModel::get_pool() const +SCP_vector ShipAltShipClassModel::getPool() const { - return alt_class_pool; + return _altClassPool; } -SCP_vector> ShipAltShipClassModel::get_classes() +SCP_vector> ShipAltShipClassModel::getClasses() { // Fill the ship classes combo box - SCP_vector> _m_set_from_ship_class; + SCP_vector> classPool; std::pair classData; // Add the default entry if we need one followed by all the ship classes - classData.first = "Set From Variable"; - classData.second = -1; - _m_set_from_ship_class.push_back(classData); + classData.first = "Set From Variable"; + classData.second = -1; + classPool.push_back(classData); for (auto it = Ship_info.cbegin(); it != Ship_info.cend(); ++it) { - if (!(it->flags[Ship::Info_Flags::Player_ship])) { + if (!(it->flags[Ship::Info_Flags::Player_ship])) { continue; } classData.first = it->name; classData.second = std::distance(Ship_info.cbegin(), it); - _m_set_from_ship_class.push_back(classData); + classPool.push_back(classData); } - return _m_set_from_ship_class; + return classPool; } -SCP_vector> ShipAltShipClassModel::get_variables() +SCP_vector> ShipAltShipClassModel::getVariables() { // Fill the variable combo box - SCP_vector> _m_set_from_variables; + SCP_vector> variablePool; std::pair variableData; variableData.first = "Set From Ship Class"; variableData.second = -1; - _m_set_from_variables.push_back(variableData); + variablePool.push_back(variableData); for (int i = 0; i < MAX_SEXP_VARIABLES; i++) { if (Sexp_variables[i].type & SEXP_VARIABLE_STRING) { std::ostringstream oss; @@ -70,46 +72,46 @@ SCP_vector> ShipAltShipClassModel::get_variables() buff = oss.str(); variableData.first = buff; variableData.second = i; - _m_set_from_variables.push_back(variableData); - //_string_variables.push_back(variable); - // _string_variables[0].get().type = 1234; + variablePool.push_back(variableData); } } - return _m_set_from_variables; + return variablePool; } -void ShipAltShipClassModel::sync_data(const SCP_vector& new_pool) { - if (new_pool == alt_class_pool) { + +void ShipAltShipClassModel::syncData(const SCP_vector& newPool) +{ + if (newPool == _altClassPool) { return; } else { - alt_class_pool = new_pool; + _altClassPool = newPool; set_modified(); } } + void ShipAltShipClassModel::initializeData() { - _num_selected_ships = 0; - _m_selected_ships.clear(); + _numSelectedShips = 0; + _selectedShips.clear(); // have we got multiple selected ships? object* objp = GET_FIRST(&obj_used_list); while (objp != END_OF_LIST(&obj_used_list)) { if ((objp->type == OBJ_START) || (objp->type == OBJ_SHIP)) { if (objp->flags[Object::Object_Flags::Marked]) { - _m_selected_ships.push_back(objp->instance); - _num_selected_ships++; + _selectedShips.push_back(objp->instance); + _numSelectedShips++; } } objp = GET_NEXT(objp); } - Assertion(_num_selected_ships > 0, "No Ships Selected"); - // Assert(Objects[cur_object_index].flags[Object::Object_Flags::Marked]); + Assertion(_numSelectedShips > 0, "No Ships Selected"); - alt_class_pool.clear(); + _altClassPool.clear(); objp = GET_FIRST(&obj_used_list); while (objp != END_OF_LIST(&obj_used_list)) { if ((objp->type == OBJ_START) || (objp->type == OBJ_SHIP)) { if (objp->flags[Object::Object_Flags::Marked]) { - alt_class_pool = Ships[objp->instance].s_alt_classes; + _altClassPool = Ships[objp->instance].s_alt_classes; break; } } @@ -118,4 +120,4 @@ void ShipAltShipClassModel::initializeData() _modified = false; } -} // namespace fso::fred::dialogs \ No newline at end of file +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.h index b9ac7f1c4fb..7831dc58ecd 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.h @@ -1,38 +1,29 @@ #pragma once -#include "../AbstractDialogModel.h" -namespace fso::fred::dialogs { -/** - * @brief Model for QtFRED's Alt Ship Class dialog - */ -class ShipAltShipClassModel : public AbstractDialogModel { - private: - /** - * @brief Initialises data for the model - */ - void initializeData(); - SCP_vector alt_class_pool; - - int _num_selected_ships = 0; - - SCP_vector _m_selected_ships; +#include "../AbstractDialogModel.h" - SCP_vector _m_alt_class_list; +namespace fso::fred::dialogs { +class ShipAltShipClassModel : public AbstractDialogModel { + Q_OBJECT public: - /** - * @brief Constructor - * @param [in] parent The parent dialog. - * @param [in] viewport The viewport this dialog is attacted to. - */ ShipAltShipClassModel(QObject* parent, EditorViewport* viewport); bool apply() override; void reject() override; - SCP_vector get_pool() const; + SCP_vector getPool() const; + + static SCP_vector> getClasses(); + static SCP_vector> getVariables(); + void syncData(const SCP_vector& newPool); - static SCP_vector> get_classes(); - static SCP_vector> get_variables(); - void sync_data(const SCP_vector&); + private: // NOLINT(readability-redundant-access-specifiers) + void initializeData(); + + SCP_vector _altClassPool; + int _numSelectedShips = 0; + SCP_vector _selectedShips; + SCP_vector _altClassList; }; -} // namespace fso::fred::dialogs \ No newline at end of file + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.cpp index 59d886dd8d6..2e3bfc60547 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.cpp @@ -9,6 +9,7 @@ #include namespace fso::fred::dialogs { + ShipAltShipClass::ShipAltShipClass(QDialog* parent, EditorViewport* viewport) : QDialog(parent), ui(new Ui::ShipAltShipClass()), _model(new ShipAltShipClassModel(this, viewport)), _viewport(viewport) @@ -21,8 +22,9 @@ ShipAltShipClass::ShipAltShipClass(QDialog* parent, EditorViewport* viewport) ShipAltShipClass::~ShipAltShipClass() = default; void ShipAltShipClass::accept() -{ // If apply() returns true, close the dialog - sync_data(); +{ + // If apply() returns true, close the dialog + syncData(); if (_model->apply()) { QDialog::accept(); } @@ -30,10 +32,11 @@ void ShipAltShipClass::accept() } void ShipAltShipClass::reject() -{ // Asks the user if they want to save changes, if any +{ + // Asks the user if they want to save changes, if any // If they do, it runs _model->apply() and returns the success value // If they don't, it runs _model->reject() and returns true - sync_data(); + syncData(); if (rejectOrCloseHandler(this, _model.get(), _viewport)) { QDialog::reject(); // actually close } @@ -50,6 +53,7 @@ void ShipAltShipClass::on_buttonBox_accepted() { accept(); } + void ShipAltShipClass::on_buttonBox_rejected() { reject(); @@ -57,7 +61,7 @@ void ShipAltShipClass::on_buttonBox_rejected() void ShipAltShipClass::on_addButton_clicked() { - auto item = generate_item(ui->shipCombo->currentData(Qt::UserRole).toInt(), + auto item = generateItem(ui->shipCombo->currentData(Qt::UserRole).toInt(), ui->variableCombo->currentData(Qt::UserRole).toInt(), ui->defaultCheckbox->isChecked()); if (item != nullptr) { @@ -69,7 +73,7 @@ void ShipAltShipClass::on_insertButton_clicked() { auto current = ui->classList->currentIndex(); if (current.isValid()) { - auto item = generate_item(ui->shipCombo->currentData(Qt::UserRole).toInt(), + auto item = generateItem(ui->shipCombo->currentData(Qt::UserRole).toInt(), ui->variableCombo->currentData(Qt::UserRole).toInt(), ui->defaultCheckbox->isChecked()); if (item != nullptr) { @@ -129,7 +133,7 @@ void ShipAltShipClass::on_shipCombo_currentIndexChanged(int index) on_variableCombo_currentIndexChanged(1); } } else { - QString classname = generate_name(ui->shipCombo->itemData(index, Qt::UserRole).toInt(), -1); + QString classname = generateName(ui->shipCombo->itemData(index, Qt::UserRole).toInt(), -1); ui->classList->model()->setData(current, classname, Qt::DisplayRole); ui->classList->model()->setData(current, ui->shipCombo->itemData(index, Qt::UserRole), Qt::UserRole + 1); ui->classList->model()->setData(current, -1, Qt::UserRole + 2); @@ -152,7 +156,7 @@ void ShipAltShipClass::on_variableCombo_currentIndexChanged(int index) } else { int ship_class = ship_info_lookup(Sexp_variables[ui->variableCombo->itemData(index, Qt::UserRole).toInt()].text); - QString classname = generate_name(ship_class, ui->variableCombo->itemData(index, Qt::UserRole).toInt()); + QString classname = generateName(ship_class, ui->variableCombo->itemData(index, Qt::UserRole).toInt()); ui->classList->model()->setData(current, classname, Qt::DisplayRole); ui->classList->model()->setData(current, ship_class, Qt::UserRole + 1); ui->classList->model()->setData(current, @@ -171,34 +175,35 @@ void ShipAltShipClass::on_defaultCheckbox_toggled(bool toggled) ui->classList->model()->setData(current, toggled, Qt::UserRole); } } + void ShipAltShipClass::initUI() { - alt_pool = new QStandardItemModel(); - for (auto& alt_class : _model->get_pool()) { - auto item = generate_item(alt_class.ship_class, alt_class.variable_index, alt_class.default_to_this_class); + _altPool = new QStandardItemModel(); + for (auto& alt_class : _model->getPool()) { + auto item = generateItem(alt_class.ship_class, alt_class.variable_index, alt_class.default_to_this_class); if (item != nullptr) { - alt_pool->appendRow(item); + _altPool->appendRow(item); } } - ui->classList->setModel(alt_pool); + ui->classList->setModel(_altPool); connect(ui->classList->selectionModel(), &QItemSelectionModel::currentChanged, this, &ShipAltShipClass::classListChanged); auto ship_pool = new QStandardItemModel(); - for (auto& ship : _model->get_classes()) { - QString classname = ship.first.c_str(); + for (auto& shipClass : _model->getClasses()) { + QString classname = shipClass.first.c_str(); auto item = new QStandardItem(classname); - item->setData(ship.second, Qt::UserRole); + item->setData(shipClass.second, Qt::UserRole); ship_pool->appendRow(item); } auto shipproxyModel = new InverseSortFilterProxyModel(this); shipproxyModel->setSourceModel(ship_pool); ui->shipCombo->setModel(shipproxyModel); auto variable_pool = new QStandardItemModel(); - for (auto& variable : _model->get_variables()) { + for (auto& variable : _model->getVariables()) { QString classname = variable.first.c_str(); auto item = new QStandardItem(classname); item->setData(variable.second, Qt::UserRole); @@ -214,10 +219,10 @@ void ShipAltShipClass::initUI() ui->downButton->setText(QString()); ui->downButton->setToolTip(tr("Move selected class down")); - updateUI(); + updateUi(); } -void ShipAltShipClass::updateUI() +void ShipAltShipClass::updateUi() { util::SignalBlockers blockers(this); // block signals while we set up the UI auto current = ui->classList->currentIndex(); @@ -279,17 +284,19 @@ void ShipAltShipClass::updateUI() } ui->defaultCheckbox->setChecked(default_ship); } + void ShipAltShipClass::classListChanged(const QModelIndex& current) { SCP_UNUSED(current); - updateUI(); + updateUi(); } -QStandardItem* ShipAltShipClass::generate_item(const int classid, const int variable, const bool default_ship) + +QStandardItem* ShipAltShipClass::generateItem(int classid, int variable, bool defaultShip) { - QString classname = generate_name(classid, variable); + QString classname = generateName(classid, variable); if (!classname.isEmpty()) { auto item = new QStandardItem(classname); - item->setData(default_ship, Qt::UserRole); + item->setData(defaultShip, Qt::UserRole); item->setData(classid, Qt::UserRole + 1); item->setData(variable, Qt::UserRole + 2); return item; @@ -301,7 +308,8 @@ QStandardItem* ShipAltShipClass::generate_item(const int classid, const int vari return nullptr; } } -QString ShipAltShipClass::generate_name(const int classid, const int variable) + +QString ShipAltShipClass::generateName(int classid, int variable) { QString classname; if (variable != -1) { @@ -322,7 +330,9 @@ QString ShipAltShipClass::generate_name(const int classid, const int variable) } return classname; } -void ShipAltShipClass::sync_data() { + +void ShipAltShipClass::syncData() +{ SCP_vector new_pool; int n = ui->classList->model()->rowCount(); for (int i = 0; i < n; i++) { @@ -335,12 +345,15 @@ void ShipAltShipClass::sync_data() { dynamic_cast(ui->classList->model())->index(i, 0).data(Qt::UserRole + 2).toInt(); new_pool.push_back(new_list_item); } - _model->sync_data(new_pool); + _model->syncData(new_pool); } + InverseSortFilterProxyModel::InverseSortFilterProxyModel(QObject* parent) : QSortFilterProxyModel(parent) {} + bool InverseSortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { bool accept = QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); return !accept; } + } // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.h b/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.h index 3def433be6b..5d04ed106e6 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.h @@ -1,16 +1,16 @@ #pragma once + #include #include #include -#include +#include + namespace fso::fred::dialogs { + namespace Ui { class ShipAltShipClass; } -/** - * @brief QtFRED's Alternate Ship Class Editor - */ class InverseSortFilterProxyModel : public QSortFilterProxyModel { Q_OBJECT @@ -19,16 +19,11 @@ class InverseSortFilterProxyModel : public QSortFilterProxyModel { protected: bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; - }; + class ShipAltShipClass : public QDialog { Q_OBJECT public: - /** - * @brief Constructor - * @param [in] parent The parent dialog. - * @param [in] viewport The viewport this dialog is attatched to. - */ explicit ShipAltShipClass(QDialog* parent, EditorViewport* viewport); ~ShipAltShipClass() override; @@ -36,10 +31,6 @@ class ShipAltShipClass : public QDialog { void reject() override; protected: - /** - * @brief Overides the Dialogs Close event to add a confermation dialog - * @param [in] *e The event. - */ void closeEvent(QCloseEvent*) override; private: @@ -48,19 +39,15 @@ class ShipAltShipClass : public QDialog { EditorViewport* _viewport; void initUI(); + void updateUi(); - /** - * @brief Populates the UI - */ - void updateUI(); - - QStandardItemModel* alt_pool; + QStandardItemModel* _altPool; void classListChanged(const QModelIndex& current); - static QStandardItem* generate_item(const int classid, const int variable, const bool default_ship); - static QString generate_name(const int classid, const int variable); + static QStandardItem* generateItem(int classid, int variable, bool defaultShip); + static QString generateName(int classid, int variable); - void sync_data(); + void syncData(); private slots: // NOLINT(readability-redundant-access-specifiers) void on_buttonBox_accepted(); @@ -74,4 +61,5 @@ class ShipAltShipClass : public QDialog { void on_variableCombo_currentIndexChanged(int); void on_defaultCheckbox_toggled(bool); }; -} // namespace fso::fred::dialogs \ No newline at end of file + +} // namespace fso::fred::dialogs From 05619eba9d65a1227eae0acd51fec517894a3548 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Thu, 14 May 2026 06:53:14 -0500 Subject: [PATCH 46/65] ship custom warp stylization pass (#7436) --- .../ShipEditor/ShipCustomWarpDialogModel.cpp | 189 ++++++++++-------- .../ShipEditor/ShipCustomWarpDialogModel.h | 128 +++--------- .../ShipEditor/ShipCustomWarpDialog.cpp | 10 +- .../dialogs/ShipEditor/ShipCustomWarpDialog.h | 24 +-- 4 files changed, 140 insertions(+), 211 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.cpp index e9305c360fd..8371c4838f1 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.cpp @@ -1,9 +1,11 @@ #include "ShipCustomWarpDialogModel.h" #include "ship/shipfx.h" + namespace fso::fred::dialogs { + ShipCustomWarpDialogModel::ShipCustomWarpDialogModel(QObject* parent, EditorViewport* viewport, bool departure) - : AbstractDialogModel(parent, viewport), _m_departure(departure), _target(Target::Selection) + : AbstractDialogModel(parent, viewport), _departure(departure), _target(Target::Selection) { initializeData(); } @@ -13,7 +15,7 @@ ShipCustomWarpDialogModel::ShipCustomWarpDialogModel(QObject* parent, bool departure, Target target, int wingIndex) - : AbstractDialogModel(parent, viewport), _m_departure(departure), _target(target), _wingIndex(wingIndex) + : AbstractDialogModel(parent, viewport), _departure(departure), _target(target), _wingIndex(wingIndex) { initializeData(); } @@ -21,53 +23,53 @@ ShipCustomWarpDialogModel::ShipCustomWarpDialogModel(QObject* parent, bool ShipCustomWarpDialogModel::apply() { WarpParams params; - params.direction = _m_departure ? WarpDirection::WARP_OUT : WarpDirection::WARP_IN; + params.direction = _departure ? WarpDirection::WARP_OUT : WarpDirection::WARP_IN; - if (_m_warp_type < Num_warp_types) { - params.warp_type = _m_warp_type; + if (_warpType < Num_warp_types) { + params.warp_type = _warpType; } else { - params.warp_type = (_m_warp_type - Num_warp_types) | WT_DEFAULT_WITH_FIREBALL; + params.warp_type = (_warpType - Num_warp_types) | WT_DEFAULT_WITH_FIREBALL; } - if (!_m_start_sound.empty()) { - gamesnd_id id = gamesnd_get_by_name(_m_start_sound.c_str()); + if (!_startSound.empty()) { + gamesnd_id id = gamesnd_get_by_name(_startSound.c_str()); if (id.value() == -1) { - Warning(LOCATION, "Game Sound \"%s\" does not exist. Skipping", _m_start_sound.c_str()); + Warning(LOCATION, "Game Sound \"%s\" does not exist. Skipping", _startSound.c_str()); } else { params.snd_start = id; } } - if (!_m_end_sound.empty()) { - gamesnd_id id = gamesnd_get_by_name(_m_end_sound.c_str()); + if (!_endSound.empty()) { + gamesnd_id id = gamesnd_get_by_name(_endSound.c_str()); if (id.value() == -1) { - Warning(LOCATION, "Game Sound \"%s\" does not exist. Skipping", _m_end_sound.c_str()); + Warning(LOCATION, "Game Sound \"%s\" does not exist. Skipping", _endSound.c_str()); } else { params.snd_end = id; } } - if (_m_departure && _m_warpout_engage_time) { - params.warpout_engage_time = fl2i(_m_warpout_engage_time * 1000.0f); + if (_departure && _warpoutEngageTime) { + params.warpout_engage_time = fl2i(_warpoutEngageTime * 1000.0f); } - if (_m_speed) { - params.speed = _m_speed; + if (_speed) { + params.speed = _speed; } - if (_m_time) { - params.time = fl2i(_m_time * 1000.0f); + if (_time) { + params.time = fl2i(_time * 1000.0f); } - if (_m_accel_exp) { - params.accel_exp = _m_accel_exp; + if (_accelExp) { + params.accel_exp = _accelExp; } - if (_m_radius) { - params.radius = _m_radius; + if (_radius) { + params.radius = _radius; } - if (!_m_anim.empty()) { - strcpy_s(params.anim, _m_anim.c_str()); + if (!_anim.empty()) { + strcpy_s(params.anim, _anim.c_str()); } - params.supercap_warp_physics = _m_supercap_warp_physics; - if (_m_departure && _m_player_warpout_speed) { - params.warpout_player_speed = _m_player_warpout_speed; + params.supercap_warp_physics = _supercapWarpPhysics; + if (_departure && _playerWarpoutSpeed) { + params.warpout_player_speed = _playerWarpoutSpeed; } int index = find_or_add_warp_params(params); @@ -77,7 +79,7 @@ bool ShipCustomWarpDialogModel::apply() auto& sh = Ships[objp->instance]; if (sh.wingnum != _wingIndex) continue; - if (!_m_departure) + if (!_departure) sh.warpin_params_index = index; else sh.warpout_params_index = index; @@ -88,7 +90,7 @@ bool ShipCustomWarpDialogModel::apply() if ((objp->type == OBJ_SHIP) || (objp->type == OBJ_START)) { if (objp->flags[Object::Object_Flags::Marked]) { auto& sh = Ships[objp->instance]; - if (!_m_departure) + if (!_departure) sh.warpin_params_index = index; else sh.warpout_params_index = index; @@ -104,67 +106,67 @@ void ShipCustomWarpDialogModel::reject() {} int ShipCustomWarpDialogModel::getType() const { - return _m_warp_type; + return _warpType; } SCP_string ShipCustomWarpDialogModel::getStartSound() const { - return _m_start_sound; + return _startSound; } SCP_string ShipCustomWarpDialogModel::getEndSound() const { - return _m_end_sound; + return _endSound; } float ShipCustomWarpDialogModel::getEngageTime() const { - return _m_warpout_engage_time; + return _warpoutEngageTime; } float ShipCustomWarpDialogModel::getSpeed() const { - return _m_speed; + return _speed; } float ShipCustomWarpDialogModel::getTime() const { - return _m_time; + return _time; } float ShipCustomWarpDialogModel::getExponent() const { - return _m_accel_exp; + return _accelExp; } float ShipCustomWarpDialogModel::getRadius() const { - return _m_radius; + return _radius; } SCP_string ShipCustomWarpDialogModel::getAnim() const { - return _m_anim; + return _anim; } bool ShipCustomWarpDialogModel::getSupercap() const { - return _m_supercap_warp_physics; + return _supercapWarpPhysics; } float ShipCustomWarpDialogModel::getPlayerSpeed() const { - return _m_player_warpout_speed; + return _playerWarpoutSpeed; } bool ShipCustomWarpDialogModel::departMode() const { - return _m_departure; + return _departure; } bool ShipCustomWarpDialogModel::isPlayer() const { - return _m_player; + return _player; } void ShipCustomWarpDialogModel::initializeData() @@ -172,17 +174,17 @@ void ShipCustomWarpDialogModel::initializeData() // find the params of the first marked ship WarpParams* params = nullptr; if (_target == Target::Wing && _wingIndex >= 0) { - // Use first ship in the wing for initial values; mark _m_player if the wing contains the player + // Use first ship in the wing for initial values; mark _player if the wing contains the player for (object* objp : list_range(&obj_used_list)) { if ((objp->type == OBJ_SHIP) || (objp->type == OBJ_START)) { const auto& sh = Ships[objp->instance]; if (sh.wingnum == _wingIndex) { - if (!_m_departure) + if (!_departure) params = &Warp_params[sh.warpin_params_index]; else params = &Warp_params[sh.warpout_params_index]; if (objp->type == OBJ_START) - _m_player = true; + _player = true; break; } } @@ -192,12 +194,12 @@ void ShipCustomWarpDialogModel::initializeData() if ((objp->type == OBJ_SHIP) || (objp->type == OBJ_START)) { if (objp->flags[Object::Object_Flags::Marked]) { const auto& sh = Ships[objp->instance]; - if (!_m_departure) + if (!_departure) params = &Warp_params[sh.warpin_params_index]; else params = &Warp_params[sh.warpout_params_index]; if (objp->type == OBJ_START) - _m_player = true; + _player = true; break; } } @@ -206,107 +208,118 @@ void ShipCustomWarpDialogModel::initializeData() if (params != nullptr) { if (params->warp_type & WT_DEFAULT_WITH_FIREBALL) { - _m_warp_type = (params->warp_type & WT_FLAG_MASK) + Num_warp_types; + _warpType = (params->warp_type & WT_FLAG_MASK) + Num_warp_types; } else if (params->warp_type >= 0 && params->warp_type < Num_warp_types) { - _m_warp_type = params->warp_type; + _warpType = params->warp_type; } if (params->snd_start.isValid()) { - _m_start_sound = gamesnd_get_game_sound(params->snd_start)->name; + _startSound = gamesnd_get_game_sound(params->snd_start)->name; } if (params->snd_end.isValid()) { - _m_end_sound = gamesnd_get_game_sound(params->snd_end)->name; + _endSound = gamesnd_get_game_sound(params->snd_end)->name; } if (params->warpout_engage_time > 0) { - _m_warpout_engage_time = i2fl(params->warpout_engage_time) / 1000.0f; + _warpoutEngageTime = i2fl(params->warpout_engage_time) / 1000.0f; } if (params->speed > 0.0f) { - _m_speed = params->speed; + _speed = params->speed; } if (params->time > 0.0f) { - _m_time = i2fl(params->time) / 1000.0f; + _time = i2fl(params->time) / 1000.0f; } if (params->accel_exp > 0.0f) { - _m_accel_exp = params->accel_exp; + _accelExp = params->accel_exp; } if (params->radius > 0.0f) { - _m_radius = params->radius; + _radius = params->radius; } if (strlen(params->anim) > 0) { - _m_anim = params->anim; + _anim = params->anim; } - _m_supercap_warp_physics = params->supercap_warp_physics; + _supercapWarpPhysics = params->supercap_warp_physics; if (params->warpout_player_speed > 0.0f) { - _m_player_warpout_speed = params->warpout_player_speed; + _playerWarpoutSpeed = params->warpout_player_speed; } } _modified = false; } -void ShipCustomWarpDialogModel::setType(const int index) +void ShipCustomWarpDialogModel::setType(int index) { - modify(_m_warp_type, index); + modify(_warpType, index); } -void ShipCustomWarpDialogModel::setStartSound(const SCP_string& newSound) + +void ShipCustomWarpDialogModel::setStartSound(const SCP_string& sound) { - if (!newSound.empty()) { - modify(_m_start_sound, newSound); + if (!sound.empty()) { + modify(_startSound, sound); } else { - _m_start_sound = ""; + _startSound = ""; set_modified(); } } -void ShipCustomWarpDialogModel::setEndSound(const SCP_string& newSound) + +void ShipCustomWarpDialogModel::setEndSound(const SCP_string& sound) { - if (!newSound.empty()) { - modify(_m_end_sound, newSound); + if (!sound.empty()) { + modify(_endSound, sound); } else { - _m_end_sound = ""; + _endSound = ""; set_modified(); } } -void ShipCustomWarpDialogModel::setEngageTime(const double newValue) + +void ShipCustomWarpDialogModel::setEngageTime(double engageTime) { - modify(_m_warpout_engage_time, static_cast(newValue)); + modify(_warpoutEngageTime, static_cast(engageTime)); } -void ShipCustomWarpDialogModel::setSpeed(const double newValue) + +void ShipCustomWarpDialogModel::setSpeed(double speed) { - modify(_m_speed, static_cast(newValue)); + modify(_speed, static_cast(speed)); } -void ShipCustomWarpDialogModel::setTime(const double newValue) + +void ShipCustomWarpDialogModel::setTime(double time) { - modify(_m_time, static_cast(newValue)); + modify(_time, static_cast(time)); } -void ShipCustomWarpDialogModel::setExponent(const double newValue) + +void ShipCustomWarpDialogModel::setExponent(double exponent) { - modify(_m_accel_exp, static_cast(newValue)); + modify(_accelExp, static_cast(exponent)); } -void ShipCustomWarpDialogModel::setRadius(const double newValue) + +void ShipCustomWarpDialogModel::setRadius(double radius) { - modify(_m_radius, static_cast(newValue)); + modify(_radius, static_cast(radius)); } -void ShipCustomWarpDialogModel::setAnim(const SCP_string& newAnim) + +void ShipCustomWarpDialogModel::setAnim(const SCP_string& anim) { - if (!newAnim.empty()) { - modify(_m_anim, newAnim); + if (!anim.empty()) { + modify(_anim, anim); } else { - _m_anim = ""; + _anim = ""; set_modified(); } } -void ShipCustomWarpDialogModel::setSupercap(const bool checked) + +void ShipCustomWarpDialogModel::setSupercap(bool supercap) { - modify(_m_supercap_warp_physics, checked); + modify(_supercapWarpPhysics, supercap); } -void ShipCustomWarpDialogModel::setPlayerSpeed(const double newValue) + +void ShipCustomWarpDialogModel::setPlayerSpeed(double playerSpeed) { - modify(_m_player_warpout_speed, static_cast(newValue)); + modify(_playerWarpoutSpeed, static_cast(playerSpeed)); } -} // namespace dialogs \ No newline at end of file + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.h index 7aa3fe90fc1..e0e6714a2a5 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.h @@ -1,132 +1,66 @@ #pragma once + #include "../AbstractDialogModel.h" + namespace fso::fred::dialogs { -/** - * @brief Model for QtFRED's Custom warp dialog - */ + class ShipCustomWarpDialogModel : public AbstractDialogModel { + Q_OBJECT public: enum class Target { Selection, Wing }; - /** - * @brief Constructor - * @param [in] parent The parent dialog. - * @param [in] viewport The viewport this dialog is attacted to. - * @param [in] departure Whether the dialog is changeing warp-in or warp-out. - */ + ShipCustomWarpDialogModel(QObject* parent, EditorViewport* viewport, bool departure); ShipCustomWarpDialogModel(QObject* parent, EditorViewport* viewport, bool departure, Target target, int wingIndex); bool apply() override; void reject() override; - // Getters - /** - * @brief Getter - * @return Index of warp type - */ int getType() const; - /** - * @brief Getter - * @return Sound name - */ SCP_string getStartSound() const; - /** - * @brief Getter - * @return Sound name - */ SCP_string getEndSound() const; - /** - * @brief Getter - * @return Engage time in seconds - */ float getEngageTime() const; - /** - * @brief Getter - * @return ship speed - */ float getSpeed() const; - /** - * @brief Getter - * @return Time in seconds - */ float getTime() const; - /** - * @brief Getter - * @return Exponent - */ float getExponent() const; - /** - * @brief Getter - * @return Radius of effect - */ float getRadius() const; - /** - * @brief Getter - * @return anim name - */ SCP_string getAnim() const; - /** - * @brief Getter - * @return Supercap Physics - */ bool getSupercap() const; - /** - * @brief Getter - * @return Player Warpout Speed - */ float getPlayerSpeed() const; - /** - * @brief Getter - * @return If the model is in depart mode. - */ bool departMode() const; - /** - * @brief Getter - * @return If the model is working on a player. - */ bool isPlayer() const; - // Setters - void setType(const int index); - void setStartSound(const SCP_string&); - void setEndSound(const SCP_string&); - void setEngageTime(const double); - void setSpeed(const double); - void setTime(const double); - void setExponent(const double); - void setRadius(const double); - void setAnim(const SCP_string&); - void setSupercap(const bool); - void setPlayerSpeed(const double); + void setType(int index); + void setStartSound(const SCP_string& sound); + void setEndSound(const SCP_string& sound); + void setEngageTime(double engageTime); + void setSpeed(double speed); + void setTime(double time); + void setExponent(double exponent); + void setRadius(double radius); + void setAnim(const SCP_string& anim); + void setSupercap(bool supercap); + void setPlayerSpeed(double playerSpeed); - private: - /** - * @brief Initialises data for the model - */ + private: // NOLINT(readability-redundant-access-specifiers) void initializeData(); - bool _m_departure; - - int _m_warp_type; - SCP_string _m_start_sound; - SCP_string _m_end_sound; - float _m_warpout_engage_time; - float _m_speed; - float _m_time; - float _m_accel_exp; - float _m_radius; - SCP_string _m_anim; - bool _m_supercap_warp_physics; - float _m_player_warpout_speed; - bool _m_player = false; + bool _departure; + int _warpType; + SCP_string _startSound; + SCP_string _endSound; + float _warpoutEngageTime; + float _speed; + float _time; + float _accelExp; + float _radius; + SCP_string _anim; + bool _supercapWarpPhysics; + float _playerWarpoutSpeed; + bool _player = false; Target _target = Target::Selection; int _wingIndex = -1; - - /** - * @brief Marks the model as modifed - */ }; -} // namespace dialogs \ No newline at end of file +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.cpp index 756d3a8a9e2..a22b4f7676c 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.cpp @@ -16,7 +16,7 @@ ShipCustomWarpDialog::ShipCustomWarpDialog(QDialog* parent, EditorViewport* view _model(new ShipCustomWarpDialogModel(this, viewport, departure)), _viewport(viewport) { ui->setupUi(this); - connect(_model.get(), &AbstractDialogModel::modelChanged, this, [this]() { updateUI(false); }); + connect(_model.get(), &AbstractDialogModel::modelChanged, this, [this]() { updateUi(false); }); ui->lineEditStartSound->setMaxLength(MAX_FILENAME_LEN - 1); ui->lineEditEndSound->setMaxLength(MAX_FILENAME_LEN - 1); @@ -27,7 +27,7 @@ ShipCustomWarpDialog::ShipCustomWarpDialog(QDialog* parent, EditorViewport* view } else { this->setWindowTitle("Edit Warp-In Parameters"); } - updateUI(true); + updateUi(true); // Resize the dialog to the minimum size resize(QDialog::sizeHint()); } @@ -52,7 +52,7 @@ ShipCustomWarpDialog::ShipCustomWarpDialog(QDialog* parent, _model.reset(new ShipCustomWarpDialogModel(this, viewport, departure)); } - connect(_model.get(), &AbstractDialogModel::modelChanged, this, [this]() { updateUI(false); }); + connect(_model.get(), &AbstractDialogModel::modelChanged, this, [this]() { updateUi(false); }); ui->lineEditStartSound->setMaxLength(MAX_FILENAME_LEN - 1); ui->lineEditEndSound->setMaxLength(MAX_FILENAME_LEN - 1); @@ -63,7 +63,7 @@ ShipCustomWarpDialog::ShipCustomWarpDialog(QDialog* parent, } else { this->setWindowTitle("Edit Warp-In Parameters"); } - updateUI(true); + updateUi(true); // Resize the dialog to the minimum size resize(QDialog::sizeHint()); } @@ -146,7 +146,7 @@ void ShipCustomWarpDialog::closeEvent(QCloseEvent* e) reject(); e->ignore(); // Don't let the base class close the window } -void ShipCustomWarpDialog::updateUI(const bool firstrun) +void ShipCustomWarpDialog::updateUi(const bool firstrun) { util::SignalBlockers blockers(this); if (firstrun) { diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.h index 7caa354bbcd..e3ed8640e3c 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.h @@ -7,20 +7,10 @@ namespace fso::fred::dialogs { namespace Ui { class ShipCustomWarpDialog; } -/** - * @brief QtFRED's Custom Warp Editor - */ class ShipCustomWarpDialog : public QDialog { Q_OBJECT public: - /** - * @brief Constructor - * @param [in] parent The parent dialog. - * @param [in] viewport The viewport this dialog is attacted to. - * @param [in] departure Whether the dialog is changeing warp-in or warp-out. - */ - explicit ShipCustomWarpDialog(QDialog* parent, EditorViewport* viewport, const bool departure = false); - // Constructor for wing mode + explicit ShipCustomWarpDialog(QDialog* parent, EditorViewport* viewport, bool departure = false); ShipCustomWarpDialog(QDialog* parent, EditorViewport* viewport, bool departure, int wingIndex, bool wingMode); ~ShipCustomWarpDialog() override; @@ -28,10 +18,6 @@ class ShipCustomWarpDialog : public QDialog { void reject() override; protected: - /** - * @brief Overides the Dialogs Close event to add a confermation dialog - * @param [in] *e The event. - */ void closeEvent(QCloseEvent*) override; private slots: void on_buttonBox_accepted(); @@ -51,10 +37,6 @@ class ShipCustomWarpDialog : public QDialog { std::unique_ptr ui; std::unique_ptr _model; EditorViewport* _viewport; - /** - * @brief Populates the UI - * @param [in] firstRun If this is the first run. - */ - void updateUI(const bool firstRun = false); + void updateUi(bool firstRun = false); }; -} // namespace fso::fred::dialogs \ No newline at end of file +} // namespace fso::fred::dialogs From 17b08706c1b289691f6a491dcb81adf00f7a3501 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Thu, 14 May 2026 06:53:23 -0500 Subject: [PATCH 47/65] ship flags stylization pass (#7437) --- .../ShipEditor/ShipFlagsDialogModel.cpp | 93 ++++++++++--------- .../dialogs/ShipEditor/ShipFlagsDialogModel.h | 32 ++++--- .../ui/dialogs/ShipEditor/ShipFlagsDialog.cpp | 23 +++-- .../ui/dialogs/ShipEditor/ShipFlagsDialog.h | 7 +- 4 files changed, 79 insertions(+), 76 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.cpp index 2d56cdb4e7c..8515c0522df 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.cpp @@ -1,6 +1,3 @@ -// -// - #include "ShipFlagsDialogModel.h" #include @@ -13,81 +10,82 @@ static const Ship::Ship_Flags player_start_hidden_flags[] = { Ship::Ship_Flags::Reinforcement, Ship::Ship_Flags::Kill_before_mission, }; -int ShipFlagsDialogModel::tristate_set(int val, int cur_state) + +int ShipFlagsDialogModel::tristateSet(int val, int curState) { - // cur_state uses Qt::CheckState encoding (0=Unchecked, 1=PartiallyChecked, 2=Checked) - if (cur_state == Qt::PartiallyChecked) + // curState uses Qt::CheckState encoding (0=Unchecked, 1=PartiallyChecked, 2=Checked) + if (curState == Qt::PartiallyChecked) return Qt::PartiallyChecked; - bool cur_bool = (cur_state == Qt::Checked); + bool cur_bool = (curState == Qt::Checked); if (static_cast(val) != cur_bool) return Qt::PartiallyChecked; - return cur_state; + return curState; } -std::pair* ShipFlagsDialogModel::getFlag(const SCP_string& flag_name) -{ - for (auto& flag : flags) { - if (!stricmp(flag_name.c_str(), flag.first.c_str())) { +std::pair* ShipFlagsDialogModel::getFlag(const SCP_string& flagName) +{ + for (auto& flag : _flags) { + if (!stricmp(flagName.c_str(), flag.first.c_str())) { return &flag; } } // Only assert if the name isn't a known flag at all; it may have been legitimately filtered out for ship starts bool known = false; for (size_t i = 0; i < Num_Parse_ship_flags && !known; ++i) - known = !stricmp(flag_name.c_str(), Parse_ship_flags[i].name); + known = !stricmp(flagName.c_str(), Parse_ship_flags[i].name); for (size_t i = 0; i < Num_Parse_ship_ai_flags && !known; ++i) - known = !stricmp(flag_name.c_str(), Parse_ship_ai_flags[i].name); + known = !stricmp(flagName.c_str(), Parse_ship_ai_flags[i].name); for (size_t i = 0; i < Num_Parse_ship_object_flags && !known; ++i) - known = !stricmp(flag_name.c_str(), Parse_ship_object_flags[i].name); - Assertion(known, "Illegal flag name \"%s\"", flag_name.c_str()); + known = !stricmp(flagName.c_str(), Parse_ship_object_flags[i].name); + Assertion(known, "Illegal flag name \"%s\"", flagName.c_str()); return nullptr; } -void ShipFlagsDialogModel::setFlag(const SCP_string& flag_name, int value) +void ShipFlagsDialogModel::setFlag(const SCP_string& flagName, int state) { - for (auto& flag : flags) { - if (!stricmp(flag_name.c_str(), flag.first.c_str())) { - flag.second = value; + for (auto& flag : _flags) { + if (!stricmp(flagName.c_str(), flag.first.c_str())) { + flag.second = state; set_modified(); } } } -void ShipFlagsDialogModel::setDestroyTime(int value) +void ShipFlagsDialogModel::setDestroyTime(int destroyTime) { - modify(destroytime, value); + modify(_destroyTime, destroyTime); } int ShipFlagsDialogModel::getDestroyTime() const { - return destroytime; + return _destroyTime; } -void ShipFlagsDialogModel::setEscortPriority(int value) +void ShipFlagsDialogModel::setEscortPriority(int priority) { - modify(escortp, value); + modify(_escortPriority, priority); } int ShipFlagsDialogModel::getEscortPriority() const { - return escortp; + return _escortPriority; } -void ShipFlagsDialogModel::setKamikazeDamage(int value) +void ShipFlagsDialogModel::setKamikazeDamage(int damage) { - modify(kamikazed, value); + modify(_kamikazeDamage, damage); } int ShipFlagsDialogModel::getKamikazeDamage() const { - return kamikazed; + return _kamikazeDamage; } -void ShipFlagsDialogModel::update_ship(const int shipnum) +void ShipFlagsDialogModel::updateShip(int shipNum) { - ship* shipp = &Ships[shipnum]; + ship* shipp = &Ships[shipNum]; object* objp = &Objects[shipp->objnum]; - for (const auto& [name, checked] : flags) { + for (const auto& [name, checked] : _flags) { // PartiallyChecked means mixed selection — leave each ship's flag as-is if (checked == Qt::PartiallyChecked) continue; @@ -145,10 +143,11 @@ void ShipFlagsDialogModel::update_ship(const int shipnum) } } } - Ai_info[shipp->ai_index].kamikaze_damage = kamikazed; - shipp->escort_priority = escortp; - shipp->final_death_time = destroytime; + Ai_info[shipp->ai_index].kamikaze_damage = _kamikazeDamage; + shipp->escort_priority = _escortPriority; + shipp->final_death_time = _destroyTime; } + ShipFlagsDialogModel::ShipFlagsDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) { @@ -163,7 +162,7 @@ bool ShipFlagsDialogModel::apply() while (objp != END_OF_LIST(&obj_used_list)) { if ((objp->type == OBJ_START) || (objp->type == OBJ_SHIP)) { if (objp->flags[Object::Object_Flags::Marked]) { - update_ship(objp->instance); + updateShip(objp->instance); } } objp = GET_NEXT(objp); @@ -176,7 +175,7 @@ void ShipFlagsDialogModel::reject() {} const SCP_vector>& ShipFlagsDialogModel::getFlagsList() { - return flags; + return _flags; } SCP_vector> ShipFlagsDialogModel::getShipFlagDescriptions() @@ -262,9 +261,9 @@ void ShipFlagsDialogModel::initializeData() if (objp->flags[Object::Object_Flags::Marked]) { shipp = &Ships[objp->instance]; if (first) { - kamikazed = Ai_info[shipp->ai_index].kamikaze_damage; - escortp = shipp->escort_priority; - destroytime = shipp->final_death_time; + _kamikazeDamage = Ai_info[shipp->ai_index].kamikaze_damage; + _escortPriority = shipp->escort_priority; + _destroyTime = shipp->final_death_time; for (size_t i = 0; i < Num_Parse_ship_flags; i++) { auto flagDef = Parse_ship_flags[i]; @@ -290,12 +289,12 @@ void ShipFlagsDialogModel::initializeData() if (hidden) continue; } bool checked = shipp->flags[flagDef.def]; - flags.emplace_back(flagDef.name, checked ? Qt::Checked : Qt::Unchecked); + _flags.emplace_back(flagDef.name, checked ? Qt::Checked : Qt::Unchecked); } for (size_t i = 0; i < Num_Parse_ship_ai_flags; i++) { auto flagDef = Parse_ship_ai_flags[i]; bool checked = Ai_info[shipp->ai_index].ai_flags[flagDef.def]; - flags.emplace_back(flagDef.name, checked ? Qt::Checked : Qt::Unchecked); + _flags.emplace_back(flagDef.name, checked ? Qt::Checked : Qt::Unchecked); } for (size_t i = 0; i < Num_Parse_ship_object_flags; i++) { auto flagDef = Parse_ship_object_flags[i]; @@ -305,7 +304,7 @@ void ShipFlagsDialogModel::initializeData() } else { checked = objp->flags[flagDef.def]; } - flags.emplace_back(flagDef.name, checked ? Qt::Checked : Qt::Unchecked); + _flags.emplace_back(flagDef.name, checked ? Qt::Checked : Qt::Unchecked); } first = 0; } else { @@ -333,12 +332,12 @@ void ShipFlagsDialogModel::initializeData() if (hidden) continue; } bool checked = shipp->flags[flagDef.def]; - getFlag(flagDef.name)->second = tristate_set(checked, getFlag(flagDef.name)->second); + getFlag(flagDef.name)->second = tristateSet(checked, getFlag(flagDef.name)->second); } for (size_t i = 0; i < Num_Parse_ship_ai_flags; i++) { auto flagDef = Parse_ship_ai_flags[i]; bool checked = Ai_info[shipp->ai_index].ai_flags[flagDef.def]; - getFlag(flagDef.name)->second = tristate_set(checked, getFlag(flagDef.name)->second); + getFlag(flagDef.name)->second = tristateSet(checked, getFlag(flagDef.name)->second); } for (size_t i = 0; i < Num_Parse_ship_object_flags; i++) { auto flagDef = Parse_ship_object_flags[i]; @@ -352,7 +351,7 @@ void ShipFlagsDialogModel::initializeData() } else { checked = objp->flags[flagDef.def]; } - getFlag(flagDef.name)->second = tristate_set(checked, getFlag(flagDef.name)->second); + getFlag(flagDef.name)->second = tristateSet(checked, getFlag(flagDef.name)->second); } } } @@ -360,7 +359,9 @@ void ShipFlagsDialogModel::initializeData() objp = GET_NEXT(objp); } + _modified = false; modelChanged(); _modified = false; } + } // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.h index 308661cbbd5..1189b3ae836 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.h @@ -7,31 +7,33 @@ namespace fso::fred::dialogs { class ShipFlagsDialogModel : public AbstractDialogModel { - private: - static int tristate_set(const int val, const int cur_state); - void update_ship(const int); - SCP_vector> flags; - int destroytime; - int escortp; - int kamikazed; - + Q_OBJECT public: ShipFlagsDialogModel(QObject* parent, EditorViewport* viewport); - void initializeData(); bool apply() override; void reject() override; const SCP_vector>& getFlagsList(); static SCP_vector> getShipFlagDescriptions(); - std::pair* getFlag(const SCP_string& flag_name); - void setFlag(const SCP_string& flag_name, int); + std::pair* getFlag(const SCP_string& flagName); + void setFlag(const SCP_string& flagName, int state); - void setDestroyTime(int); + void setDestroyTime(int destroyTime); int getDestroyTime() const; - void setEscortPriority(int); + void setEscortPriority(int priority); int getEscortPriority() const; - void setKamikazeDamage(int); + void setKamikazeDamage(int damage); int getKamikazeDamage() const; + + private: // NOLINT(readability-redundant-access-specifiers) + void initializeData(); + static int tristateSet(int val, int curState); + void updateShip(int shipNum); + + SCP_vector> _flags; + int _destroyTime; + int _escortPriority; + int _kamikazeDamage; }; -} // namespace fso::fred::dialogs \ No newline at end of file +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.cpp index cf46520f126..a6459a63fa0 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.cpp @@ -21,7 +21,7 @@ ShipFlagsDialog::ShipFlagsDialog(QWidget* parent, EditorViewport* viewport) for (const auto& [name, state] : snapshot) { _model->setFlag(name.toUtf8().constData(), state); } - updateUI(); + updateUi(); }); const auto& flags = _model->getFlagsList(); @@ -33,16 +33,19 @@ ShipFlagsDialog::ShipFlagsDialog(QWidget* parent, EditorViewport* viewport) toWidget.append({name, p.second}); } - ui->flagList->setFlags(toWidget); + { + QSignalBlocker blocker(ui->flagList); + ui->flagList->setFlags(toWidget); - const auto descs = _model->getShipFlagDescriptions(); - QVector> qtDescs; - qtDescs.reserve(static_cast(descs.size())); - for (const auto& d : descs) - qtDescs.append({QString::fromUtf8(d.first.c_str()), QString::fromUtf8(d.second.c_str())}); - ui->flagList->setFlagDescriptions(qtDescs); + const auto descs = _model->getShipFlagDescriptions(); + QVector> qtDescs; + qtDescs.reserve(static_cast(descs.size())); + for (const auto& d : descs) + qtDescs.append({QString::fromUtf8(d.first.c_str()), QString::fromUtf8(d.second.c_str())}); + ui->flagList->setFlagDescriptions(qtDescs); + } - updateUI(); + updateUi(); // Resize the dialog to the minimum size resize(QDialog::sizeHint()); } @@ -92,7 +95,7 @@ void ShipFlagsDialog::on_kamikazeDamageSpinBox_valueChanged(int value) { _model->setKamikazeDamage(value); } -void ShipFlagsDialog::updateUI() +void ShipFlagsDialog::updateUi() { util::SignalBlockers blockers(this); ui->destroySecondsSpinBox->setValue(_model->getDestroyTime()); diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.h index ea88db2da10..3f8d5a3c5a7 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.h @@ -1,5 +1,4 @@ -#ifndef SHIPFLAGDIALOG_H -#define SHIPFLAGDIALOG_H +#pragma once #include #include @@ -33,8 +32,6 @@ class ShipFlagsDialog : public QDialog { std::unique_ptr ui; std::unique_ptr _model; EditorViewport* _viewport; - void updateUI(); + void updateUi(); }; } // namespace fso::fred::dialogs - -#endif // !SHIPFLAGDIALOG_H \ No newline at end of file From 3d43ddbcef7d889eb54b42d5e2d53dd498863e90 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Thu, 14 May 2026 06:55:32 -0500 Subject: [PATCH 48/65] QtFred Ship goals stylization pass (#7438) * ship goals stylization pass * clang * static access --- .../ShipEditor/ShipGoalsDialogModel.cpp | 1490 +++++++++-------- .../dialogs/ShipEditor/ShipGoalsDialogModel.h | 100 +- .../ui/dialogs/ShipEditor/ShipGoalsDialog.cpp | 16 +- .../ui/dialogs/ShipEditor/ShipGoalsDialog.h | 10 +- 4 files changed, 803 insertions(+), 813 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.cpp index 608f2c4b5f9..dab14a652dd 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.cpp @@ -3,792 +3,794 @@ #include #include #include -namespace fso { - namespace fred { - namespace dialogs { - ShipGoalsDialogModel::ShipGoalsDialogModel(QObject* parent, EditorViewport* viewport, bool multi, int shipp, int wingp) - : AbstractDialogModel(parent, viewport) - { - initializeData(multi, shipp, wingp); - } - void ShipGoalsDialogModel::init_combo_data() - { - // don't add more than one of the same string (case-insensitive) - SCP_unordered_map strings_to_indexes; - - // start by adding "None" - auto none_str = "None"; - strings_to_indexes.emplace(none_str, 0); - SCP_set none_set{ AI_GOAL_NONE }; - m_ai_goal_combo_data.clear(); - m_ai_goal_combo_data.emplace_back(none_str, std::move(none_set)); - - // initialize the data used in the combo boxes in the Initial Orders dialog - for (int i = 0; i < Ai_goal_list_size; ++i) - { - if (!valid[i]) - continue; - auto &entry = Editor::getAi_goal_list()[i]; - - // see if we already added the string - auto ii = strings_to_indexes.find(entry.name); - if (ii != strings_to_indexes.end()) - { - // skip adding the string, but add the entry's goal definition to the combo box data at the existing index - m_ai_goal_combo_data[ii->second].second.insert(entry.def); - } - else - { - // this string will correspond to the index that is about to be created - strings_to_indexes[entry.name] = m_ai_goal_combo_data.size(); - - // add the entry's goal definition as the first (maybe only) member of the set - SCP_set new_set{ entry.def }; - m_ai_goal_combo_data.emplace_back(entry.name, std::move(new_set)); - } + +namespace fso::fred::dialogs { + +ShipGoalsDialogModel::ShipGoalsDialogModel(QObject* parent, EditorViewport* viewport, bool multi, int selfShip, int selfWing) + : AbstractDialogModel(parent, viewport) +{ + initializeData(multi, selfShip, selfWing); +} + +void ShipGoalsDialogModel::initComboData() +{ + // don't add more than one of the same string (case-insensitive) + SCP_unordered_map strings_to_indexes; + + // start by adding "None" + auto none_str = "None"; + strings_to_indexes.emplace(none_str, 0); + SCP_set none_set{ AI_GOAL_NONE }; + _aiGoalComboData.clear(); + _aiGoalComboData.emplace_back(none_str, std::move(none_set)); + + // initialize the data used in the combo boxes in the Initial Orders dialog + for (int i = 0; i < _aiGoalListSize; ++i) { + if (!_valid[i]) + continue; + auto& entry = Editor::getAi_goal_list()[i]; + + // see if we already added the string + auto ii = strings_to_indexes.find(entry.name); + if (ii != strings_to_indexes.end()) { + // skip adding the string, but add the entry's goal definition to the combo box data at the existing index + _aiGoalComboData[ii->second].second.insert(entry.def); + } else { + // this string will correspond to the index that is about to be created + strings_to_indexes[entry.name] = _aiGoalComboData.size(); + + // add the entry's goal definition as the first (maybe only) member of the set + SCP_set new_set{ entry.def }; + _aiGoalComboData.emplace_back(entry.name, std::move(new_set)); + } + } +} + +const SCP_vector>>& ShipGoalsDialogModel::getAiGoalComboData() +{ + return _aiGoalComboData; +} + +ai_goal_mode ShipGoalsDialogModel::getFirstModeFromComboBox(int whichItem) +{ + // whichItem indicates initial goal 1 through MAX_AI_GOALS, so find that behavior... + int behavior_index = _behavior[whichItem]; + + // if we have a superposition of behaviors, bail here + if (behavior_index < 0) + return ai_goal_mode::AI_GOAL_SCHROEDINGER; + + // the behavior is the index into the combo box that contains a subset of goals from Ai_goal_list + const auto& set = _aiGoalComboData[behavior_index].second; + + // just get the first mode in the set, since chase/chase-wing and guard/guard-wing are handled respectively together + return *(set.begin()); +} + +bool ShipGoalsDialogModel::apply() +{ + int i; + + if (_goalp) { + for (i = 0; i < ED_MAX_GOALS; i++) + updateItem(i); + + verifyOrders(); + } else { + object* ptr; + + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { + _goalp = Ai_info[Ships[ptr->instance].ai_index].goals; + for (i = 0; i < ED_MAX_GOALS; i++) { + updateItem(i); } } - const SCP_vector>> &ShipGoalsDialogModel::get_ai_goal_combo_data() - { - return m_ai_goal_combo_data; - }; - ai_goal_mode ShipGoalsDialogModel::get_first_mode_from_combo_box(int which_item) - { - // which_item indicates initial goal 1 through MAX_AI_GOALS, so find that behavior... - int behavior_index = m_behavior[which_item]; - - // if we have a superposition of behaviors, bail here - if (behavior_index < 0) - return ai_goal_mode::AI_GOAL_SCHROEDINGER; - - // the behavior is the index into the combo box that contains a subset of goals from Ai_goal_list - const auto &set = m_ai_goal_combo_data[behavior_index].second; - - // just get the first mode in the set, since chase/chase-wing and guard/guard-wing are handled respectively together - return *(set.begin()); - } - bool ShipGoalsDialogModel::apply() - { - int i; - if (goalp) { - for (i = 0; i < ED_MAX_GOALS; i++) - update_item(i); + ptr = GET_NEXT(ptr); + } - verify_orders(); + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { + _selfShip = ptr->instance; + _goalp = Ai_info[Ships[_selfShip].ai_index].goals; + verifyOrders(); + } + ptr = GET_NEXT(ptr); + } + } + + _editor->missionChanged(); + return true; +} + +int ShipGoalsDialogModel::verifyOrders() +{ + ErrorChecker checker(_viewport); + if (!checker.runCheck(ErrorCheckType::InitialOrders, {_goalp, _selfShip, _selfWing})) + return 0; + + SCP_string message; + for (const auto& entry : checker.getErrors()) { + if (!message.empty()) + message += "\n\n"; + message += entry.message; + } + + auto button = _viewport->dialogProvider->showButtonDialog(DialogType::Error, + "Order Error", + message, + {DialogButton::Ok, DialogButton::Cancel}); + return (button == DialogButton::Ok) ? 0 : 1; +} + +void ShipGoalsDialogModel::updateItem(int item) +{ + ai_goal_mode mode; + char save[80]{}; + SCP_string error_message; + waypoint_list* wp_list; + + if (item >= MAX_AI_GOALS) + return; + + if (!_multiEdit || _priority[item] >= 0) + _goalp[item].priority = _priority[item]; + + mode = getFirstModeFromComboBox(item); + switch (mode) { + case AI_GOAL_NONE: + case AI_GOAL_CHASE_ANY: + case AI_GOAL_UNDOCK: + case AI_GOAL_KEEP_SAFE_DISTANCE: + case AI_GOAL_PLAY_DEAD: + case AI_GOAL_PLAY_DEAD_PERSISTENT: + case AI_GOAL_WARP: + // these goals do not have a target in the dialog box, so let's set the goal and return immediately + // so that we don't run afoul of the "doesn't have a valid target" code at the bottom of the function + modify(_goalp[item].ai_mode, mode); + return; + + case AI_GOAL_SCHROEDINGER: + // return, but don't set the goal + return; + + case AI_GOAL_WAYPOINTS: + case AI_GOAL_WAYPOINTS_ONCE: + case AI_GOAL_DISABLE_SHIP: + case AI_GOAL_DISABLE_SHIP_TACTICAL: + case AI_GOAL_DISARM_SHIP: + case AI_GOAL_DISARM_SHIP_TACTICAL: + case AI_GOAL_IGNORE: + case AI_GOAL_IGNORE_NEW: + case AI_GOAL_EVADE_SHIP: + case AI_GOAL_STAY_NEAR_SHIP: + case AI_GOAL_FORM_ON_WING: + case AI_GOAL_STAY_STILL: + case AI_GOAL_CHASE_SHIP_CLASS: + break; + + case AI_GOAL_DESTROY_SUBSYSTEM: { + if (_subsys[item].empty()) { + sprintf(error_message, "Order #%d doesn't have valid subsystem name. Order will be removed", item + 1); + _viewport->dialogProvider->showButtonDialog(DialogType::Information, + "Order Error", + error_message, + { DialogButton::Ok }); + modify(_goalp[item].ai_mode, AI_GOAL_NONE); + return; + } + + // Look up the subsystem in the target ship to get a persistent name pointer. + // Storing _subsys[item].c_str() directly would dangle after the model is destroyed. + const char* persistent_name = nullptr; + int target_ship_idx = _object[item] & DATA_MASK; + if (target_ship_idx >= 0 && target_ship_idx < MAX_SHIPS) { + ship_subsys* cur_ss = GET_FIRST(&Ships[target_ship_idx].subsys_list); + while (cur_ss != END_OF_LIST(&Ships[target_ship_idx].subsys_list)) { + if (!stricmp(cur_ss->system_info->subobj_name, _subsys[item].c_str())) { + persistent_name = cur_ss->system_info->subobj_name; + break; } - else { - object* ptr; - - ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { - goalp = Ai_info[Ships[ptr->instance].ai_index].goals; - for (i = 0; i < ED_MAX_GOALS; i++) { - update_item(i); - } - } - - ptr = GET_NEXT(ptr); - } - - ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { - self_ship = ptr->instance; - goalp = Ai_info[Ships[self_ship].ai_index].goals; - verify_orders(); + cur_ss = GET_NEXT(cur_ss); + } + } + + if (!persistent_name) { + sprintf(error_message, "Order #%d doesn't have valid subsystem name. Order will be removed", item + 1); + _viewport->dialogProvider->showButtonDialog(DialogType::Information, + "Order Error", + error_message, + { DialogButton::Ok }); + modify(_goalp[item].ai_mode, AI_GOAL_NONE); + return; + } + + if (!_goalp[item].docker.name || stricmp(_goalp[item].docker.name, persistent_name) != 0) + set_modified(); + _goalp[item].docker.name = persistent_name; + break; + } + + case AI_GOAL_CHASE: + case AI_GOAL_CHASE_WING: + switch (_object[item] & TYPE_MASK) { + case TYPE_SHIP: + case TYPE_PLAYER: + mode = AI_GOAL_CHASE; + break; + + case TYPE_WING: + mode = AI_GOAL_CHASE_WING; + break; + } + + break; + + case AI_GOAL_DOCK: { + // Resolve persistent dock bay name pointers from the polymodel. + // Storing _subsys[item].c_str() directly would dangle after the model is destroyed. + char* docker = nullptr; + char* dockee = nullptr; + + if (!_multiEdit || (_object[item] && !_subsys[item].empty())) { + if (_selfShip >= 0) { + int model_num = Ship_info[Ships[_selfShip].ship_info_index].model_num; + polymodel* pm = model_get(model_num); + if (pm) { + for (int b = 0; b < pm->n_docks; b++) { + if (!stricmp(pm->docking_bays[b].name, _subsys[item].c_str())) { + docker = pm->docking_bays[b].name; + break; } - - ptr = GET_NEXT(ptr); } } - - _editor->missionChanged(); - return true; } - - int ShipGoalsDialogModel::verify_orders() - { - ErrorChecker checker(_viewport); - if (!checker.runCheck(ErrorCheckType::InitialOrders, {goalp, self_ship, self_wing})) - return 0; - - SCP_string message; - for (const auto& entry : checker.getErrors()) { - if (!message.empty()) - message += "\n\n"; - message += entry.message; + } + + if (!_multiEdit || (_object[item] && (_dock2[item] >= 0))) { + int dockee_ship = _object[item] & DATA_MASK; + if (dockee_ship >= 0 && dockee_ship < MAX_SHIPS && _dock2[item] >= 0) { + int model_num = Ship_info[Ships[dockee_ship].ship_info_index].model_num; + polymodel* pm = model_get(model_num); + if (pm && _dock2[item] < pm->n_docks) { + dockee = pm->docking_bays[_dock2[item]].name; } - - auto button = _viewport->dialogProvider->showButtonDialog(DialogType::Error, - "Order Error", - message, - { DialogButton::Ok, DialogButton::Cancel }); - return (button == DialogButton::Ok) ? 0 : 1; } - - void ShipGoalsDialogModel::update_item(const int item) - { - ai_goal_mode mode; - char save[80]{}; - SCP_string error_message; - waypoint_list* wp_list; - - if (item >= MAX_AI_GOALS) - return; - - if (!m_multi_edit || m_priority[item] >= 0) - goalp[item].priority = m_priority[item]; - - mode = get_first_mode_from_combo_box(item); - switch (mode) { - case AI_GOAL_NONE: - case AI_GOAL_CHASE_ANY: - case AI_GOAL_UNDOCK: - case AI_GOAL_KEEP_SAFE_DISTANCE: - case AI_GOAL_PLAY_DEAD: - case AI_GOAL_PLAY_DEAD_PERSISTENT: - case AI_GOAL_WARP: - // these goals do not have a target in the dialog box, so let's set the goal and return immediately - // so that we don't run afoul of the "doesn't have a valid target" code at the bottom of the function - modify(goalp[item].ai_mode, mode); - return; - - case AI_GOAL_SCHROEDINGER: - // return, but don't set the goal - return; - - case AI_GOAL_WAYPOINTS: - case AI_GOAL_WAYPOINTS_ONCE: - case AI_GOAL_DISABLE_SHIP: - case AI_GOAL_DISABLE_SHIP_TACTICAL: - case AI_GOAL_DISARM_SHIP: - case AI_GOAL_DISARM_SHIP_TACTICAL: - case AI_GOAL_IGNORE: - case AI_GOAL_IGNORE_NEW: - case AI_GOAL_EVADE_SHIP: - case AI_GOAL_STAY_NEAR_SHIP: - case AI_GOAL_FORM_ON_WING: - case AI_GOAL_STAY_STILL: - case AI_GOAL_CHASE_SHIP_CLASS: - break; - - case AI_GOAL_DESTROY_SUBSYSTEM: { - if (m_subsys[item].empty()) { - sprintf(error_message, "Order #%d doesn't have valid subsystem name. Order will be removed", item + 1); - _viewport->dialogProvider->showButtonDialog(DialogType::Information, - "Order Error", - error_message, - { DialogButton::Ok }); - modify(goalp[item].ai_mode, AI_GOAL_NONE); - return; - } - - // Look up the subsystem in the target ship to get a persistent name pointer. - // Storing m_subsys[item].c_str() directly would dangle after the model is destroyed. - const char* persistent_name = nullptr; - int target_ship_idx = m_object[item] & DATA_MASK; - if (target_ship_idx >= 0 && target_ship_idx < MAX_SHIPS) { - ship_subsys* cur_ss = GET_FIRST(&Ships[target_ship_idx].subsys_list); - while (cur_ss != END_OF_LIST(&Ships[target_ship_idx].subsys_list)) { - if (!stricmp(cur_ss->system_info->subobj_name, m_subsys[item].c_str())) { - persistent_name = cur_ss->system_info->subobj_name; - break; - } - cur_ss = GET_NEXT(cur_ss); - } - } - - if (!persistent_name) { - sprintf(error_message, "Order #%d doesn't have valid subsystem name. Order will be removed", item + 1); - _viewport->dialogProvider->showButtonDialog(DialogType::Information, - "Order Error", - error_message, - { DialogButton::Ok }); - modify(goalp[item].ai_mode, AI_GOAL_NONE); - return; - } - - if (!goalp[item].docker.name || stricmp(goalp[item].docker.name, persistent_name) != 0) - set_modified(); - goalp[item].docker.name = persistent_name; - break; - } - - case AI_GOAL_CHASE: - case AI_GOAL_CHASE_WING: - switch (m_object[item] & TYPE_MASK) { - case TYPE_SHIP: - case TYPE_PLAYER: - mode = AI_GOAL_CHASE; - break; - - case TYPE_WING: - mode = AI_GOAL_CHASE_WING; - break; - } - - break; - - case AI_GOAL_DOCK: { - // Resolve persistent dock bay name pointers from the polymodel. - // Storing m_subsys[item].c_str() directly would dangle after the model is destroyed. - char* docker = nullptr; - char* dockee = nullptr; - - if (!m_multi_edit || (m_object[item] && !m_subsys[item].empty())) { - if (self_ship >= 0) { - int model_num = Ship_info[Ships[self_ship].ship_info_index].model_num; - polymodel* pm = model_get(model_num); - if (pm) { - for (int b = 0; b < pm->n_docks; b++) { - if (!stricmp(pm->docking_bays[b].name, m_subsys[item].c_str())) { - docker = pm->docking_bays[b].name; - break; - } - } - } - } - } - - if (!m_multi_edit || (m_object[item] && (m_dock2[item] >= 0))) { - int dockee_ship = m_object[item] & DATA_MASK; - if (dockee_ship >= 0 && dockee_ship < MAX_SHIPS && m_dock2[item] >= 0) { - int model_num = Ship_info[Ships[dockee_ship].ship_info_index].model_num; - polymodel* pm = model_get(model_num); - if (pm && m_dock2[item] < pm->n_docks) { - dockee = pm->docking_bays[m_dock2[item]].name; - } - } - } - - if (!docker || !dockee) { - sprintf(error_message, "Order #%d doesn't have valid docking points. Order will be removed", item + 1); - _viewport->dialogProvider->showButtonDialog(DialogType::Information, - "Order Error", - error_message, - { DialogButton::Ok }); - modify(goalp[item].ai_mode, AI_GOAL_NONE); - return; - } else { - if (!goalp[item].docker.name || stricmp(goalp[item].docker.name, docker) != 0) - set_modified(); - if (!goalp[item].dockee.name || stricmp(goalp[item].dockee.name, dockee) != 0) - set_modified(); - - goalp[item].docker.name = docker; - goalp[item].dockee.name = dockee; - } - - break; + } + + if (!docker || !dockee) { + sprintf(error_message, "Order #%d doesn't have valid docking points. Order will be removed", item + 1); + _viewport->dialogProvider->showButtonDialog(DialogType::Information, + "Order Error", + error_message, + { DialogButton::Ok }); + modify(_goalp[item].ai_mode, AI_GOAL_NONE); + return; + } else { + if (!_goalp[item].docker.name || stricmp(_goalp[item].docker.name, docker) != 0) + set_modified(); + if (!_goalp[item].dockee.name || stricmp(_goalp[item].dockee.name, dockee) != 0) + set_modified(); + + _goalp[item].docker.name = docker; + _goalp[item].dockee.name = dockee; + } + + break; + } + + case AI_GOAL_GUARD: + case AI_GOAL_GUARD_WING: + switch (_object[item] & TYPE_MASK) { + case TYPE_SHIP: + case TYPE_PLAYER: + mode = AI_GOAL_GUARD; + break; + + case TYPE_WING: + mode = AI_GOAL_GUARD_WING; + break; + } + + break; + + default: + Warning(LOCATION, "Unknown AI_GOAL type 0x%x", mode); + modify(_goalp[item].ai_mode, AI_GOAL_NONE); + return; + } + modify(_goalp[item].ai_mode, mode); + + *save = 0; + if (_goalp[item].target_name) + strcpy_s(save, _goalp[item].target_name); + + switch (_object[item] & TYPE_MASK) { + int not_used; + + case TYPE_SHIP: + case TYPE_PLAYER: + _goalp[item].target_name = ai_get_goal_target_name(Ships[_object[item] & DATA_MASK].ship_name, ¬_used); + break; + + case TYPE_WING: + _goalp[item].target_name = ai_get_goal_target_name(Wings[_object[item] & DATA_MASK].name, ¬_used); + break; + + case TYPE_PATH: + wp_list = find_waypoint_list_at_index(_object[item] & DATA_MASK); + Assert(wp_list != nullptr); + _goalp[item].target_name = ai_get_goal_target_name(wp_list->get_name(), ¬_used); + break; + + case TYPE_WAYPOINT: + _goalp[item].target_name = ai_get_goal_target_name(object_name(_object[item] & DATA_MASK), ¬_used); + break; + + case TYPE_SHIP_CLASS: + _goalp[item].target_name = ai_get_goal_target_name(Ship_info[_object[item] & DATA_MASK].name, ¬_used); + break; + + case 0: + case (-1 & TYPE_MASK): + if (_multiEdit) + return; + + sprintf(error_message, "Order #%d doesn't have a valid target. Order will be removed", item + 1); + _viewport->dialogProvider->showButtonDialog(DialogType::Information, + "Order Error", + error_message, + { DialogButton::Ok }); + modify(_goalp[item].ai_mode, AI_GOAL_NONE); + return; + + default: + Error(LOCATION, "Unhandled TYPE_X #define %d in ship goals dialog box", _object[item] & TYPE_MASK); + } + + if (stricmp(save, _goalp[item].target_name)) + set_modified(); +} + +void ShipGoalsDialogModel::reject() {} + +void ShipGoalsDialogModel::initializeData(bool multi, int selfShip, int selfWing) +{ + int i, j, z; + object* ptr; + for (i = 0; i < ED_MAX_GOALS; i++) { + _behavior[i] = -1; + _object[i] = -1; + _priority[i] = 0; + _subsys[i] = ""; + _dock2[i] = -1; + } + _goalp = nullptr; + _multiEdit = multi; + _selfShip = selfShip; + _selfWing = selfWing; + Assert(_aiGoalListSize <= MAX_VALID); + + // start off with all goals available + for (i = 0; i < _aiGoalListSize; i++) { + _valid[i] = 1; + } + + if (_selfShip >= 0) { // editing orders for just one ship + for (i = 0; i < _aiGoalListSize; i++) { + if (!(ai_query_goal_valid(_selfShip, Editor::getAi_goal_list()[i].def))) { + _valid[i] = 0; + } + } + } else if (_selfWing >= 0) { // editing orders for just one wing + for (i = 0; i < Wings[_selfWing].wave_count; i++) { + for (j = 0; j < _aiGoalListSize; j++) { + if (!ai_query_goal_valid(Wings[_selfWing].ship_index[i], Editor::getAi_goal_list()[j].def)) { + _valid[j] = 0; } - - case AI_GOAL_GUARD: - case AI_GOAL_GUARD_WING: - switch (m_object[item] & TYPE_MASK) { - case TYPE_SHIP: - case TYPE_PLAYER: - mode = AI_GOAL_GUARD; - break; - - case TYPE_WING: - mode = AI_GOAL_GUARD_WING; - break; + } + } + for (i = 0; i < _aiGoalListSize; i++) { + if (Editor::getAi_goal_list()[i].def == AI_GOAL_DOCK) { // a whole wing can't dock with one object.. + _valid[i] = 0; + } + } + } else { // editing orders for all marked ships + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { + for (i = 0; i < _aiGoalListSize; i++) { + if (!ai_query_goal_valid(ptr->instance, Editor::getAi_goal_list()[i].def)) { + _valid[i] = 0; } - - break; - - default: - Warning(LOCATION, "Unknown AI_GOAL type 0x%x", mode); - modify(goalp[item].ai_mode, AI_GOAL_NONE); - return; } - modify(goalp[item].ai_mode, mode); - - *save = 0; - if (goalp[item].target_name) - strcpy_s(save, goalp[item].target_name); - - switch (m_object[item] & TYPE_MASK) { - int not_used; - - case TYPE_SHIP: - case TYPE_PLAYER: - goalp[item].target_name = ai_get_goal_target_name(Ships[m_object[item] & DATA_MASK].ship_name, ¬_used); - break; - - case TYPE_WING: - goalp[item].target_name = ai_get_goal_target_name(Wings[m_object[item] & DATA_MASK].name, ¬_used); - break; + } - case TYPE_PATH: - wp_list = find_waypoint_list_at_index(m_object[item] & DATA_MASK); - Assert(wp_list != nullptr); - goalp[item].target_name = ai_get_goal_target_name(wp_list->get_name(), ¬_used); - break; + ptr = GET_NEXT(ptr); + } + } + if (Waypoint_lists.empty()) { + for (i = 0; i < _aiGoalListSize; i++) { + switch (Editor::getAi_goal_list()[i].def) { + case AI_GOAL_WAYPOINTS: + case AI_GOAL_WAYPOINTS_ONCE: + _valid[i] = 0; + break; + default: + break; + } + } + } - case TYPE_WAYPOINT: - goalp[item].target_name = ai_get_goal_target_name(object_name(m_object[item] & DATA_MASK), ¬_used); - break; + z = 0; + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if ((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) { + i = ptr->instance; - case TYPE_SHIP_CLASS: - goalp[item].target_name = ai_get_goal_target_name(Ship_info[m_object[item] & DATA_MASK].name, ¬_used); + if ((_selfShip > 0) && (_selfShip != i) && ship_docking_valid(_selfShip, i)) { + z = 1; + } + } + ptr = GET_NEXT(ptr); + } + + if (!z) { + for (i = 0; i < _aiGoalListSize; i++) { + if (Editor::getAi_goal_list()[i].def == AI_GOAL_DOCK) { + _valid[i] = 0; + } + } + } + + // initialize the data for the behavior boxes (they remain constant while the dialog is open) + initComboData(); + + if (_selfShip >= 0) { + initialize(Ai_info[Ships[_selfShip].ai_index].goals); + } else if (_selfWing >= 0) { + initialize(Wings[_selfWing].ai_goals); + } else { + initializeMulti(); + } + modelChanged(); + _modified = false; +} + +void ShipGoalsDialogModel::initialize(ai_goal* goals) +{ + int i, item, inst, flag; + ai_goal_mode mode; + object* ptr; + SCP_vector docks; + + // note that the flag variable is a bitfield: + // 1 = ships + // 2 = wings + // 4 = waypoint paths + // 8 = individual waypoints + // 16 = ship classes + + _goalp = goals; + for (item = 0; item < ED_MAX_GOALS; item++) { + flag = 1; + _priority[item] = 0; + mode = AI_GOAL_NONE; + + if (item < MAX_AI_GOALS) { + _priority[item] = _goalp[item].priority; + mode = _goalp[item].ai_mode; + } + + if (_priority[item] < 0 || _priority[item] > MAX_EDITOR_GOAL_PRIORITY) { + _priority[item] = 50; + } + + _behavior[item] = 0; + if (mode != AI_GOAL_NONE) { + i = static_cast(_aiGoalComboData.size()); + while (i-- > 0) { + const auto& set = _aiGoalComboData[i].second; + if (set.find(mode) != set.end()) { + _behavior[item] = i; break; - - case 0: - case (-1 & TYPE_MASK): - if (m_multi_edit) - return; - - sprintf(error_message, "Order #%d doesn't have a valid target. Order will be removed", item + 1); - _viewport->dialogProvider->showButtonDialog(DialogType::Information, - "Order Error", - error_message, - { DialogButton::Ok }); - modify(goalp[item].ai_mode, AI_GOAL_NONE); - return; - - default: - Error(LOCATION, "Unhandled TYPE_X #define %d in ship goals dialog box", m_object[item] & TYPE_MASK); } - - if (stricmp(save, goalp[item].target_name)) - set_modified(); } - - void ShipGoalsDialogModel::reject() {} - - void ShipGoalsDialogModel::initializeData(const bool multi, const int shipp, const int wingp) - { - int i, j, z; - object* ptr; - for (i = 0; i < ED_MAX_GOALS; i++) { - m_behavior[i] = -1; - m_object[i] = -1; - m_priority[i] = 0; - m_subsys[i] = ""; - m_dock2[i] = -1; - // m_data[i] = 0; - } - goalp = nullptr; - m_multi_edit = multi; - self_ship = shipp; - self_wing = wingp; - Assert(Ai_goal_list_size <= MAX_VALID); - - - // start off with all goals available - for (i = 0; i < Ai_goal_list_size; i++) { - valid[i] = 1; - } - - if (self_ship >= 0) { // editing orders for just one ship - for (i = 0; i < Ai_goal_list_size; i++) { - if (!(ai_query_goal_valid(self_ship, Editor::getAi_goal_list()[i].def))) { - valid[i] = 0; - } - } - } - else if (self_wing >= 0) { // editing orders for just one wing - for (i = 0; i < Wings[self_wing].wave_count; i++) { - for (j = 0; j < Ai_goal_list_size; j++) { - if (!ai_query_goal_valid(Wings[self_wing].ship_index[i], Editor::getAi_goal_list()[j].def)) { - valid[j] = 0; - } - } - } - for (i = 0; i < Ai_goal_list_size; i++) { - if (Editor::getAi_goal_list()[i].def == AI_GOAL_DOCK) { // a whole wing can't dock with one object.. - valid[i] = 0; - } - } - } - else { // editing orders for all marked ships - ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { - for (i = 0; i < Ai_goal_list_size; i++) { - if (!ai_query_goal_valid(ptr->instance, Editor::getAi_goal_list()[i].def)) { - valid[i] = 0; - } - } - } - - ptr = GET_NEXT(ptr); - } - } - if (Waypoint_lists.empty()) { - for (i = 0; i < Ai_goal_list_size; i++) { - switch (Editor::getAi_goal_list()[i].def) { - case AI_GOAL_WAYPOINTS: - case AI_GOAL_WAYPOINTS_ONCE: - // case AI_GOAL_WARP: - valid[i] = 0; + } + + switch (mode) { + case AI_GOAL_NONE: + case AI_GOAL_CHASE_ANY: + case AI_GOAL_UNDOCK: + case AI_GOAL_KEEP_SAFE_DISTANCE: + case AI_GOAL_PLAY_DEAD: + case AI_GOAL_PLAY_DEAD_PERSISTENT: + case AI_GOAL_WARP: + continue; + + case AI_GOAL_CHASE_SHIP_CLASS: + flag = 16; // target is a ship class + break; + + case AI_GOAL_STAY_STILL: + flag = 9; // target is a ship or a waypoint + break; + + case AI_GOAL_CHASE: + case AI_GOAL_GUARD: + case AI_GOAL_DISABLE_SHIP: + case AI_GOAL_DISABLE_SHIP_TACTICAL: + case AI_GOAL_DISARM_SHIP: + case AI_GOAL_DISARM_SHIP_TACTICAL: + case AI_GOAL_IGNORE: + case AI_GOAL_IGNORE_NEW: + case AI_GOAL_EVADE_SHIP: + case AI_GOAL_STAY_NEAR_SHIP: + case AI_GOAL_FORM_ON_WING: + break; + + case AI_GOAL_WAYPOINTS: + case AI_GOAL_WAYPOINTS_ONCE: + flag = 4; // target is a waypoint + break; + + case AI_GOAL_DESTROY_SUBSYSTEM: + // docker.name already holds the subsystem name string... copy it directly. + // (ship_find_subsys returns an int index, not the name, so don't use it here.) + if (_goalp[item].docker.name != nullptr) + _subsys[item] = _goalp[item].docker.name; + break; + + case AI_GOAL_DOCK: + // Store the docker bay name string directly; the persistent pointer lives in the polymodel. + if (_goalp[item].docker.name != nullptr) + _subsys[item] = _goalp[item].docker.name; + break; + + case AI_GOAL_CHASE_WING: + case AI_GOAL_GUARD_WING: + flag = 2; // target is a wing + break; + + default: + Error(LOCATION, "Unhandled AI_GOAL_X #define %d in ship goals dialog box", mode); + } + + if (flag & 0x1) { + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if ((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) { + inst = ptr->instance; + if (ptr->type == OBJ_SHIP) { + Assertion(inst >= 0 && inst < MAX_SHIPS, "inst must be a valid ship index"); // NOLINT(readability-simplify-boolean-expr) + if (!stricmp(_goalp[item].target_name, Ships[inst].ship_name)) { + _object[item] = inst | TYPE_SHIP; break; - default: + } + } else { + Assertion(inst >= 0 && inst < MAX_SHIPS, "inst must be a valid ship index"); // NOLINT(readability-simplify-boolean-expr) + if (!stricmp(_goalp[item].target_name, Ships[inst].ship_name)) { + _object[item] = inst | TYPE_PLAYER; break; } } } - z = 0; - ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - if ((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) { - i = ptr->instance; - - if ((self_ship > 0) && (self_ship != i) && ship_docking_valid(self_ship, i)) { - z = 1; - } - } - ptr = GET_NEXT(ptr); - } + ptr = GET_NEXT(ptr); + } + } - if (!z) { - for (i = 0; i < Ai_goal_list_size; i++) { - if (Editor::getAi_goal_list()[i].def == AI_GOAL_DOCK) { - valid[i] = 0; - } + if (flag & 0x2) { + for (i = 0; i < MAX_WINGS; i++) { + if (Wings[i].wave_count) { + if (!stricmp(_goalp[item].target_name, Wings[i].name)) { + _object[item] = i | TYPE_WING; + break; } } + } + } - // initialize the data for the behavior boxes (they remain constant while the dialog is open) - init_combo_data(); - - if (self_ship >= 0) { - initialize(Ai_info[Ships[self_ship].ai_index].goals); - } - else if (self_wing >= 0) { - initialize(Wings[self_wing].ai_goals); + if (flag & 0x4) { // data is a waypoint path name + SCP_vector::iterator ii; + for (i = 0, ii = Waypoint_lists.begin(); ii != Waypoint_lists.end(); ++i, ++ii) { + if (!stricmp(_goalp[item].target_name, ii->get_name())) { + _object[item] = i | TYPE_PATH; + break; } - else { - initialize_multi(); + } + } + + if (flag & 0x8) { // data is a waypoint name + waypoint* wpt = find_matching_waypoint(_goalp[item].target_name); + if (wpt != nullptr) + _object[item] = wpt->get_objnum() | TYPE_WAYPOINT; + } + + if (flag & 0x10) { // data is a ship class + for (i = 0; i < ship_info_size(); i++) { + if (!stricmp(_goalp[item].target_name, Ship_info[i].name)) { + _object[item] = i | TYPE_SHIP_CLASS; + break; } - modelChanged(); - _modified = false; } - void ShipGoalsDialogModel::initialize(ai_goal* goals) - { - int i, item, inst, flag; - ai_goal_mode mode; - object* ptr; - SCP_vector docks; - - // note that the flag variable is a bitfield: - // 1 = ships - // 2 = wings - // 4 = waypoint paths - // 8 = individual waypoints - // 16 = ship classes - - goalp = goals; - for (item = 0; item < ED_MAX_GOALS; item++) { - flag = 1; - //m_data[item] = 0; - m_priority[item] = 0; - mode = AI_GOAL_NONE; - - if (item < MAX_AI_GOALS) { - m_priority[item] = goalp[item].priority; - mode = goalp[item].ai_mode; - } - - if (m_priority[item] < 0 || m_priority[item] > MAX_EDITOR_GOAL_PRIORITY) { - m_priority[item] = 50; - } - - m_behavior[item] = 0; - if (mode != AI_GOAL_NONE) { - i = static_cast(m_ai_goal_combo_data.size()); - while (i-- > 0) { - const auto &set = m_ai_goal_combo_data[i].second; - if (set.find(mode) != set.end()) { - m_behavior[item] = i; - break; - } - } - } - - switch (mode) { - case AI_GOAL_NONE: - case AI_GOAL_CHASE_ANY: - case AI_GOAL_UNDOCK: - case AI_GOAL_KEEP_SAFE_DISTANCE: - case AI_GOAL_PLAY_DEAD: - case AI_GOAL_PLAY_DEAD_PERSISTENT: - case AI_GOAL_WARP: - continue; - - case AI_GOAL_CHASE_SHIP_CLASS: - flag = 16; // target is a ship class - break; - - case AI_GOAL_STAY_STILL: - flag = 9; // target is a ship or a waypoint - break; - - case AI_GOAL_CHASE: - case AI_GOAL_GUARD: - case AI_GOAL_DISABLE_SHIP: - case AI_GOAL_DISABLE_SHIP_TACTICAL: - case AI_GOAL_DISARM_SHIP: - case AI_GOAL_DISARM_SHIP_TACTICAL: - case AI_GOAL_IGNORE: - case AI_GOAL_IGNORE_NEW: - case AI_GOAL_EVADE_SHIP: - case AI_GOAL_STAY_NEAR_SHIP: - case AI_GOAL_FORM_ON_WING: - break; - - case AI_GOAL_WAYPOINTS: - case AI_GOAL_WAYPOINTS_ONCE: - flag = 4; // target is a waypoint - break; - - case AI_GOAL_DESTROY_SUBSYSTEM: - // docker.name already holds the subsystem name string... copy it directly. - // (ship_find_subsys returns an int index, not the name, so don't use it here.) - if (goalp[item].docker.name != nullptr) - m_subsys[item] = goalp[item].docker.name; + } + + switch (mode) { + case AI_GOAL_DOCK: + _dock2[item] = -1; + if (_object[item]) { + docks = fso::fred::Editor::get_docking_list(Ship_info[Ships[_object[item] & DATA_MASK].ship_info_index].model_num); + for (i = 0; unsigned(i) < docks.size(); i++) { + Assert(_goalp[item].dockee.name); + Assert(_goalp[item].dockee.index != -1); + if (!stricmp(_goalp[item].dockee.name, docks[i].c_str())) { + _dock2[item] = i; break; - - case AI_GOAL_DOCK: - // Store the docker bay name string directly; the persistent pointer lives in the polymodel. - if (goalp[item].docker.name != nullptr) - m_subsys[item] = goalp[item].docker.name; - break; - - case AI_GOAL_CHASE_WING: - case AI_GOAL_GUARD_WING: - flag = 2; // target is a wing - break; - - default: - Error(LOCATION, "Unhandled AI_GOAL_X #define %d in ship goals dialog box", mode); - } - - if (flag & 0x1) { - ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - if ((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) { - inst = ptr->instance; - if (ptr->type == OBJ_SHIP) { - Assert(inst >= 0 && inst < MAX_SHIPS); - if (!stricmp(goalp[item].target_name, Ships[inst].ship_name)) { - m_object[item] = inst | TYPE_SHIP; - break; - } - - } - else { - Assert(inst >= 0 && inst < MAX_SHIPS); - if (!stricmp(goalp[item].target_name, Ships[inst].ship_name)) { - m_object[item] = inst | TYPE_PLAYER; - break; - } - } - } - - ptr = GET_NEXT(ptr); - } } - - if (flag & 0x2) { - for (i = 0; i < MAX_WINGS; i++) { - if (Wings[i].wave_count) { - if (!stricmp(goalp[item].target_name, Wings[i].name)) { - m_object[item] = i | TYPE_WING; - break; - } - } - } - } - - if (flag & 0x4) { // data is a waypoint path name - SCP_vector::iterator ii; - for (i = 0, ii = Waypoint_lists.begin(); ii != Waypoint_lists.end(); ++i, ++ii) { - if (!stricmp(goalp[item].target_name, ii->get_name())) { - m_object[item] = i | TYPE_PATH; - break; - } - } + } + } + break; + default: + break; + } + } +} + +void ShipGoalsDialogModel::initializeMulti() +{ + int i, flag = 0; + object* ptr; + int behavior[ED_MAX_GOALS]{}; + int priority[ED_MAX_GOALS]{}; + SCP_string subsys[ED_MAX_GOALS]{}; + int dock2[ED_MAX_GOALS]{}; + int data[ED_MAX_GOALS]{}; + + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { + initialize(Ai_info[Ships[ptr->instance].ai_index].goals); + if (!flag) { + flag = 1; + for (i = 0; i < ED_MAX_GOALS; i++) { + behavior[i] = _behavior[i]; + priority[i] = _priority[i]; + subsys[i] = _subsys[i]; + dock2[i] = _dock2[i]; + data[i] = _object[i]; + } + } else { + for (i = 0; i < ED_MAX_GOALS; i++) { + if (behavior[i] != _behavior[i]) { + behavior[i] = -1; + data[i] = -1; } - if (flag & 0x8) { // data is a waypoint name - waypoint* wpt = find_matching_waypoint(goalp[item].target_name); - if (wpt != nullptr) - m_object[item] = wpt->get_objnum() | TYPE_WAYPOINT; + if (data[i] != _object[i]) { + data[i] = -1; + subsys[i] = -1; + dock2[i] = -1; } - if (flag & 0x10) { // data is a ship class - for (i = 0; i < ship_info_size(); i++) { - if (!stricmp(goalp[item].target_name, Ship_info[i].name)) { - m_object[item] = i | TYPE_SHIP_CLASS; - break; - } - } + if (priority[i] != _priority[i]) { + priority[i] = -1; } - - switch (mode) { - case AI_GOAL_DOCK: - m_dock2[item] = -1; - if (m_object[item]) { - docks = - _editor->get_docking_list(Ship_info[Ships[m_object[item] & DATA_MASK].ship_info_index].model_num); - for (i = 0; unsigned(i) < docks.size(); i++) { - Assert(goalp[item].dockee.name); - Assert(goalp[item].dockee.index != -1); - if (!stricmp(goalp[item].dockee.name, docks[i].c_str())) { - m_dock2[item] = i; - break; - } - } - } - break; - default: - break; + if (subsys[i] != _subsys[i]) { + subsys[i] = -1; } - - // Assert(m_data[item]); - } - } - void ShipGoalsDialogModel::initialize_multi() - { - int i, flag = 0; - object* ptr; - int behavior[ED_MAX_GOALS]{}; - int priority[ED_MAX_GOALS]{}; - SCP_string subsys[ED_MAX_GOALS]{}; - int dock2[ED_MAX_GOALS]{}; - int data[ED_MAX_GOALS]{}; - - ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { - initialize(Ai_info[Ships[ptr->instance].ai_index].goals); - if (!flag) { - flag = 1; - for (i = 0; i < ED_MAX_GOALS; i++) { - behavior[i] = m_behavior[i]; - priority[i] = m_priority[i]; - subsys[i] = m_subsys[i]; - dock2[i] = m_dock2[i]; - data[i] = m_object[i]; - } - - } - else { - for (i = 0; i < ED_MAX_GOALS; i++) { - if (behavior[i] != m_behavior[i]) { - behavior[i] = -1; - data[i] = -1; - } - - if (data[i] != m_object[i]) { - data[i] = -1; - subsys[i] = -1; - dock2[i] = -1; - } - - if (priority[i] != m_priority[i]) { - priority[i] = -1; - } - if (subsys[i] != m_subsys[i]) { - subsys[i] = -1; - } - if (dock2[i] != m_dock2[i]) { - dock2[i] = -1; - } - } - } + if (dock2[i] != _dock2[i]) { + dock2[i] = -1; } - - ptr = GET_NEXT(ptr); } - - goalp = nullptr; - for (i = 0; i < ED_MAX_GOALS; i++) { - m_behavior[i] = behavior[i]; - m_priority[i] = priority[i]; - m_subsys[i] = subsys[i]; - m_dock2[i] = dock2[i]; - m_object[i] = data[i]; - } - } - void ShipGoalsDialogModel::setShip(const int ship) - { - self_ship = ship; - } - int ShipGoalsDialogModel::getShip() const - { - return self_ship; - } - void ShipGoalsDialogModel::setWing(const int data) - { - modify(self_wing, data); - } - int ShipGoalsDialogModel::getWing() const - { - return self_wing; - } - ai_goal* ShipGoalsDialogModel::getGoal() const - { - return goalp; - } - int ShipGoalsDialogModel::getValid(const int pos) const - { - return valid[pos]; - } - const ai_goal_list* ShipGoalsDialogModel::getGoalTypes() - { - return Editor::getAi_goal_list(); - } - int ShipGoalsDialogModel::getGoalsSize() const - { - return Ai_goal_list_size; - } - void ShipGoalsDialogModel::setBehavior(const int pos, const int data) - { - modify(m_behavior[pos], data); - } - int ShipGoalsDialogModel::getBehavior(const int pos) const - { - return m_behavior[pos]; - } - void ShipGoalsDialogModel::setObject(const int pos, const int data) - { - modify(m_object[pos], data); - } - int ShipGoalsDialogModel::getObject(const int pos) const - { - return m_object[pos]; - } - void ShipGoalsDialogModel::setSubsys(const int pos, const SCP_string& data) - { - modify(m_subsys[pos], data); - } - SCP_string ShipGoalsDialogModel::getSubsys(const int pos) const - { - return m_subsys[pos]; - } - void ShipGoalsDialogModel::setDock(const int pos, const long long data) - { - modify(m_dock2[pos], data); - } - int ShipGoalsDialogModel::getDock(const int pos) const - { - return m_dock2[pos]; - } - void ShipGoalsDialogModel::setPriority(const int pos, const int data) - { - modify(m_priority[pos], data); - } - int ShipGoalsDialogModel::getPriority(const int pos) const - { - return m_priority[pos]; } - } // namespace dialogs - } // namespace fred -} // namespace fso \ No newline at end of file + } + + ptr = GET_NEXT(ptr); + } + + _goalp = nullptr; + for (i = 0; i < ED_MAX_GOALS; i++) { + _behavior[i] = behavior[i]; + _priority[i] = priority[i]; + _subsys[i] = subsys[i]; + _dock2[i] = dock2[i]; + _object[i] = data[i]; + } +} + +void ShipGoalsDialogModel::setShip(int shipNum) +{ + _selfShip = shipNum; +} + +int ShipGoalsDialogModel::getShip() const +{ + return _selfShip; +} + +void ShipGoalsDialogModel::setWing(int wingNum) +{ + modify(_selfWing, wingNum); +} + +int ShipGoalsDialogModel::getWing() const +{ + return _selfWing; +} + +ai_goal* ShipGoalsDialogModel::getGoal() const +{ + return _goalp; +} + +int ShipGoalsDialogModel::getValid(int pos) const +{ + return _valid[pos]; +} + +const ai_goal_list* ShipGoalsDialogModel::getGoalTypes() +{ + return Editor::getAi_goal_list(); +} + +int ShipGoalsDialogModel::getGoalsSize() const +{ + return _aiGoalListSize; +} + +void ShipGoalsDialogModel::setBehavior(int index, int behavior) +{ + modify(_behavior[index], behavior); +} + +int ShipGoalsDialogModel::getBehavior(int index) const +{ + return _behavior[index]; +} + +void ShipGoalsDialogModel::setObject(int index, int objNum) +{ + modify(_object[index], objNum); +} + +int ShipGoalsDialogModel::getObject(int index) const +{ + return _object[index]; +} + +void ShipGoalsDialogModel::setSubsys(int index, const SCP_string& subsys) +{ + modify(_subsys[index], subsys); +} + +SCP_string ShipGoalsDialogModel::getSubsys(int index) const +{ + return _subsys[index]; +} + +void ShipGoalsDialogModel::setDock(int index, long long dock) +{ + modify(_dock2[index], dock); +} + +int ShipGoalsDialogModel::getDock(int index) const +{ + return _dock2[index]; +} + +void ShipGoalsDialogModel::setPriority(int index, int priority) +{ + modify(_priority[index], priority); +} + +int ShipGoalsDialogModel::getPriority(int index) const +{ + return _priority[index]; +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.h index 53fad8633c8..67cd74ad8b3 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.h @@ -5,9 +5,8 @@ #include "ai/ai.h" #include "ai/aigoals.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { + constexpr auto ED_MAX_GOALS = MAX_AI_GOALS; constexpr auto MAX_EDITOR_GOAL_PRIORITY = 200; constexpr auto TYPE_PATH = 0x1000; @@ -21,72 +20,63 @@ constexpr auto DATA_MASK = 0x0fff; constexpr auto MAX_VALID = 99; class ShipGoalsDialogModel : public AbstractDialogModel { - private: - int Ai_goal_list_size = Editor::getAigoal_list_size(); - void initialize(ai_goal* goals); - void initialize_multi(); - void init_combo_data(); - - - - int self_ship, self_wing; - int m_behavior[ED_MAX_GOALS]; - int m_object[ED_MAX_GOALS]; - int m_priority[ED_MAX_GOALS]; - SCP_string m_subsys[ED_MAX_GOALS]; - long long m_dock2[ED_MAX_GOALS]; - //int m_data[ED_MAX_GOALS]; - SCP_vector>> m_ai_goal_combo_data; - int valid[MAX_VALID]; - - bool m_multi_edit; - - ai_goal* goalp; - int verify_orders(); - - void update_item(const int item); - + Q_OBJECT public: - ShipGoalsDialogModel(QObject* parent, EditorViewport* viewport, bool multi, int self_ship, int self_wing); + ShipGoalsDialogModel(QObject* parent, EditorViewport* viewport, bool multi, int selfShip, int selfWing); - const SCP_vector>> &get_ai_goal_combo_data(); - ai_goal_mode get_first_mode_from_combo_box(int which_item); + const SCP_vector>>& getAiGoalComboData(); + ai_goal_mode getFirstModeFromComboBox(int whichItem); - void initializeData(bool multi, int self_ship, int self_wing); bool apply() override; void reject() override; - void setShip(const int); - int getShip() const; + void setShip(int shipNum); + int getShip() const; - void setWing(const int); - int getWing() const; - + void setWing(int wingNum); + int getWing() const; - ai_goal* getGoal() const; + ai_goal* getGoal() const; - //All getters take the index of the field thay are changeing + int getValid(int pos) const; + static const ai_goal_list* getGoalTypes(); + int getGoalsSize() const; - int getValid(const int) const; - static const ai_goal_list* getGoalTypes(); - int getGoalsSize() const; + void setBehavior(int index, int behavior); + int getBehavior(int index) const; - void setBehavior(const int, const int); - int getBehavior(const int) const; + void setObject(int index, int objNum); + int getObject(int index) const; - void setObject(const int, const int); - int getObject(const int) const; + void setSubsys(int index, const SCP_string& subsys); + SCP_string getSubsys(int index) const; - void setSubsys(const int, const SCP_string&); - SCP_string getSubsys(const int) const; + void setDock(int index, long long dock); + int getDock(int index) const; - void setDock(const int, const long long); - int getDock(const int) const; + void setPriority(int index, int priority); + int getPriority(int index) const; - void setPriority(const int, const int); - int getPriority(const int) const; + private: // NOLINT(readability-redundant-access-specifiers) + void initializeData(bool multi, int selfShip, int selfWing); + void initialize(ai_goal* goals); + void initializeMulti(); + void initComboData(); + int verifyOrders(); + void updateItem(int item); + + int _aiGoalListSize = Editor::getAigoal_list_size(); + int _selfShip; + int _selfWing; + int _behavior[ED_MAX_GOALS]; + int _object[ED_MAX_GOALS]; + int _priority[ED_MAX_GOALS]; + SCP_string _subsys[ED_MAX_GOALS]; + long long _dock2[ED_MAX_GOALS]; + SCP_vector>> _aiGoalComboData; + int _valid[MAX_VALID]; + bool _multiEdit; + ai_goal* _goalp; }; -} // namespace dialogs -} // namespace fred -} // namespace fso \ No newline at end of file +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipGoalsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipGoalsDialog.cpp index d75a431d61d..4efbcb3ffe4 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipGoalsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipGoalsDialog.cpp @@ -34,7 +34,7 @@ ShipGoalsDialog::ShipGoalsDialog(QWidget* parent, EditorViewport* viewport, bool ui->gridLayout->addWidget(priority[i], row, 5); } - connect(_model.get(), &AbstractDialogModel::modelChanged, this, &ShipGoalsDialog::updateUI); + connect(_model.get(), &AbstractDialogModel::modelChanged, this, &ShipGoalsDialog::updateUi); for (int i = 0; i < ED_MAX_GOALS; i++) { connect(behaviors[i], QOverload::of(&QComboBox::currentIndexChanged), [=](int index) { _model->setBehavior(i, index); @@ -57,7 +57,7 @@ ShipGoalsDialog::ShipGoalsDialog(QWidget* parent, EditorViewport* viewport, bool }); } - updateUI(); + updateUi(); // Resize the dialog to the minimum size resize(QDialog::sizeHint()); @@ -97,11 +97,11 @@ void ShipGoalsDialog::on_cancelButton_clicked() { reject(); } -void ShipGoalsDialog::updateUI() +void ShipGoalsDialog::updateUi() { - if (m_updating_ui) + if (_updatingUi) return; - m_updating_ui = true; + _updatingUi = true; util::SignalBlockers blockers(this); for (int i = 0; i < ED_MAX_GOALS; i++) { @@ -110,14 +110,14 @@ void ShipGoalsDialog::updateUI() subsys[i]->clear(); docks[i]->clear(); - for (const auto& entry : _model->get_ai_goal_combo_data()) { + for (const auto& entry : _model->getAiGoalComboData()) { behaviors[i]->addItem(entry.first); } auto value = _model->getBehavior(i); behaviors[i]->setCurrentIndex(value); - auto mode = _model->get_first_mode_from_combo_box(i); + auto mode = _model->getFirstModeFromComboBox(i); SCP_vector::iterator ii; if (value < 1) { @@ -332,6 +332,6 @@ void ShipGoalsDialog::updateUI() } } } - m_updating_ui = false; + _updatingUi = false; } } // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipGoalsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipGoalsDialog.h index 3b359c2f83e..b10a7ac89ca 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipGoalsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipGoalsDialog.h @@ -1,5 +1,4 @@ -#ifndef SHIPGOALSDIALOG_H -#define SHIPGOALSDIALOG_H +#pragma once #include @@ -26,7 +25,7 @@ class ShipGoalsDialog : public QDialog { void on_okButton_clicked(); void on_cancelButton_clicked(); - private:// NOLINT(readability-redundant-access-specifiers) + private: // NOLINT(readability-redundant-access-specifiers) std::unique_ptr ui; std::unique_ptr _model; EditorViewport* _viewport; @@ -37,8 +36,7 @@ class ShipGoalsDialog : public QDialog { QComboBox* docks[ED_MAX_GOALS]; QSpinBox* priority[ED_MAX_GOALS]; - void updateUI(); - bool m_updating_ui = false; + void updateUi(); + bool _updatingUi = false; }; } // namespace fso::fred::dialogs -#endif // !SHIPGOALSDIALOG_H \ No newline at end of file From 6ddc6c519d892234cfae558a0f221a90ace59abb Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Thu, 14 May 2026 06:55:47 -0500 Subject: [PATCH 49/65] qtfred always generates sexps file (#7452) --- qtfred/src/mission/management.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qtfred/src/mission/management.cpp b/qtfred/src/mission/management.cpp index 6db702bed5e..fc70ad4c83b 100644 --- a/qtfred/src/mission/management.cpp +++ b/qtfred/src/mission/management.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -198,6 +199,11 @@ initialize(const std::string& cfilepath, int argc, char* argv[], Editor* editor, listener(SubSystem::SEXPs); sexp_startup(); + // Regenerate sexps.html for the Help Topics dialog. Mirrors the FS2 -output_sexps + // behavior so QtFRED's SEXP Operator Reference always reflects the current operator + // table in the help dialog + output_sexps("sexps.html"); + listener(SubSystem::Objects); obj_init(); From 20b55eb58c6abfa079270ce3b62201e27aea46a9 Mon Sep 17 00:00:00 2001 From: Shivansps Date: Thu, 14 May 2026 09:07:11 -0300 Subject: [PATCH 50/65] TTS Speech rework (#7357) * add imgui speech options * adapt existing windows sapi speech implementation * adapt existing mac speech integration * add speech linux stubs * add speech support in linux * Add array checks * Use dlopen for speech-dispatcher * corrrect lib name * missing includes and static cast * do not change mac file type * fix clang tidy warnings 1 * set tts rate * set localization ids * fix clang tidy warnings 2 * correct symbol name * Remove voice cache and fix win enumerate_voices overriding voice selection * fix mac rate Done by notimaginative * requested changes * re-add voice cache for linux * Open connection for linux get flags * fix missing } * change voice option combobox to std::pair * delete duplicated voice id sanitizer on windows set voice * Use pairs for speech_enumerate_voices() and adapt linux speech * use reference * actually free vector memory * Clear voice cache when ingame options closes * Use extern to call fsspeech_options_cleanup() * update localization ids * Fixing some linux and window speech oversights * Apply volume and rate after a voice change, correct voice, volume and rate apply order --- CMakeLists.txt | 10 +- cmake/finder/FindSpeech.cmake | 2 + code/cmdline/cmdline.cpp | 2 +- code/localization/localize.cpp | 2 +- code/options/Ingame_Options.cpp | 3 + code/sound/fsspeech.cpp | 254 +++++++++++++++- code/sound/fsspeech.h | 3 + code/sound/speech.cpp | 382 ------------------------ code/sound/speech.h | 12 +- code/sound/speech_linux.cpp | 282 +++++++++++++++++ code/sound/{speech.mm => speech_mac.mm} | 65 ++-- code/sound/speech_win.cpp | 271 +++++++++++++++++ code/source_groups.cmake | 15 +- 13 files changed, 863 insertions(+), 440 deletions(-) delete mode 100644 code/sound/speech.cpp create mode 100644 code/sound/speech_linux.cpp rename code/sound/{speech.mm => speech_mac.mm} (67%) create mode 100644 code/sound/speech_win.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 82075aa4d1b..6acedb4b79c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,9 +74,11 @@ IF(RESET_INSTALL_PREFIX) ENDIF(NOT $ENV{FS2PATH} STREQUAL "") ENDIF(RESET_INSTALL_PREFIX) -IF(WIN32 OR APPLE) +IF(WIN32 OR APPLE OR CMAKE_SYSTEM_NAME STREQUAL "Linux") OPTION(FSO_USE_SPEECH "Use text-to-speach libraries" ON) -ENDIF(WIN32 OR APPLE) +ELSE() + OPTION(FSO_USE_SPEECH "Use text-to-speach libraries" OFF) +ENDIF() IF (WIN32) OPTION(FSO_USE_VOICEREC "Enable voice recognition support" ON) @@ -227,9 +229,7 @@ include(package) include(doxygen) # Print used options to log -IF(WIN32 OR APPLE) - message(STATUS "Using text to speech: ${FSO_USE_SPEECH}") -ENDIF() +message(STATUS "Using text to speech: ${FSO_USE_SPEECH}") IF (WIN32) message(STATUS "Using voice recogition: ${FSO_USE_VOICEREC}") message(STATUS "Building FRED2: ${FSO_BUILD_FRED2}") diff --git a/cmake/finder/FindSpeech.cmake b/cmake/finder/FindSpeech.cmake index b85b5b7fe9a..c7cc6b50b4c 100644 --- a/cmake/finder/FindSpeech.cmake +++ b/cmake/finder/FindSpeech.cmake @@ -11,6 +11,8 @@ if (WIN32) endif() elseif(APPLE) # it should just work +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + # uses speech-dispatcher with dlopen else() message(SEND_ERROR "Text to Speech is not supported on this platform!") endif() diff --git a/code/cmdline/cmdline.cpp b/code/cmdline/cmdline.cpp index 36a90ef9df5..c68a29bdcd9 100644 --- a/code/cmdline/cmdline.cpp +++ b/code/cmdline/cmdline.cpp @@ -1414,7 +1414,7 @@ static json_t* json_get_v1() { auto voices = speech_enumerate_voices(); for (auto& voice : voices) { - json_array_append_new(voices_array, json_string(voice.c_str())); + json_array_append_new(voices_array, json_string(voice.second.c_str())); } json_object_set_new(root, "voices", voices_array); diff --git a/code/localization/localize.cpp b/code/localization/localize.cpp index 207596154fa..12a8d34ca2b 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 1918 // This is the next available ID +// #define XSTR_SIZE 1932 // 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/options/Ingame_Options.cpp b/code/options/Ingame_Options.cpp index 801245458d1..0caebc09c81 100644 --- a/code/options/Ingame_Options.cpp +++ b/code/options/Ingame_Options.cpp @@ -98,8 +98,11 @@ void ingame_options_init() } } +extern void fsspeech_options_cleanup(); + void ingame_options_close() { + fsspeech_options_cleanup(); OCGR.reset(); } diff --git a/code/sound/fsspeech.cpp b/code/sound/fsspeech.cpp index 65ef525bb3a..98e501c132d 100644 --- a/code/sound/fsspeech.cpp +++ b/code/sound/fsspeech.cpp @@ -10,7 +10,7 @@ #include "osapi/osregistry.h" #include "sound/fsspeech.h" #include "sound/speech.h" - +#include "options/Option.h" extern int Cmdline_freespace_no_sound; @@ -30,6 +30,204 @@ const char *FSSpeech_play_id[FSSPEECH_FROM_MAX] = char Speech_buffer[MAX_SPEECH_BUFFER_LEN] = ""; size_t Speech_buffer_len; +static bool ttsrate_change(float new_val, bool initial) +{ + if (initial) { + return false; + } + speech_set_rate(new_val); + return true; +} + +static bool ttsingame_change(bool new_val, bool initial) +{ + if (initial) { + return false; + } + FSSpeech_play_from[FSSPEECH_FROM_INGAME] = new_val; + return true; +} + +static bool ttsmulti_change(bool new_val, bool initial) +{ + if (initial) { + return false; + } + FSSpeech_play_from[FSSPEECH_FROM_MULTI] = new_val; + return true; +} + +static bool ttsbriefing_change(bool new_val, bool initial) +{ + if (initial) { + return false; + } + FSSpeech_play_from[FSSPEECH_FROM_BRIEFING] = new_val; + return true; +} + +static bool ttstechroom_change(bool new_val, bool initial) +{ + if (initial) { + return false; + } + FSSpeech_play_from[FSSPEECH_FROM_TECHROOM] = new_val; + return true; +} + +static bool ttsvolume_change(float new_val, bool initial) +{ + if (initial) { + return false; + } + speech_set_volume((unsigned short) new_val); + return true; +} + +static std::pair ttsvoice_deserializer(const json_t* el) +{ + int id; + const char* name = nullptr; + + json_error_t err; + if (json_unpack_ex((json_t*)el, &err, 0, "{s:i, s:s}", "id", &id, "name", &name) != 0) { + throw json_exception(err); + } + + return std::make_pair(id, name); +} + +static json_t* ttsvoice_serializer(const std::pair& value) +{ + return json_pack("{s:i, s:s}", "id", value.first, "name", value.second.c_str()); +} + +static SCP_vector> voice_list_cache; + +static SCP_vector> ttsvoice_enumerator() +{ + if(voice_list_cache.empty()) { + auto voices = speech_enumerate_voices(); + + if (voices.empty()) { + voices.emplace_back(std::make_pair(0, "No voices loaded")); + } + voice_list_cache = voices; + return voices; + } + else { + return voice_list_cache; + } +} + +static SCP_string ttsvoice_display(const std::pair& vi) +{ + return vi.second; +} + +static auto SpeechVolumeOption = options::OptionBuilder("Speech.Volume", + std::pair{"TTS Volume", 1920}, + std::pair{"Volume used for playing TTS speech", 1921}) + .category(std::make_pair("Audio", 1826)) + .range(0.0f, 100.0f) + .default_val(100.0f) + .change_listener(ttsvolume_change) + .importance(2) + .finish(); + +static auto SpeechRateOption = options::OptionBuilder("Speech.Rate", + std::pair{"TTS Rate", 1922}, + std::pair{"Speed of the TTS voice (100 = normal)", 1923}) + .category(std::make_pair("Audio", 1826)) + .range(50.0f, 150.0f) + .default_val(100.0f) + .change_listener(ttsrate_change) + .importance(1) + .finish(); + +static bool ttsvoice_change(const std::pair& new_voice, bool initial) +{ + if (initial) { + return false; + } + speech_set_voice(new_voice.first); + // Re-apply volume and rate, it is needed on Mac and maybe on other OS as well + speech_set_volume((unsigned short)SpeechVolumeOption->getValue()); + speech_set_rate(SpeechRateOption->getValue()); + return true; +} + +static auto SpeechVoiceOption = options::OptionBuilder>("Speech.Voice", + std::pair{"TTS Voice", 1918}, + std::pair{"The voice used to read text", 1919}) + .category(std::make_pair("Audio", 1826)) + .level(options::ExpertLevel::Beginner) + .default_func([]() { return ttsvoice_enumerator().front(); }) // always guarantees at least 1 value + .enumerator(ttsvoice_enumerator) + .display(ttsvoice_display) + .serializer(ttsvoice_serializer) + .deserializer(ttsvoice_deserializer) + .flags({ options::OptionFlags::ForceMultiValueSelection }) + .change_listener(ttsvoice_change) + .importance(3) + .finish(); + +static auto SpeechBriefingOption = options::OptionBuilder("Speech.Briefing", + std::pair{"TTS in briefings", 1924}, + std::pair{"Enable or disable TTS in briefings", 1925}) + .category(std::make_pair("Audio", 1826)) + .level(options::ExpertLevel::Beginner) + .change_listener(ttsbriefing_change) + .default_val(true) + .importance(0) + .finish(); + +static auto SpeechTechroomOption = options::OptionBuilder("Speech.Techroom", + std::pair{"TTS in techroom", 1926}, + std::pair{"Enable or disable TTS in techroom", 1927}) + .category(std::make_pair("Audio", 1826)) + .level(options::ExpertLevel::Beginner) + .change_listener(ttstechroom_change) + .default_val(true) + .importance(0) + .finish(); + +static auto SpeechIngameOption = options::OptionBuilder("Speech.Ingame", + std::pair{"TTS in-game", 1928}, + std::pair{"Enable or disable TTS in-game", 1929}) + .category(std::make_pair("Audio", 1826)) + .level(options::ExpertLevel::Beginner) + .change_listener(ttsingame_change) + .default_val(true) + .importance(0) + .finish(); + +static auto SpeechMultiOption = options::OptionBuilder("Speech.Multi", + std::pair{"TTS in multiplayer", 1930}, + std::pair{"Enable or disable TTS in multiplayer", 1931}) + .category(std::make_pair("Audio", 1826)) + .level(options::ExpertLevel::Beginner) + .change_listener(ttsmulti_change) + .default_val(true) + .importance(0) + .finish(); + +void sanitize_text(const char* input, SCP_string& output) { + output.clear(); + bool saw_dollar = false; + for (auto ch : unicode::codepoint_range(input)) { + if (ch == UNICODE_CHAR('$')) { + saw_dollar = true; + continue; + } + else if (saw_dollar) { + saw_dollar = false; + continue; + } + unicode::encode(ch, std::back_inserter(output)); + } +} + bool fsspeech_init() { if (speech_inited) { @@ -45,18 +243,34 @@ bool fsspeech_init() return false; } - // Get the settings from the registry - for(int i = 0; i < FSSPEECH_FROM_MAX; i++) { - FSSpeech_play_from[i] = - os_config_read_uint(NULL, FSSpeech_play_id[i], 0) ? true : false; - nprintf(("Speech", "Play %s: %s\n", FSSpeech_play_id[i], FSSpeech_play_from[i] ? "true" : "false")); + if (Using_in_game_options) + { + FSSpeech_play_from[FSSPEECH_FROM_TECHROOM] = SpeechTechroomOption->getValue(); + FSSpeech_play_from[FSSPEECH_FROM_BRIEFING] = SpeechBriefingOption->getValue(); + FSSpeech_play_from[FSSPEECH_FROM_INGAME] = SpeechIngameOption->getValue(); + FSSpeech_play_from[FSSPEECH_FROM_MULTI] = SpeechMultiOption->getValue(); + // The apply order must be Voice->Volume/Rate to avoid issues on Mac. + speech_set_voice(SpeechVoiceOption->getValue().first); + speech_set_volume((unsigned short)SpeechVolumeOption->getValue()); + speech_set_rate(SpeechRateOption->getValue()); + } + else + { + // Get the settings from the registry + for (int i = 0; i < FSSPEECH_FROM_MAX; i++) { + FSSpeech_play_from[i] = static_cast(os_config_read_uint(nullptr, FSSpeech_play_id[i], 0)); + nprintf(("Speech", "Play %s: %s\n", FSSpeech_play_id[i], FSSpeech_play_from[i] ? "true" : "false")); + } + + int voice = os_config_read_uint(nullptr, "SpeechVoice", 0); + speech_set_voice(voice); + + int volume = os_config_read_uint(nullptr, "SpeechVolume", 100); + speech_set_volume((unsigned short)volume); + + int rate = os_config_read_uint(nullptr, "SpeechRate", 100); + speech_set_rate(static_cast(rate)); } - - int volume = os_config_read_uint(NULL, "SpeechVolume", 100); - speech_set_volume((unsigned short) volume); - - int voice = os_config_read_uint(NULL, "SpeechVoice", 0); - speech_set_voice(voice); speech_inited = 1; @@ -75,6 +289,11 @@ void fsspeech_deinit() void fsspeech_play(int type, const char *text) { + if (text == nullptr) { + nprintf(("Speech", "Not playing speech because passed text is null.\n")); + return; + } + if (!speech_inited) { nprintf(("Speech", "Aborting fsspech_play because speech_inited is false.\n")); return; @@ -90,7 +309,10 @@ void fsspeech_play(int type, const char *text) return; } - speech_play(text); + SCP_string sanitized_string; + sanitize_text(text, sanitized_string); + + speech_play(sanitized_string); } void fsspeech_stop() @@ -157,3 +379,9 @@ bool fsspeech_playing() return speech_is_speaking(); } + +void fsspeech_options_cleanup() +{ + voice_list_cache.clear(); + voice_list_cache.shrink_to_fit(); +} diff --git a/code/sound/fsspeech.h b/code/sound/fsspeech.h index 874b0c37468..cd80b3515bd 100644 --- a/code/sound/fsspeech.h +++ b/code/sound/fsspeech.h @@ -31,4 +31,7 @@ void fsspeech_play_buffer(int type); bool fsspeech_play_from(int type); bool fsspeech_playing(); +// Cleanup the voice cache after the options menu is closed +void fsspeech_options_cleanup(); + #endif // header define diff --git a/code/sound/speech.cpp b/code/sound/speech.cpp deleted file mode 100644 index 7967950ac10..00000000000 --- a/code/sound/speech.cpp +++ /dev/null @@ -1,382 +0,0 @@ -/* - * Code created by Thomas Whittaker (RT) for a FreeSpace 2 source code project - * - * You may not sell or otherwise commercially exploit the source or things you - * created based on the source. - * -*/ - - - - - -#ifndef FS2_SPEECH -#if defined(_WIN32) || defined(__APPLE__) -#if NDEBUG - #pragma message( "WARNING: You have not compiled speech into this build (use FS2_SPEECH)" ) -#endif // NDEBUG -#endif // _WIN32 or __APPLE__ -#elif !defined(__APPLE__) // to end-of-file ... - - -#ifdef LAUNCHER -#include "stdafx.h" -#endif //LAUNCHER - -#ifdef _WIN32 - -// Since we define these ourself we need to undefine them for the sapi header -#pragma push_macro("strcpy_s") -#pragma push_macro("strncpy_s") -#pragma push_macro("strcat_s") -#pragma push_macro("memset") -#pragma push_macro("memcpy") -#undef strcpy_s -#undef strncpy_s -#undef strcat_s -#undef memset -#undef memcpy - - #include - #include - - #include - -#pragma pushpop_macro("strcpy_s") -#pragma pushpop_macro("strncpy_s") -#pragma pushpop_macro("strcat_s") -#pragma pushpop_macro("memset") -#pragma pushpop_macro("memcpy") - - ISpVoice *Voice_device; -#elif defined(SCP_UNIX) - #include -// #include - - int speech_dev = -1; -// FILE *speech_dev = NULL; -#else - #pragma error( "ERROR: Unknown platform, speech (FS2_SPEECH) is not supported" ) -#endif //_WIN32 - -#pragma warning(push) -#pragma warning(disable: 4995) -// Visual Studio complains that some functions are deprecated so this fixes that -#include -#include -#include -#pragma warning(pop) - -#include "globalincs/pstypes.h" -#include "utils/unicode.h" -#include "speech.h" - - -bool Speech_init = false; - -bool speech_init() -{ -#ifdef _WIN32 - HRESULT hr = CoCreateInstance( - CLSID_SpVoice, - NULL, - CLSCTX_ALL, - IID_ISpVoice, - (void **)&Voice_device); - - Speech_init = SUCCEEDED(hr); -#else - - speech_dev = open("/dev/speech", O_WRONLY | O_DIRECT); -// speech_dev = fopen("/dev/speech", "w"); - - if (speech_dev == -1) { -// if (speech_dev == NULL) { - mprintf(("Couldn't open '/dev/speech', turning text-to-speech off...\n")); - return false; - } - - Speech_init = true; -#endif - - nprintf(("Speech", "Speech init %s\n", Speech_init ? "succeeded!" : "failed!")); - return Speech_init; -} - -void speech_deinit() -{ - if(Speech_init == false) return; - -#ifdef _WIN32 - Voice_device->Release(); -#else - close(speech_dev); -// fclose(speech_dev); -#endif -} - -bool speech_play(const char *text) -{ - nprintf(("Speech", "Attempting to play speech string %s...\n", text)); - - if(Speech_init == false) return true; - if (text == NULL) { - nprintf(("Speech", "Not playing speech because passed text is null.\n")); - return false; - } - -#ifdef _WIN32 - SCP_string work_buffer; - - bool saw_dollar = false; - for (auto ch : unicode::codepoint_range(text)) { - if (ch == UNICODE_CHAR('$')) { - // Skip $ escape sequences which appear in briefing text - saw_dollar = true; - continue; - } else if (saw_dollar) { - saw_dollar = false; - continue; - } - - unicode::encode(ch, std::back_inserter(work_buffer)); - } - - // Determine the needed amount of data - auto num_chars = MultiByteToWideChar(CP_UTF8, 0, work_buffer.c_str(), (int) work_buffer.size(), nullptr, 0); - - if (num_chars <= 0) { - // Error - return false; - } - - std::wstring wide_string; - wide_string.resize(num_chars); - - auto err = MultiByteToWideChar(CP_UTF8, 0, work_buffer.c_str(), (int)work_buffer.size(), &wide_string[0], num_chars); - - if (err <= 0) { - return false; - } - - speech_stop(); - return SUCCEEDED(Voice_device->Speak(wide_string.c_str(), SPF_ASYNC, NULL)); -#else - int len = strlen(text); - char Conversion_buffer[MAX_SPEECH_CHAR_LEN]; - - if(len > (MAX_SPEECH_CHAR_LEN - 1)) { - len = MAX_SPEECH_CHAR_LEN - 1; - } - - int count = 0; - for(int i = 0; i < len; i++) { - if(text[i] == '$') { - i++; - continue; - } - - Conversion_buffer[count] = text[i]; - count++; - } - - Conversion_buffer[count] = '\0'; - - if ( write(speech_dev, Conversion_buffer, count) == -1 ) - return false; -// if (fwrite(Conversion_buffer, count, 1, speech_dev)) -// fflush(speech_dev); -// else -// return false; - - return true; -#endif //_WIN32 -} - -bool speech_pause() -{ - if(Speech_init == false) return true; -#ifdef _WIN32 - return SUCCEEDED(Voice_device->Pause()); -#else - STUB_FUNCTION; - - return true; -#endif -} - -bool speech_resume() -{ - if(Speech_init == false) return true; -#ifdef _WIN32 - return SUCCEEDED(Voice_device->Resume()); -#else - STUB_FUNCTION; - - return true; -#endif -} - -bool speech_stop() -{ - if(Speech_init == false) return true; -#ifdef _WIN32 - return SUCCEEDED(Voice_device->Speak( NULL, SPF_PURGEBEFORESPEAK, NULL )); -#else - STUB_FUNCTION; - - return true; -#endif -} - -bool speech_set_volume(unsigned short volume) -{ -#ifdef _WIN32 - return SUCCEEDED(Voice_device->SetVolume(volume)); -#else - STUB_FUNCTION; - - return true; -#endif -} - -bool speech_set_voice(int voice) -{ -#ifdef _WIN32 - HRESULT hr; - CComPtr cpVoiceToken; - CComPtr cpEnum; - ULONG num_voices = 0; - - //Enumerate the available voices - hr = SpEnumTokens(SPCAT_VOICES, NULL, NULL, &cpEnum); - - if(FAILED(hr)) return false; - - hr = cpEnum->GetCount(&num_voices); - - if(FAILED(hr)) return false; - - int count = 0; - // Obtain a list of available voice tokens, set the voice to the token, and call Speak - while (num_voices -- ) - { - cpVoiceToken.Release(); - - hr = cpEnum->Next( 1, &cpVoiceToken, NULL ); - - if(FAILED(hr)) { - return false; - } - - if(count == voice) { - return SUCCEEDED(Voice_device->SetVoice(cpVoiceToken)); - } - - count++; - } - return false; -#else - STUB_FUNCTION; - - return true; -#endif -} - -// Goober5000 -bool speech_is_speaking() -{ -#ifdef _WIN32 - HRESULT hr; - SPVOICESTATUS pStatus; - - hr = Voice_device->GetStatus(&pStatus, NULL); - if (FAILED(hr)) return false; - - return (pStatus.dwRunningState != SPRS_DONE); -#else - STUB_FUNCTION; - - return false; -#endif -} - -SCP_vector speech_enumerate_voices() -{ -#ifdef _WIN32 - HRESULT hr = CoCreateInstance( - CLSID_SpVoice, - NULL, - CLSCTX_ALL, - IID_ISpVoice, - (void **)&Voice_device); - - if (FAILED(hr)) { - return SCP_vector(); - } - - // This code is mostly copied from wxLauncher - ISpObjectTokenCategory * comTokenCategory = NULL; - IEnumSpObjectTokens * comVoices = NULL; - ULONG comVoicesCount = 0; - - // Generate enumeration of voices - hr = ::CoCreateInstance(CLSID_SpObjectTokenCategory, NULL, - CLSCTX_INPROC_SERVER, IID_ISpObjectTokenCategory, (LPVOID*)&comTokenCategory); - if (FAILED(hr)) { - return SCP_vector(); - } - - hr = comTokenCategory->SetId(SPCAT_VOICES, false); - if (FAILED(hr)) { - return SCP_vector(); - } - - hr = comTokenCategory->EnumTokens(NULL, NULL, &comVoices); - if (FAILED(hr)) { - return SCP_vector(); - } - - hr = comVoices->GetCount(&comVoicesCount); - if (FAILED(hr)) { - return SCP_vector(); - } - - SCP_vector voices; - while (comVoicesCount > 0) { - ISpObjectToken * comAVoice = NULL; - - comVoices->Next(1, &comAVoice, NULL); // retrieve just one - - LPWSTR id = NULL; - comAVoice->GetStringValue(NULL, &id); - - auto idlength = wcslen(id); - auto buffer_size = WideCharToMultiByte(CP_UTF8, 0, id, (int)idlength, nullptr, 0, nullptr, nullptr); - - if (buffer_size > 0) { - SCP_string voiceName; - voiceName.resize(buffer_size); - buffer_size = WideCharToMultiByte(CP_UTF8, 0, id, (int)idlength, &voiceName[0], buffer_size, nullptr, nullptr); - - voices.push_back(voiceName); - } - - CoTaskMemFree(id); - comAVoice->Release(); - comVoicesCount--; - } - - comTokenCategory->Release(); - - Voice_device->Release(); - - return voices; -#else - STUB_FUNCTION; - - return SCP_vector(); -#endif -} - -#endif // FS2_SPEECH diff --git a/code/sound/speech.h b/code/sound/speech.h index 3f731dd5a7f..07d7d9debf6 100644 --- a/code/sound/speech.h +++ b/code/sound/speech.h @@ -15,32 +15,34 @@ bool speech_init(); void speech_deinit(); -bool speech_play(const char *text); +bool speech_play(const SCP_string& text); bool speech_pause(); bool speech_resume(); bool speech_stop(); bool speech_set_volume(unsigned short volume); bool speech_set_voice(int voice); +bool speech_set_rate(float rate); bool speech_is_speaking(); -SCP_vector speech_enumerate_voices(); +SCP_vector> speech_enumerate_voices(); #else inline bool speech_init() { return false; } inline void speech_deinit() {} -inline bool speech_play(const char* /*text*/) { return false; } +inline bool speech_play(const SCP_string& /*text*/) { return false; } inline bool speech_pause() { return false; } inline bool speech_resume() { return false; } inline bool speech_stop() { return false; } inline bool speech_set_volume(unsigned short /*volume*/) { return false; } inline bool speech_set_voice(int /*voice*/) { return false; } +inline bool speech_set_rate(float /*rate*/) { return false; } inline bool speech_is_speaking() { return false; } -inline SCP_vector speech_enumerate_voices() { - return SCP_vector(); +inline SCP_vector> speech_enumerate_voices() { + return SCP_vector>(); } #endif diff --git a/code/sound/speech_linux.cpp b/code/sound/speech_linux.cpp new file mode 100644 index 00000000000..e2e2c629955 --- /dev/null +++ b/code/sound/speech_linux.cpp @@ -0,0 +1,282 @@ +#ifdef FS2_SPEECH +#include +#include "globalincs/pstypes.h" +#include "utils/unicode.h" +#include "speech.h" + +// Adapted from libspeechd.h / speechd_types.h +// https://github.com/brailcom/speechd/tree/master/src/api/c + +typedef struct SPDConnection SPDConnection; + +typedef struct { + char *name; + char *language; + char *variant; +} SPDVoice; + +typedef enum { + SPD_MODE_SINGLE = 0, + SPD_MODE_THREADED = 1 +} SPDConnectionMode; + +typedef enum { + SPD_IMPORTANT = 1, + SPD_MESSAGE = 2, + SPD_TEXT = 3, + SPD_NOTIFICATION = 4, + SPD_PROGRESS = 5 +} SPDPriority; + +static void* lib_handle = nullptr; + +typedef SPDConnection* (*pfn_spd_open)(const char*, const char*, const char*, SPDConnectionMode); +typedef void (*pfn_spd_close)(SPDConnection*); +typedef int (*pfn_spd_say)(SPDConnection*, SPDPriority, const char*); +typedef int (*pfn_spd_pause)(SPDConnection*); +typedef int (*pfn_spd_resume)(SPDConnection*); +typedef int (*pfn_spd_stop)(SPDConnection*); +typedef int (*pfn_spd_set_volume)(SPDConnection*, signed int); +typedef int (*pfn_spd_set_synthesis_voice)(SPDConnection*, const char*); +typedef int (*pfn_spd_set_voice_rate)(SPDConnection*, signed int); +typedef SPDVoice** (*pfn_spd_list_synthesis_voices)(SPDConnection*); +typedef void (*pfn_free_spd_voices)(SPDVoice**); + +static pfn_spd_open p_spd_open = nullptr; +static pfn_spd_close p_spd_close = nullptr; +static pfn_spd_say p_spd_say = nullptr; +static pfn_spd_pause p_spd_pause = nullptr; +static pfn_spd_resume p_spd_resume = nullptr; +static pfn_spd_stop p_spd_stop = nullptr; +static pfn_spd_set_volume p_spd_set_volume = nullptr; +static pfn_spd_set_synthesis_voice p_spd_set_synthesis_voice = nullptr; +static pfn_spd_list_synthesis_voices p_spd_list_synthesis_voices = nullptr; +static pfn_spd_set_voice_rate p_spd_set_voice_rate = nullptr; +static pfn_free_spd_voices p_free_spd_voices = nullptr; + +// Load speech-dispatcher with dlopen and load symbols +static bool ensure_speechd_lib() +{ + if (lib_handle) return true; + lib_handle = dlopen("libspeechd.so.2", RTLD_LAZY | RTLD_LOCAL); + if (!lib_handle) { + lib_handle = dlopen("libspeechd.so", RTLD_LAZY | RTLD_LOCAL); + } + + if (!lib_handle) { + mprintf(("Speech: Unable to load libspeechd.so: %s\n", dlerror())); + return false; + } + + // used symbols + p_spd_open = (pfn_spd_open) dlsym(lib_handle, "spd_open"); + p_spd_close = (pfn_spd_close) dlsym(lib_handle, "spd_close"); + p_spd_say = (pfn_spd_say) dlsym(lib_handle, "spd_say"); + p_spd_pause = (pfn_spd_pause) dlsym(lib_handle, "spd_pause"); + p_spd_resume = (pfn_spd_resume) dlsym(lib_handle, "spd_resume"); + p_spd_stop = (pfn_spd_stop) dlsym(lib_handle, "spd_stop"); + p_spd_set_volume = (pfn_spd_set_volume) dlsym(lib_handle, "spd_set_volume"); + p_spd_set_synthesis_voice = (pfn_spd_set_synthesis_voice) dlsym(lib_handle, "spd_set_synthesis_voice"); + p_spd_list_synthesis_voices = (pfn_spd_list_synthesis_voices) dlsym(lib_handle, "spd_list_synthesis_voices"); + p_spd_set_voice_rate = (pfn_spd_set_voice_rate) dlsym(lib_handle, "spd_set_voice_rate"); + p_free_spd_voices = (pfn_free_spd_voices) dlsym(lib_handle, "free_spd_voices"); + + if (!p_spd_open || !p_spd_close || !p_spd_say || !p_spd_pause || !p_spd_resume || !p_spd_stop || !p_spd_set_volume + || !p_spd_set_voice_rate || !p_spd_set_synthesis_voice || !p_spd_list_synthesis_voices || !p_free_spd_voices) { + mprintf(("Speech: Unable to load one or more symbols from libspeechd.so: %s\n", dlerror())); + dlclose(lib_handle); + lib_handle = nullptr; + return false; + } + + return true; +} + +// Speech handling starts here +static bool Speech_init = false; +static SPDConnection* spd = nullptr; + +bool speech_init() +{ + if (Speech_init) { + return true; + } + + if (!ensure_speechd_lib()) { + return false; + } + + spd = p_spd_open("freespace_open", "main", nullptr, SPD_MODE_SINGLE); + if (!spd) { + mprintf(("Speech: Unable to connect to speech-dispatcher\n")); + if (lib_handle) { + dlclose(lib_handle); + lib_handle = nullptr; + } + return false; + } + + Speech_init = true; + return true; +} + +void speech_deinit() +{ + if ( !Speech_init ) { + return; + } + p_spd_close(spd); + Speech_init = false; + spd = nullptr; + if (lib_handle) { + dlclose(lib_handle); + lib_handle = nullptr; + } +} + +bool speech_play(const SCP_string& text) +{ + if ( !Speech_init ) { + return false; + } + + if (text.empty()) { + nprintf(("Speech", "Not playing speech because passed text is empty.\n")); + return false; + } + + return (p_spd_say(spd, SPD_TEXT, text.c_str()) >= 0); +} + +bool speech_pause() +{ + if ( !Speech_init ) { + return false; + } + + p_spd_pause(spd); + + return true; +} + +bool speech_resume() +{ + if ( !Speech_init ) { + return false; + } + + p_spd_resume(spd); + + return true; +} + +bool speech_stop() +{ + if ( !Speech_init ) { + return false; + } + + p_spd_stop(spd); + + return true; +} + +bool speech_set_volume(unsigned short volume) +{ + if ( !Speech_init ) { + return false; + } + + p_spd_set_volume(spd, volume); + + return true; +} + +bool speech_set_voice(int voice) +{ + if ( !Speech_init ) { + return false; + } + + auto voices = speech_enumerate_voices(); + + if (voice < 0 || static_cast(voice) >= voices.size()) { + return false; + } + + p_spd_set_synthesis_voice(spd, voices[voice].second.c_str()); + + return true; +} + +bool speech_set_rate(float rate_percent) +{ + if (!Speech_init) { + return false; + } + + // 50 / +150 -> 100 = normal -> range -100 / +100 + auto rate = static_cast(rate_percent - 100.0f); + CAP(rate, -100, 100); + + p_spd_set_voice_rate(spd, rate); + return true; +} + +/* + TODO: Try to implement this in some way, + there is no simple way to do it. +*/ +bool speech_is_speaking() +{ + if ( !Speech_init ) { + return false; + } + + return false; +} + +SCP_vector> speech_enumerate_voices() +{ + SCP_vector> fsoVoices; + + if (!Speech_init) { + if (!ensure_speechd_lib()) { + return fsoVoices; + } + spd = p_spd_open("freespace_open", "main", nullptr, SPD_MODE_SINGLE); + if (!spd) { + mprintf(("Speech: Unable to connect to speech-dispatcher\n")); + if (lib_handle) { + dlclose(lib_handle); + lib_handle = nullptr; + } + return fsoVoices; + } + } + + SPDVoice** voices = p_spd_list_synthesis_voices(spd); + + if (voices) { + for (int i = 0; voices[i] != nullptr; i++) { + fsoVoices.emplace_back(std::make_pair(i, voices[i]->name)); + } + p_free_spd_voices(voices); + } + else { + mprintf(("Speech: Unable to get voice list from speech-dispatcher.\n")); + } + + if (!Speech_init) { + p_spd_close(spd); + spd = nullptr; + if (lib_handle) { + dlclose(lib_handle); + lib_handle = nullptr; + } + } + + return fsoVoices; +} + +#endif diff --git a/code/sound/speech.mm b/code/sound/speech_mac.mm similarity index 67% rename from code/sound/speech.mm rename to code/sound/speech_mac.mm index 0cb45534028..cb18966ca37 100644 --- a/code/sound/speech.mm +++ b/code/sound/speech_mac.mm @@ -5,11 +5,11 @@ #include "globalincs/pstypes.h" #include "utils/unicode.h" - +#include "speech.h" static NSSpeechSynthesizer *synth = nil; static bool Speech_init = false; - +static int voice_default_rate = 200; bool speech_init() { @@ -36,40 +36,20 @@ void speech_deinit() Speech_init = false; } -bool speech_play(const char *text) +bool speech_play(const SCP_string& text) { if ( !Speech_init ) { return false; } - if ( !text || !strlen(text) ) { - nprintf(("Speech", "Not playing speech because passed text is null.\n")); - return false; - } - - SCP_string work_buffer; - - bool saw_dollar = false; - for (auto ch : unicode::codepoint_range(text)) { - if (ch == UNICODE_CHAR('$')) { - // Skip $ escape sequences which appear in briefing text - saw_dollar = true; - continue; - } else if (saw_dollar) { - saw_dollar = false; - continue; - } - - unicode::encode(ch, std::back_inserter(work_buffer)); - } - - if (work_buffer.empty()) { + if (text.empty()) { + nprintf(("Speech", "Not playing speech because passed text is empty.\n")); return false; } [synth startSpeakingString: [NSString stringWithUTF8String: - work_buffer.c_str() + text.c_str() ] ]; @@ -140,9 +120,34 @@ bool speech_set_voice(int voice) [synth setVoice: [voices objectAtIndex:voice]]; + // reset voice to defaults + [synth setObject:nil forProperty:NSSpeechResetProperty error:nil]; + + // get default rate for voice + NSNumber *voiceRate = [synth objectForProperty:NSSpeechRateProperty error:nil]; + voice_default_rate = voiceRate ? [voiceRate intValue] : 200; // median normal rate as default + return true; } +bool speech_set_rate(float rate_percent) +{ + if (!Speech_init) { + return false; + } + + CAP(rate_percent, 25.0f, 300.f); + + int rate = fl2i(voice_default_rate * (rate_percent / 100.0f)); + + [synth + setObject:[NSNumber numberWithInt:rate] + forProperty:NSSpeechRateProperty error:nil + ]; + + return true; +} + bool speech_is_speaking() { if ( !Speech_init ) { @@ -152,17 +157,17 @@ bool speech_is_speaking() return [synth isSpeaking]; } -SCP_vector speech_enumerate_voices() +SCP_vector> speech_enumerate_voices() { NSArray *voices = [NSSpeechSynthesizer availableVoices]; - SCP_vector fsoVoices; + SCP_vector> fsoVoices; + int voiceID = 0; for (NSString *voiceIdentifier in voices) { NSDictionary *attributes = [NSSpeechSynthesizer attributesForVoice:voiceIdentifier]; NSString *name = [attributes objectForKey:NSVoiceName]; - - fsoVoices.push_back([name UTF8String]); + fsoVoices.emplace_back(std::make_pair(voiceID++, [name UTF8String])); } return fsoVoices; diff --git a/code/sound/speech_win.cpp b/code/sound/speech_win.cpp new file mode 100644 index 00000000000..60cad8c4a64 --- /dev/null +++ b/code/sound/speech_win.cpp @@ -0,0 +1,271 @@ +/* + * Code created by Thomas Whittaker (RT) for a FreeSpace 2 source code project + * + * You may not sell or otherwise commercially exploit the source or things you + * created based on the source. + * +*/ +#ifndef FS2_SPEECH +#if defined(_WIN32) +#if NDEBUG + #pragma message( "WARNING: You have not compiled speech into this build (use FS2_SPEECH)" ) +#endif // NDEBUG +#endif // _WIN32 +#elif defined(_WIN32) // FS2_SPEECH + +#ifdef LAUNCHER +#include "stdafx.h" +#endif //LAUNCHER + +// Since we define these ourself we need to undefine them for the sapi header +#pragma push_macro("strcpy_s") +#pragma push_macro("strncpy_s") +#pragma push_macro("strcat_s") +#pragma push_macro("memset") +#pragma push_macro("memcpy") +#undef strcpy_s +#undef strncpy_s +#undef strcat_s +#undef memset +#undef memcpy + +#include +#include +#include + +#pragma pushpop_macro("strcpy_s") +#pragma pushpop_macro("strncpy_s") +#pragma pushpop_macro("strcat_s") +#pragma pushpop_macro("memset") +#pragma pushpop_macro("memcpy") + +ISpVoice *Voice_device; + +#pragma warning(push) +#pragma warning(disable: 4995) +// Visual Studio complains that some functions are deprecated so this fixes that +#include +#include +#include +#pragma warning(pop) +#include "globalincs/pstypes.h" +#include "utils/unicode.h" +#include "speech.h" + +bool Speech_init = false; + +bool speech_init() +{ + HRESULT hr = CoCreateInstance( + CLSID_SpVoice, + nullptr, + CLSCTX_ALL, + IID_ISpVoice, + (void **)&Voice_device); + + Speech_init = SUCCEEDED(hr); + + nprintf(("Speech", "Speech init %s\n", Speech_init ? "succeeded!" : "failed!")); + return Speech_init; +} + +void speech_deinit() +{ + if(Speech_init == false) return; + Voice_device->Release(); +} + +bool speech_play(const SCP_string& text) +{ + nprintf(("Speech", "Attempting to play speech string %s...\n", text.c_str())); + + if(Speech_init == false) return true; + + if (text.empty()) { + nprintf(("Speech", "Not playing speech because passed text is empty.\n")); + return false; + } + + // Determine the needed amount of data + auto num_chars = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), (int)text.size(), nullptr, 0); + + if (num_chars <= 0) { + // Error + return false; + } + + std::wstring wide_string; + wide_string.resize(num_chars); + + auto err = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), (int)text.size(), &wide_string[0], num_chars); + + if (err <= 0) { + return false; + } + + speech_stop(); + return SUCCEEDED(Voice_device->Speak(wide_string.c_str(), SPF_ASYNC, nullptr)); +} + +bool speech_pause() +{ + if(Speech_init == false) return true; + return SUCCEEDED(Voice_device->Pause()); +} + +bool speech_resume() +{ + if(Speech_init == false) return true; + return SUCCEEDED(Voice_device->Resume()); +} + +bool speech_stop() +{ + if(Speech_init == false) return true; + return SUCCEEDED(Voice_device->Speak(nullptr, SPF_PURGEBEFORESPEAK, nullptr)); +} + +bool speech_set_volume(unsigned short volume) +{ + if (Speech_init == false) return true; + return SUCCEEDED(Voice_device->SetVolume(volume)); +} + +bool speech_set_voice(int voice) +{ + if (Speech_init == false) + return false; + + HRESULT hr; + CComPtr cpVoiceToken; + CComPtr cpEnum; + ULONG num_voices = 0; + + //Enumerate the available voices + hr = SpEnumTokens(SPCAT_VOICES, nullptr, nullptr, &cpEnum); + + if(FAILED(hr)) return false; + + hr = cpEnum->GetCount(&num_voices); + + if(FAILED(hr)) return false; + + int count = 0; + // Obtain a list of available voice tokens, set the voice to the token, and call Speak + while (num_voices -- ) + { + cpVoiceToken.Release(); + + hr = cpEnum->Next( 1, &cpVoiceToken, nullptr); + + if(FAILED(hr)) { + return false; + } + + if(count == voice) { + return SUCCEEDED(Voice_device->SetVoice(cpVoiceToken)); + } + + count++; + } + return false; +} + +bool speech_set_rate(float rate_percent) +{ + if (!Speech_init) { + return false; + } + + // 50 / +150 -> 100 = normal -> range -10 / +10 + auto rate = static_cast((rate_percent - 100.0f) * 0.1f); + if (rate < -10) { + rate = -10; + } + else if (rate > 10) { + rate = 10; + } + + return SUCCEEDED(Voice_device->SetRate(rate)); +} + +// Goober5000 +bool speech_is_speaking() +{ + if (Speech_init == false) return false; + HRESULT hr; + SPVOICESTATUS pStatus; + + hr = Voice_device->GetStatus(&pStatus, nullptr); + if (FAILED(hr)) return false; + + return (pStatus.dwRunningState != SPRS_DONE); +} + +SCP_vector> speech_enumerate_voices() +{ + SCP_vector> voices; + + ISpObjectTokenCategory* comTokenCategory = nullptr; + IEnumSpObjectTokens* comVoices = nullptr; + ULONG comVoicesCount = 0; + + HRESULT hr = ::CoCreateInstance(CLSID_SpObjectTokenCategory, nullptr, + CLSCTX_INPROC_SERVER, IID_ISpObjectTokenCategory, (LPVOID*)&comTokenCategory); + + if (FAILED(hr)) { + return voices; + } + + hr = comTokenCategory->SetId(SPCAT_VOICES, false); + if (FAILED(hr)) { + comTokenCategory->Release(); + return voices; + } + + hr = comTokenCategory->EnumTokens(nullptr, nullptr, &comVoices); + if (FAILED(hr)) { + comTokenCategory->Release(); + return voices; + } + + hr = comVoices->GetCount(&comVoicesCount); + if (FAILED(hr)) { + comVoices->Release(); + comTokenCategory->Release(); + return voices; + } + + int voiceID = 0; + while (comVoicesCount > 0) { + ISpObjectToken* comAVoice = nullptr; + + comVoices->Next(1, &comAVoice, nullptr); + + LPWSTR id = nullptr; + comAVoice->GetStringValue(nullptr, &id); + + if (id) { + auto idlength = wcslen(id); + int buffer_size = WideCharToMultiByte(CP_UTF8, 0, id, (int)idlength, nullptr, 0, nullptr, nullptr); + + if (buffer_size > 0) { + SCP_string voiceName; + voiceName.resize(buffer_size); + WideCharToMultiByte(CP_UTF8, 0, id, (int)idlength, &voiceName[0], buffer_size, nullptr, nullptr); + voices.emplace_back(std::make_pair(voiceID++, voiceName)); + } + CoTaskMemFree(id); + } + + comAVoice->Release(); + comVoicesCount--; + } + + comVoices->Release(); + comTokenCategory->Release(); + + return voices; +} + +#endif // FS2_SPEECH \ No newline at end of file diff --git a/code/source_groups.cmake b/code/source_groups.cmake index cd55353b901..dbba52510dc 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -1621,16 +1621,25 @@ add_file_folder("Sound" sound/rtvoice.h sound/sound.cpp sound/sound.h - sound/speech.cpp sound/speech.h sound/voicerec.cpp sound/voicerec.h ) -if (APPLE) +if (WIN32) add_file_folder("Sound" ${file_root_sound} - sound/speech.mm + sound/speech_win.cpp + ) +elseif (APPLE) + add_file_folder("Sound" + ${file_root_sound} + sound/speech_mac.mm + ) +elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_file_folder("Sound" + ${file_root_sound} + sound/speech_linux.cpp ) endif() From c3b8922fe3fe36e656d7cde098bf73928edb7107 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Thu, 14 May 2026 23:35:00 -0400 Subject: [PATCH 51/65] address feedback --- code/mod_table/mod_table.cpp | 8 ++++---- code/mod_table/mod_table.h | 2 +- code/ship/shiphit.cpp | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/code/mod_table/mod_table.cpp b/code/mod_table/mod_table.cpp index dde809ec526..b5fd84f2c6d 100644 --- a/code/mod_table/mod_table.cpp +++ b/code/mod_table/mod_table.cpp @@ -184,7 +184,7 @@ float Shield_percent_skips_damage; float Min_radius_for_persistent_debris; bool Zero_radius_explosions_skip_fireballs; bool Render_insignias_as_decals; -bool Link_subsystems_to_destroyed_submodels; +bool Link_special_point_subsystems_to_destroyed_submodels; #ifdef WITH_DISCORD @@ -1650,8 +1650,8 @@ void parse_mod_table(const char *filename) stuff_boolean(&Zero_radius_explosions_skip_fireballs); } - if (optional_string("$Link subsystems to -destroyed submodels:")) { - stuff_boolean(&Link_subsystems_to_destroyed_submodels); + if (optional_string("$Link special-point subsystems to -destroyed submodels:")) { + stuff_boolean(&Link_special_point_subsystems_to_destroyed_submodels); } // end of options ---------------------------------------- @@ -1902,7 +1902,7 @@ void mod_table_reset() Min_radius_for_persistent_debris = 50.0f; Zero_radius_explosions_skip_fireballs = false; Render_insignias_as_decals = false; - Link_subsystems_to_destroyed_submodels = false; + Link_special_point_subsystems_to_destroyed_submodels = false; } void mod_table_set_version_flags() diff --git a/code/mod_table/mod_table.h b/code/mod_table/mod_table.h index 433461d63d8..022d6e34fc3 100644 --- a/code/mod_table/mod_table.h +++ b/code/mod_table/mod_table.h @@ -206,7 +206,7 @@ extern float Shield_percent_skips_damage; extern float Min_radius_for_persistent_debris; extern bool Zero_radius_explosions_skip_fireballs; extern bool Render_insignias_as_decals; -extern bool Link_subsystems_to_destroyed_submodels; +extern bool Link_special_point_subsystems_to_destroyed_submodels; void mod_table_init(); void mod_table_post_process(); diff --git a/code/ship/shiphit.cpp b/code/ship/shiphit.cpp index 96fb5ddac76..2721bd0ab00 100755 --- a/code/ship/shiphit.cpp +++ b/code/ship/shiphit.cpp @@ -116,7 +116,7 @@ static bool is_subsys_destroyed(ship *shipp, int submodel) void check_subsystem_submodel_link(const ship *shipp, const ship_subsys *subsys, bool was_destroyed) { - if (!Link_subsystems_to_destroyed_submodels) + if (!Link_special_point_subsystems_to_destroyed_submodels) return; Assertion(shipp && subsys, "the ship and subsystem must exist!"); From 4741869bbe07e77ae52a4d1c535c9e95a2de0827 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Fri, 15 May 2026 06:32:59 -0500 Subject: [PATCH 52/65] QtFRED Jump Node Dialog Upgrade (#7420) * make jump node multi select and direct edit * clang * update qtfred help docs * fixes --- .../doc/dialogs/JumpNodeEditorDialog.html | 43 +- .../dialogs/JumpNodeEditorDialogModel.cpp | 522 ++++++++++-------- .../dialogs/JumpNodeEditorDialogModel.h | 43 +- qtfred/src/ui/FredView.cpp | 2 +- .../src/ui/dialogs/JumpNodeEditorDialog.cpp | 126 +++-- qtfred/src/ui/dialogs/JumpNodeEditorDialog.h | 14 +- qtfred/ui/JumpNodeEditorDialog.ui | 39 +- 7 files changed, 463 insertions(+), 326 deletions(-) diff --git a/qtfred/help-src/doc/dialogs/JumpNodeEditorDialog.html b/qtfred/help-src/doc/dialogs/JumpNodeEditorDialog.html index 6440d073845..67050646ec4 100644 --- a/qtfred/help-src/doc/dialogs/JumpNodeEditorDialog.html +++ b/qtfred/help-src/doc/dialogs/JumpNodeEditorDialog.html @@ -10,24 +10,43 @@

Jump Node Editor

Opens via Editors › Jump Node Editor.

Creates and configures subspace jump nodes. Jump nodes are fixed points in space that -ships can use to enter or exit subspace. They appear on the HUD radar and can be targeted. -Select a node from the drop-down to edit its properties.

+ships can use to enter or exit subspace. They appear on the HUD radar and can be +targeted.

+ +

The dialog tracks the viewport selection. Select jump node objects in the viewport +to edit their properties. When multiple nodes are selected, all edits except the +node name apply to every selected node at once. Color channels and the hidden state +display a mixed indicator when the selected nodes disagree, and editing one of them +clears that mixed state.

+ +

Navigation

+ + + +
ButtonDescription
Prev / NextCycles the viewport selection to the previous or next + jump node in the mission, allowing sequential editing without switching back + to the viewport.

Key fields

- - + Must be unique. When multiple nodes are selected, shows the name of the first + selected node and renames only that node. + - - - + Set to <none> to fall back to the internal name. Applied to + all selected nodes; when nodes disagree the field shows blank until edited. + + +
FieldDescription
Jump nodeSelects which placed node to edit.
NameInternal identifier used in SEXPs and mission scripting. - Must be unique.
LayerThe layer the jump node is assigned to.
LayerThe layer the jump node is assigned to. Applied to all + selected nodes.
Display nameName shown to the player on the HUD and in targeting. - If left blank, the internal name is used instead.
Model filePOF model rendered at the node's position. Uses the - default jump node model if left blank.
Node color (RGB)Color used to render the node model in the - mission.
Hidden by defaultWhen checked, the node starts the mission hidden -
Model filePOF model rendered at the node's position. Applied to + all selected nodes; when nodes disagree the field shows blank until edited.
Node color (RGBA)Color used to render the node model in the + mission. The color swatch next to the fields shows a live preview of the + current color. Applied to all selected nodes. Channels that differ across the + selection show blank until edited, and the swatch shows a "?" indicator.
Hidden by defaultWhen checked, the node starts the mission hidden. + Applied to all selected nodes. Displays as a partially-checked tri-state when + nodes disagree, until the box is clicked.
diff --git a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp index 926c2f4b74c..b80bdffde9b 100644 --- a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp @@ -16,70 +16,38 @@ JumpNodeEditorDialogModel::JumpNodeEditorDialogModel(QObject* parent, EditorView connect(viewport->editor, &Editor::currentObjectChanged, this, &JumpNodeEditorDialogModel::onSelectedObjectChanged); connect(viewport->editor, &Editor::objectMarkingChanged, this, &JumpNodeEditorDialogModel::onSelectedObjectMarkingChanged); connect(viewport->editor, &Editor::missionChanged, this, &JumpNodeEditorDialogModel::onMissionChanged); - + initializeData(); } -bool JumpNodeEditorDialogModel::apply() -{ - if (_currentlySelectedNodeIndex < 0) { - // Nothing to apply - return true; - } - - // Validate - if (!validateData()) { - return false; - } - - // Commit - auto* jnp = jumpnode_get_by_objnum(getSelectedJumpNodeObjnum(_currentlySelectedNodeIndex)); - Assertion(jnp != nullptr, "Jump node not found during apply!"); - - char old_name_buf[NAME_LENGTH]; - std::strncpy(old_name_buf, jnp->GetName(), NAME_LENGTH - 1); - old_name_buf[NAME_LENGTH - 1] = '\0'; - - lcl_fred_replace_stuff(_display); - - jnp->SetName(_name.c_str()); - jnp->SetDisplayName(lcase_equal(_display, "") ? _name.c_str() : _display.c_str()); - - // Only set a non default model - if (!lcase_equal(_modelFilename, JN_DEFAULT_MODEL)) { - jnp->SetModel(_modelFilename.c_str()); - } - - jnp->SetAlphaColor(_red, _green, _blue, _alpha); - jnp->SetVisibility(!_hidden); - - // Update sexp references when name changes - if (strcmp(old_name_buf, _name.c_str()) != 0) { - update_sexp_references(old_name_buf, _name.c_str()); - } - - _editor->missionChanged(); +bool JumpNodeEditorDialogModel::apply() { return true; } -void JumpNodeEditorDialogModel::reject() -{ - // do nothing -} +void JumpNodeEditorDialogModel::reject() {} void JumpNodeEditorDialogModel::initializeData() { - buildNodeList(); - - // Find the currently selected object if it's a jump node - int objnum = -1; - if (query_valid_object(_editor->currentObject) && Objects[_editor->currentObject].type == OBJ_JUMP_NODE) { - objnum = _editor->currentObject; + _selectedJumpNodes.clear(); + _redMixed = _greenMixed = _blueMixed = _alphaMixed = false; + _hiddenMixed = false; + + // Collect all marked OBJ_JUMP_NODE objects + for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if (ptr->type == OBJ_JUMP_NODE && ptr->flags[Object::Object_Flags::Marked]) { + _selectedJumpNodes.push_back(OBJ_INDEX(ptr)); + } } - - if (objnum >= 0) { - auto* jnp = jumpnode_get_by_objnum(objnum); - Assertion(jnp != nullptr, "Jump node not found for current object!"); + + // Fall back to currentObject if nothing is marked + if (_selectedJumpNodes.empty() && query_valid_object(_editor->currentObject) && + Objects[_editor->currentObject].type == OBJ_JUMP_NODE) { + _selectedJumpNodes.push_back(_editor->currentObject); + } + + if (!_selectedJumpNodes.empty()) { + auto* jnp = jumpnode_get_by_objnum(_selectedJumpNodes.front()); + Assertion(jnp != nullptr, "Jump node not found for selected object!"); _name = jnp->GetName(); _display = jnp->HasDisplayName() ? jnp->GetDisplayName() : ""; @@ -88,7 +56,7 @@ void JumpNodeEditorDialogModel::initializeData() if (auto* pm = model_get(model_num)) { _modelFilename = pm->filename; } else { - _modelFilename.clear(); + _modelFilename = JN_DEFAULT_MODEL; } const auto& c = jnp->GetColor(); @@ -99,12 +67,35 @@ void JumpNodeEditorDialogModel::initializeData() _hidden = jnp->IsHidden(); - // Find the index of the jump node in the local list - for (const auto& node : _nodes) { - if (!stricmp(node.first.c_str(), _name.c_str())) { - _currentlySelectedNodeIndex = node.second; - break; + if (hasMultipleSelection()) { + bool displayConsistent = true; + bool modelConsistent = true; + for (size_t i = 1; i < _selectedJumpNodes.size(); ++i) { + auto* other = jumpnode_get_by_objnum(_selectedJumpNodes[i]); + if (!other) continue; + + SCP_string otherDisplay = other->HasDisplayName() ? other->GetDisplayName() : ""; + if (_display != otherDisplay) + displayConsistent = false; + + SCP_string otherModel; + if (auto* pm = model_get(other->GetModelNumber())) + otherModel = pm->filename; + else + otherModel = JN_DEFAULT_MODEL; + if (_modelFilename != otherModel) + modelConsistent = false; + + const auto& oc = other->GetColor(); + if (oc.red != _red) _redMixed = true; + if (oc.green != _green) _greenMixed = true; + if (oc.blue != _blue) _blueMixed = true; + if (oc.alpha != _alpha) _alphaMixed = true; + + if (other->IsHidden() != _hidden) _hiddenMixed = true; } + if (!displayConsistent) _display.clear(); + if (!modelConsistent) _modelFilename.clear(); } } else { _name.clear(); @@ -112,42 +103,47 @@ void JumpNodeEditorDialogModel::initializeData() _modelFilename.clear(); _red = _green = _blue = _alpha = 0; _hidden = false; - - _currentlySelectedNodeIndex = -1; } Q_EMIT jumpNodeMarkingChanged(); _modified = false; } -void JumpNodeEditorDialogModel::buildNodeList() -{ - _nodes.clear(); - int idx = 0; - for (auto& node : Jump_nodes) { - _nodes.emplace_back(node.GetName(), idx++); - } +bool JumpNodeEditorDialogModel::hasValidSelection() const { + return !_selectedJumpNodes.empty(); } -bool JumpNodeEditorDialogModel::validateData() -{ - _bypass_errors = false; +bool JumpNodeEditorDialogModel::hasMultipleSelection() const { + return _selectedJumpNodes.size() > 1; +} - SCP_trim(_name); +bool JumpNodeEditorDialogModel::hasAnyNodesInMission() { + return !Jump_nodes.empty(); +} + +int JumpNodeEditorDialogModel::getSelectionCount() const { + return static_cast(_selectedJumpNodes.size()); +} - const SCP_string name = _name; +void JumpNodeEditorDialogModel::showErrorDialogNoCancel(const SCP_string& message) { + if (_bypass_errors) { + return; + } + _bypass_errors = true; + _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Error", message, {DialogButton::Ok}); +} + +bool JumpNodeEditorDialogModel::validateName(const SCP_string& name) { if (name.empty()) { showErrorDialogNoCancel("A jump node name cannot be empty."); return false; } - // Disallow leading '<' - if (!name.empty() && name[0] == '<') { + if (name[0] == '<') { showErrorDialogNoCancel("Jump node names are not allowed to begin with '<'."); return false; } - // Wing name collision for (auto& wing : Wings) { if (!stricmp(wing.name, name.c_str())) { showErrorDialogNoCancel("This jump node name is already being used by a wing."); @@ -155,7 +151,6 @@ bool JumpNodeEditorDialogModel::validateData() } } - // Ship/start name collision for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { if (ptr->type == OBJ_SHIP || ptr->type == OBJ_START) { if (!stricmp(name.c_str(), Ships[ptr->instance].ship_name)) { @@ -165,7 +160,6 @@ bool JumpNodeEditorDialogModel::validateData() } } - // AI target priority group collision for (auto& ai : Ai_tp_list) { if (!stricmp(name.c_str(), ai.name)) { showErrorDialogNoCancel("This jump node name is already being used by a target priority group."); @@ -173,230 +167,294 @@ bool JumpNodeEditorDialogModel::validateData() } } - // Waypoint path collision if (find_matching_waypoint_list(name.c_str()) != nullptr) { showErrorDialogNoCancel("This jump node name is already being used by a waypoint path."); return false; } - // Another jump node with the same name (but not this one) - auto* jnp = jumpnode_get_by_objnum(getSelectedJumpNodeObjnum(_currentlySelectedNodeIndex)); + auto* current_jnp = _selectedJumpNodes.empty() ? nullptr : jumpnode_get_by_objnum(_selectedJumpNodes.front()); auto* found = jumpnode_get_by_name(name.c_str()); - if (found != nullptr && found != jnp) { + if (found != nullptr && found != current_jnp) { showErrorDialogNoCancel("This jump node name is already being used by another jump node."); return false; } - if (!cf_exists_full(_modelFilename.c_str(), CF_TYPE_MODELS)) { - showErrorDialogNoCancel("This jump node model file does not exist."); - return false; - } - return true; } -void JumpNodeEditorDialogModel::showErrorDialogNoCancel(const SCP_string& message) -{ - if (_bypass_errors) { - return; +bool JumpNodeEditorDialogModel::setName(const SCP_string& v) { + if (hasMultipleSelection() || _selectedJumpNodes.empty()) { + return true; } - _bypass_errors = true; - _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Error", message, {DialogButton::Ok}); -} + _bypass_errors = false; -int JumpNodeEditorDialogModel::getSelectedJumpNodeObjnum(int idx) const -{ - // Find the jump node and then mark it - for (const auto& node : Jump_nodes) { - if (!stricmp(node.GetName(), _nodes[idx].first.c_str())) { - return node.GetSCPObjectNumber(); - } + SCP_string trimmed = v; + SCP_trim(trimmed); + + if (!validateName(trimmed)) { + return false; } - return -1; -} + auto* jnp = jumpnode_get_by_objnum(_selectedJumpNodes.front()); + if (!jnp) { + return false; + } -void JumpNodeEditorDialogModel::onSelectedObjectChanged(int) -{ - initializeData(); -} + char old_name[NAME_LENGTH]; + std::strncpy(old_name, jnp->GetName(), NAME_LENGTH - 1); + old_name[NAME_LENGTH - 1] = '\0'; -void JumpNodeEditorDialogModel::onSelectedObjectMarkingChanged(int, bool) -{ - initializeData(); -} + jnp->SetName(trimmed.c_str()); -void JumpNodeEditorDialogModel::onMissionChanged() -{ - initializeData(); -} + if (strcmp(old_name, trimmed.c_str()) != 0) { + update_sexp_references(old_name, trimmed.c_str()); + } -const SCP_vector>& JumpNodeEditorDialogModel::getJumpNodeList() const -{ - return _nodes; + _name = trimmed; + set_modified(); + _editor->missionChanged(); + return true; } -void JumpNodeEditorDialogModel::selectJumpNodeByListIndex(int idx) -{ - if (_currentlySelectedNodeIndex == idx) { - // No change - return; +const SCP_string& JumpNodeEditorDialogModel::getName() const { return _name; } + +bool JumpNodeEditorDialogModel::setDisplayName(const SCP_string& v) { + if (_selectedJumpNodes.empty() || v.empty()) { + return true; } - if (!SCP_vector_inbounds(_nodes, idx)) - return; + SCP_string display = v; + lcl_fred_replace_stuff(display); + const bool useNodeName = lcase_equal(display, ""); - if (apply()) { - _editor->unmark_all(); + for (auto objnum : _selectedJumpNodes) { + auto* jnp = jumpnode_get_by_objnum(objnum); + if (!jnp) continue; + jnp->SetDisplayName(useNodeName ? jnp->GetName() : display.c_str()); + } - int objnum = getSelectedJumpNodeObjnum(idx); + _display = useNodeName ? "" : display; + set_modified(); + _editor->missionChanged(); + return true; +} - if (objnum < 0) { - _currentlySelectedNodeIndex = -1; - return; - } +const SCP_string& JumpNodeEditorDialogModel::getDisplayName() const { return _display; } - _editor->markObject(objnum); - _currentlySelectedNodeIndex = idx; +bool JumpNodeEditorDialogModel::setModelFilename(const SCP_string& v) { + if (_selectedJumpNodes.empty() || v.empty()) { + return true; } -} -int JumpNodeEditorDialogModel::getCurrentJumpNodeIndex() const -{ - return _currentlySelectedNodeIndex; -} -bool JumpNodeEditorDialogModel::hasValidSelection() const -{ - return _currentlySelectedNodeIndex >= 0; -} + _bypass_errors = false; -void JumpNodeEditorDialogModel::setName(const SCP_string& v) -{ - SCP_trim(_name); - - SCP_string current = _name; + if (!lcase_equal(v, JN_DEFAULT_MODEL) && !cf_exists_full(v.c_str(), CF_TYPE_MODELS)) { + showErrorDialogNoCancel("This jump node model file does not exist."); + return false; + } - _name = v; - if (apply()) { - set_modified(); - } else { - _name = current; // restore the old name + _modelFilename = v; + const bool useDefault = lcase_equal(_modelFilename, JN_DEFAULT_MODEL); + + for (auto objnum : _selectedJumpNodes) { + auto* jnp = jumpnode_get_by_objnum(objnum); + if (!jnp) continue; + if (!useDefault) { + jnp->SetModel(_modelFilename.c_str()); + } } -} -const SCP_string& JumpNodeEditorDialogModel::getName() const -{ - return _name; + set_modified(); + _editor->missionChanged(); + return true; } -void JumpNodeEditorDialogModel::setDisplayName(const SCP_string& v) +const SCP_string& JumpNodeEditorDialogModel::getModelFilename() const { return _modelFilename; } + +// Writes one color channel to every selected node. Channels still flagged as mixed +// keep their per-node values; the channel being set is cleared of its mixed flag. +static void applyChannelToAll(const SCP_vector& selected, + int red, int green, int blue, int alpha, + bool redMixed, bool greenMixed, bool blueMixed, bool alphaMixed) { - modify(_display, v); - apply(); // Apply changes immediately to update the display name + for (auto objnum : selected) { + auto* jnp = jumpnode_get_by_objnum(objnum); + if (!jnp) continue; + const auto& c = jnp->GetColor(); + int r = redMixed ? c.red : red; + int g = greenMixed ? c.green : green; + int b = blueMixed ? c.blue : blue; + int a = alphaMixed ? c.alpha : alpha; + jnp->SetAlphaColor(r, g, b, a); + } } -const SCP_string& JumpNodeEditorDialogModel::getDisplayName() const -{ - return _display; +void JumpNodeEditorDialogModel::setColorR(int v) { + if (v < 0) return; // mixed-state sentinel from the dialog — no-op + CLAMP(v, 0, 255); + _red = v; + _redMixed = false; + applyChannelToAll(_selectedJumpNodes, _red, _green, _blue, _alpha, + _redMixed, _greenMixed, _blueMixed, _alphaMixed); + set_modified(); + _editor->missionChanged(); } -void JumpNodeEditorDialogModel::setModelFilename(const SCP_string& v) -{ - SCP_string current = _modelFilename; +int JumpNodeEditorDialogModel::getColorR() const { return _red; } - _modelFilename = v; - if (apply()) { - set_modified(); - } else { - _modelFilename = current; // restore the old name - } +void JumpNodeEditorDialogModel::setColorG(int v) { + if (v < 0) return; + CLAMP(v, 0, 255); + _green = v; + _greenMixed = false; + applyChannelToAll(_selectedJumpNodes, _red, _green, _blue, _alpha, + _redMixed, _greenMixed, _blueMixed, _alphaMixed); + set_modified(); + _editor->missionChanged(); } -const SCP_string& JumpNodeEditorDialogModel::getModelFilename() const -{ - return _modelFilename; -} +int JumpNodeEditorDialogModel::getColorG() const { return _green; } -void JumpNodeEditorDialogModel::setColorR(int v) -{ +void JumpNodeEditorDialogModel::setColorB(int v) { + if (v < 0) return; CLAMP(v, 0, 255); - modify(_red, v); - apply(); // Apply changes immediately to update the model color + _blue = v; + _blueMixed = false; + applyChannelToAll(_selectedJumpNodes, _red, _green, _blue, _alpha, + _redMixed, _greenMixed, _blueMixed, _alphaMixed); + set_modified(); + _editor->missionChanged(); } -int JumpNodeEditorDialogModel::getColorR() const -{ - return _red; -} +int JumpNodeEditorDialogModel::getColorB() const { return _blue; } -void JumpNodeEditorDialogModel::setColorG(int v) -{ +void JumpNodeEditorDialogModel::setColorA(int v) { + if (v < 0) return; CLAMP(v, 0, 255); - modify(_green, v); - apply(); // Apply changes immediately to update the model color + _alpha = v; + _alphaMixed = false; + applyChannelToAll(_selectedJumpNodes, _red, _green, _blue, _alpha, + _redMixed, _greenMixed, _blueMixed, _alphaMixed); + set_modified(); + _editor->missionChanged(); } -int JumpNodeEditorDialogModel::getColorG() const -{ - return _green; -} +int JumpNodeEditorDialogModel::getColorA() const { return _alpha; } -void JumpNodeEditorDialogModel::setColorB(int v) -{ - CLAMP(v, 0, 255); - modify(_blue, v); - apply(); // Apply changes immediately to update the model color +bool JumpNodeEditorDialogModel::isColorRMixed() const { return _redMixed; } +bool JumpNodeEditorDialogModel::isColorGMixed() const { return _greenMixed; } +bool JumpNodeEditorDialogModel::isColorBMixed() const { return _blueMixed; } +bool JumpNodeEditorDialogModel::isColorAMixed() const { return _alphaMixed; } +bool JumpNodeEditorDialogModel::hasAnyColorMixed() const { + return _redMixed || _greenMixed || _blueMixed || _alphaMixed; } -int JumpNodeEditorDialogModel::getColorB() const -{ - return _blue; +void JumpNodeEditorDialogModel::setHidden(bool v) { + _hidden = v; + _hiddenMixed = false; + for (auto objnum : _selectedJumpNodes) { + auto* jnp = jumpnode_get_by_objnum(objnum); + if (jnp) { + jnp->SetVisibility(!v); + } + } + set_modified(); + _editor->missionChanged(); } -void JumpNodeEditorDialogModel::setColorA(int v) -{ - CLAMP(v, 0, 255); - modify(_alpha, v); - apply(); // Apply changes immediately to update the model color +bool JumpNodeEditorDialogModel::getHidden() const { return _hidden; } + +int JumpNodeEditorDialogModel::getHiddenState() const { + if (_hiddenMixed) return Qt::PartiallyChecked; + return _hidden ? Qt::Checked : Qt::Unchecked; } -int JumpNodeEditorDialogModel::getColorA() const -{ - return _alpha; +SCP_string JumpNodeEditorDialogModel::getLayer() const { + SCP_string result; + bool first = true; + for (auto objnum : _selectedJumpNodes) { + SCP_string layer = _viewport->getObjectLayerName(objnum); + if (first) { + result = layer; + first = false; + } else if (result != layer) { + return ""; + } + } + return result; } -void JumpNodeEditorDialogModel::setHidden(bool v) -{ - modify(_hidden, v); - apply(); // Apply changes immediately to update the visibility +void JumpNodeEditorDialogModel::setLayer(const SCP_string& v) { + for (auto objnum : _selectedJumpNodes) { + _viewport->moveObjectToLayer(objnum, v); + } + set_modified(); + _editor->missionChanged(); } -bool JumpNodeEditorDialogModel::getHidden() const -{ - return _hidden; +void JumpNodeEditorDialogModel::selectNodeFromObjectList(object* start, bool forward) { + auto* ptr = start; + while (ptr != END_OF_LIST(&obj_used_list)) { + if (ptr->type == OBJ_JUMP_NODE) { + _editor->unmark_all(); + _editor->markObject(OBJ_INDEX(ptr)); + return; + } + ptr = forward ? GET_NEXT(ptr) : GET_PREV(ptr); + } + // Wrap around + ptr = forward ? GET_FIRST(&obj_used_list) : GET_LAST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (ptr->type == OBJ_JUMP_NODE) { + _editor->unmark_all(); + _editor->markObject(OBJ_INDEX(ptr)); + return; + } + ptr = forward ? GET_NEXT(ptr) : GET_PREV(ptr); + } } -SCP_string JumpNodeEditorDialogModel::getLayer() const -{ - if (_currentlySelectedNodeIndex < 0) - return ""; - int objnum = getSelectedJumpNodeObjnum(_currentlySelectedNodeIndex); - if (objnum < 0) - return ""; - return _viewport->getObjectLayerName(objnum); +void JumpNodeEditorDialogModel::selectFirstNodeInMission() { + for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if (ptr->type == OBJ_JUMP_NODE) { + _editor->unmark_all(); + _editor->markObject(OBJ_INDEX(ptr)); + return; + } + } } -void JumpNodeEditorDialogModel::setLayer(const SCP_string& v) -{ - if (_currentlySelectedNodeIndex < 0) +void JumpNodeEditorDialogModel::selectNextNode() { + if (!hasValidSelection()) { + if (hasAnyNodesInMission()) { + selectFirstNodeInMission(); + } return; - int objnum = getSelectedJumpNodeObjnum(_currentlySelectedNodeIndex); - if (objnum < 0) + } + selectNodeFromObjectList(GET_NEXT(&Objects[_selectedJumpNodes.front()]), true); +} + +void JumpNodeEditorDialogModel::selectPreviousNode() { + if (!hasValidSelection()) { + if (hasAnyNodesInMission()) { + selectFirstNodeInMission(); + } return; - _viewport->moveObjectToLayer(objnum, v); - set_modified(); - _editor->missionChanged(); + } + selectNodeFromObjectList(GET_PREV(&Objects[_selectedJumpNodes.front()]), false); +} + +void JumpNodeEditorDialogModel::onSelectedObjectChanged(int) { + initializeData(); +} + +void JumpNodeEditorDialogModel::onSelectedObjectMarkingChanged(int, bool) { + initializeData(); +} + +void JumpNodeEditorDialogModel::onMissionChanged() { + initializeData(); } } // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h index bfb171886a2..27f374fe0d4 100644 --- a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h @@ -5,25 +5,25 @@ namespace fso::fred::dialogs { class JumpNodeEditorDialogModel : public AbstractDialogModel { Q_OBJECT - public: +public: explicit JumpNodeEditorDialogModel(QObject* parent, EditorViewport* viewport); bool apply() override; void reject() override; - const SCP_vector>& getJumpNodeList() const; - void selectJumpNodeByListIndex(int idx); - int getCurrentJumpNodeIndex() const; bool hasValidSelection() const; + bool hasMultipleSelection() const; + static bool hasAnyNodesInMission(); + int getSelectionCount() const; - void setName(const SCP_string& v); + bool setName(const SCP_string& v); const SCP_string& getName() const; - void setDisplayName(const SCP_string& v); // "" means use Name + bool setDisplayName(const SCP_string& v); // "" means use Name const SCP_string& getDisplayName() const; - void setModelFilename(const SCP_string& v); + bool setModelFilename(const SCP_string& v); const SCP_string& getModelFilename() const; - void setColorR(int v); + void setColorR(int v); // pass -1 for no-op (mixed sentinel) int getColorR() const; void setColorG(int v); int getColorG() const; @@ -31,37 +31,46 @@ class JumpNodeEditorDialogModel : public AbstractDialogModel { int getColorB() const; void setColorA(int v); int getColorA() const; + bool isColorRMixed() const; + bool isColorGMixed() const; + bool isColorBMixed() const; + bool isColorAMixed() const; + bool hasAnyColorMixed() const; void setHidden(bool v); bool getHidden() const; + int getHiddenState() const; // returns Qt::CheckState as int (PartiallyChecked when mixed) SCP_string getLayer() const; void setLayer(const SCP_string& v); - signals: + void selectNextNode(); + void selectPreviousNode(); + +signals: void jumpNodeMarkingChanged(); - private slots: +private slots: void onSelectedObjectChanged(int); void onSelectedObjectMarkingChanged(int, bool); void onMissionChanged(); - private: // NOLINT(readability-redundant-access-specifiers) +private: // NOLINT(readability-redundant-access-specifiers) void initializeData(); - void buildNodeList(); - bool validateData(); void showErrorDialogNoCancel(const SCP_string& message); - int getSelectedJumpNodeObjnum(int idx) const; + bool validateName(const SCP_string& name); + void selectNodeFromObjectList(object* start, bool forward); + void selectFirstNodeInMission(); - int _currentlySelectedNodeIndex = -1; + SCP_vector _selectedJumpNodes; // objnums of selected jump nodes SCP_string _name; SCP_string _display; SCP_string _modelFilename; int _red = 0, _green = 0, _blue = 0, _alpha = 0; + bool _redMixed = false, _greenMixed = false, _blueMixed = false, _alphaMixed = false; bool _hidden = false; - - SCP_vector> _nodes; + bool _hiddenMixed = false; bool _bypass_errors = false; }; diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 03c02c2a4dc..de12a4aff5e 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -1063,7 +1063,7 @@ void FredView::onUpdateContextToolbar() { } } else if (effectiveType == OBJ_WAYPOINT) { addBtn(tr("Edit Waypoint Path"), &FredView::on_actionWaypoint_Paths_triggered); - } else if (numMarked <= 1 && effectiveType == OBJ_JUMP_NODE) { + } else if (effectiveType == OBJ_JUMP_NODE) { addBtn(tr("Edit Jump Node"), &FredView::on_actionJump_Nodes_triggered); } else if (effectiveType == OBJ_PROP) { addBtn(tr("Edit Prop"), &FredView::on_actionProps_triggered); diff --git a/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp index c32ad94967f..0b28b6479cb 100644 --- a/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp @@ -18,6 +18,12 @@ JumpNodeEditorDialog::JumpNodeEditorDialog(FredView* parent, EditorViewport* vie ui->displayNameLineEdit->setMaxLength(NAME_LENGTH - 1); ui->modelFileLineEdit->setMaxLength(MAX_FILENAME_LEN - 1); + // -1 is the "mixed selection" sentinel... shown as blank via specialValueText. + for (auto* sb : {ui->redSpinBox, ui->greenSpinBox, ui->blueSpinBox, ui->alphaSpinBox}) { + sb->setMinimum(-1); + sb->setSpecialValueText(" "); + } + initializeUi(); updateUi(); @@ -36,21 +42,32 @@ void JumpNodeEditorDialog::initializeUi() { util::SignalBlockers blockers(this); - updateJumpNodeListComboBox(); - enableOrDisableControls(); -} - -void JumpNodeEditorDialog::updateJumpNodeListComboBox() -{ - ui->selectJumpNodeComboBox->clear(); - - for (auto& wp : _model->getJumpNodeList()) { - ui->selectJumpNodeComboBox->addItem(QString::fromStdString(wp.first), wp.second); + ui->layerCombo->clear(); + for (const auto& name : _viewport->getLayerNames()) { + ui->layerCombo->addItem(QString::fromStdString(name), QString::fromStdString(name)); } - ui->selectJumpNodeComboBox->setEnabled(!_model->getJumpNodeList().empty()); - - ui->selectJumpNodeComboBox->setCurrentIndex(_model->getCurrentJumpNodeIndex()); + const bool enabled = _model->hasValidSelection(); + const bool hasAny = _model->hasAnyNodesInMission(); + const bool multiSelect = _model->hasMultipleSelection(); + + ui->nameLineEdit->setEnabled(enabled && !multiSelect); + ui->displayNameLineEdit->setEnabled(enabled); + ui->modelFileLineEdit->setEnabled(enabled); + ui->redSpinBox->setEnabled(enabled); + ui->greenSpinBox->setEnabled(enabled); + ui->blueSpinBox->setEnabled(enabled); + ui->alphaSpinBox->setEnabled(enabled); + ui->hiddenByDefaultCheckBox->setEnabled(enabled); + ui->layerCombo->setEnabled(enabled); + ui->prevNodeButton->setEnabled(hasAny); + ui->nextNodeButton->setEnabled(hasAny); + + if (multiSelect) { + setWindowTitle(QString("Edit %1 Jump Nodes").arg(_model->getSelectionCount())); + } else { + setWindowTitle("Jump Node Editor"); + } } void JumpNodeEditorDialog::updateUi() @@ -61,81 +78,102 @@ void JumpNodeEditorDialog::updateUi() ui->displayNameLineEdit->setText(QString::fromStdString(_model->getDisplayName())); ui->modelFileLineEdit->setText(QString::fromStdString(_model->getModelFilename())); - ui->redSpinBox->setValue(_model->getColorR()); - ui->greenSpinBox->setValue(_model->getColorG()); - ui->blueSpinBox->setValue(_model->getColorB()); - ui->alphaSpinBox->setValue(_model->getColorA()); + ui->redSpinBox->setValue(_model->isColorRMixed() ? -1 : _model->getColorR()); + ui->greenSpinBox->setValue(_model->isColorGMixed() ? -1 : _model->getColorG()); + ui->blueSpinBox->setValue(_model->isColorBMixed() ? -1 : _model->getColorB()); + ui->alphaSpinBox->setValue(_model->isColorAMixed() ? -1 : _model->getColorA()); - ui->hiddenByDefaultCheckBox->setChecked(_model->getHidden()); + const int hiddenState = _model->getHiddenState(); + ui->hiddenByDefaultCheckBox->setTristate(hiddenState == Qt::PartiallyChecked); + ui->hiddenByDefaultCheckBox->setCheckState(static_cast(hiddenState)); - ui->layerCombo->clear(); - for (const auto& name : _viewport->getLayerNames()) { - ui->layerCombo->addItem(QString::fromStdString(name), QString::fromStdString(name)); - } ui->layerCombo->setCurrentIndex(ui->layerCombo->findData(QString::fromStdString(_model->getLayer()))); + + updateColorSwatch(); +} + +void JumpNodeEditorDialog::updateColorSwatch() +{ + if (_model->hasAnyColorMixed()) { + // Mixed selection: render a neutral patterned swatch with a "?". + ui->colorSwatch->setText("?"); + ui->colorSwatch->setAlignment(Qt::AlignCenter); + ui->colorSwatch->setStyleSheet("background: #888; color: white;" + "border: 1px solid #444; border-radius: 3px;"); + return; + } + ui->colorSwatch->setText(""); + ui->colorSwatch->setStyleSheet(QString("background: rgba(%1,%2,%3,%4);" + "border: 1px solid #444; border-radius: 3px;") + .arg(_model->getColorR()) + .arg(_model->getColorG()) + .arg(_model->getColorB()) + .arg(_model->getColorA())); } -void JumpNodeEditorDialog::enableOrDisableControls() +void JumpNodeEditorDialog::on_prevNodeButton_clicked() { - const bool enable = _model->hasValidSelection(); - - ui->nameLineEdit->setEnabled(enable); - ui->displayNameLineEdit->setEnabled(enable); - ui->modelFileLineEdit->setEnabled(enable); - ui->redSpinBox->setEnabled(enable); - ui->greenSpinBox->setEnabled(enable); - ui->blueSpinBox->setEnabled(enable); - ui->alphaSpinBox->setEnabled(enable); - ui->hiddenByDefaultCheckBox->setEnabled(enable); - ui->layerCombo->setEnabled(enable); + _model->selectPreviousNode(); } -void JumpNodeEditorDialog::on_selectJumpNodeComboBox_currentIndexChanged(int index) +void JumpNodeEditorDialog::on_nextNodeButton_clicked() { - auto itemId = ui->selectJumpNodeComboBox->itemData(index).value(); - _model->selectJumpNodeByListIndex(itemId); + _model->selectNextNode(); } void JumpNodeEditorDialog::on_nameLineEdit_editingFinished() { - _model->setName(ui->nameLineEdit->text().toUtf8().constData()); - updateUi(); // Update immediately in case the name change is rejected + if (!_model->setName(ui->nameLineEdit->text().toUtf8().constData())) { + util::SignalBlockers blockers(this); + ui->nameLineEdit->setText(QString::fromStdString(_model->getName())); + } } void JumpNodeEditorDialog::on_displayNameLineEdit_editingFinished() { - _model->setDisplayName(ui->displayNameLineEdit->text().toUtf8().constData()); + if (!_model->setDisplayName(ui->displayNameLineEdit->text().toUtf8().constData())) { + util::SignalBlockers blockers(this); + ui->displayNameLineEdit->setText(QString::fromStdString(_model->getDisplayName())); + } } void JumpNodeEditorDialog::on_modelFileLineEdit_editingFinished() { - _model->setModelFilename(ui->modelFileLineEdit->text().toUtf8().constData()); - updateUi(); // Update immediately in case the name change is rejected + if (!_model->setModelFilename(ui->modelFileLineEdit->text().toUtf8().constData())) { + util::SignalBlockers blockers(this); + ui->modelFileLineEdit->setText(QString::fromStdString(_model->getModelFilename())); + } } void JumpNodeEditorDialog::on_redSpinBox_valueChanged(int value) { _model->setColorR(value); + updateColorSwatch(); } void JumpNodeEditorDialog::on_greenSpinBox_valueChanged(int value) { _model->setColorG(value); + updateColorSwatch(); } void JumpNodeEditorDialog::on_blueSpinBox_valueChanged(int value) { _model->setColorB(value); + updateColorSwatch(); } void JumpNodeEditorDialog::on_alphaSpinBox_valueChanged(int value) { _model->setColorA(value); + updateColorSwatch(); } -void JumpNodeEditorDialog::on_hiddenByDefaultCheckBox_toggled(bool checked) +void JumpNodeEditorDialog::on_hiddenByDefaultCheckBox_clicked() { - _model->setHidden(checked); + // clicked() is used (not toggled()) so a click on a tri-state PartiallyChecked + // box still routes here. Read the post-click state from the widget itself. + _model->setHidden(ui->hiddenByDefaultCheckBox->isChecked()); } void JumpNodeEditorDialog::on_layerCombo_currentIndexChanged(int index) diff --git a/qtfred/src/ui/dialogs/JumpNodeEditorDialog.h b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.h index df9d316654e..342f1bb0747 100644 --- a/qtfred/src/ui/dialogs/JumpNodeEditorDialog.h +++ b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.h @@ -12,12 +12,13 @@ class JumpNodeEditorDialog; class JumpNodeEditorDialog : public QDialog { Q_OBJECT - public: +public: JumpNodeEditorDialog(FredView* parent, EditorViewport* viewport); ~JumpNodeEditorDialog() override; - private slots: - void on_selectJumpNodeComboBox_currentIndexChanged(int index); +private slots: + void on_prevNodeButton_clicked(); + void on_nextNodeButton_clicked(); void on_nameLineEdit_editingFinished(); void on_displayNameLineEdit_editingFinished(); void on_modelFileLineEdit_editingFinished(); @@ -25,18 +26,17 @@ class JumpNodeEditorDialog : public QDialog { void on_greenSpinBox_valueChanged(int value); void on_blueSpinBox_valueChanged(int value); void on_alphaSpinBox_valueChanged(int value); - void on_hiddenByDefaultCheckBox_toggled(bool checked); + void on_hiddenByDefaultCheckBox_clicked(); void on_layerCombo_currentIndexChanged(int index); - private: // NOLINT(readability-redundant-access-specifiers) +private: // NOLINT(readability-redundant-access-specifiers) EditorViewport* _viewport; std::unique_ptr ui; std::unique_ptr _model; void initializeUi(); - void updateJumpNodeListComboBox(); void updateUi(); - void enableOrDisableControls(); + void updateColorSwatch(); }; } // namespace fso::fred::dialogs diff --git a/qtfred/ui/JumpNodeEditorDialog.ui b/qtfred/ui/JumpNodeEditorDialog.ui index 396c8947711..abd1a9672e6 100644 --- a/qtfred/ui/JumpNodeEditorDialog.ui +++ b/qtfred/ui/JumpNodeEditorDialog.ui @@ -26,26 +26,23 @@ - - - + + + - Select Jump Node + &Prev - - + + + + &Next + + - - - - Qt::Horizontal - - - @@ -191,6 +188,22 @@ + + + + + 28 + 28 + + + + QFrame::Box + + + + + + From b8a3c9cae0df0c3c6c97188d762c775452ad78b2 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Fri, 15 May 2026 06:33:46 -0500 Subject: [PATCH 53/65] QtFred Ship initial status stylization pass (#7439) * ship initial status stylization pass * suppress false positive --- .../ShipInitialStatusDialogModel.cpp | 494 +++++++++--------- .../ShipEditor/ShipInitialStatusDialogModel.h | 171 +++--- .../ShipEditor/ShipInitialStatusDialog.cpp | 80 +-- .../ShipEditor/ShipInitialStatusDialog.h | 17 +- 4 files changed, 375 insertions(+), 387 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp index a648527f32a..10757eec4dc 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp @@ -10,55 +10,53 @@ #include namespace fso::fred::dialogs { -void reset_arrival_to_false(int shipnum, bool reset_wing, EditorViewport* _viewport) + +void resetArrivalToFalse(int shipnum, bool resetWing, EditorViewport* _viewport) { char buf[256]; ship* shipp = &Ships[shipnum]; // falsify the ship cue - if (set_cue_to_false(&shipp->arrival_cue)) { + if (setCueToFalse(&shipp->arrival_cue)) { sprintf(buf, "Setting arrival cue of ship %s\nto false for initial docking purposes.", shipp->ship_name); - // MessageBox(NULL, buf, "", MB_OK | MB_ICONEXCLAMATION); _viewport->dialogProvider->showButtonDialog(DialogType::Information, "Notice", buf, {DialogButton::Ok}); } // falsify the wing cue and all ships in that wing - if (reset_wing && shipp->wingnum >= 0) { + if (resetWing && shipp->wingnum >= 0) { int i; wing* wingp = &Wings[shipp->wingnum]; - if (set_cue_to_false(&wingp->arrival_cue)) { - + if (setCueToFalse(&wingp->arrival_cue)) { sprintf(buf, "Setting arrival cue of wing %s\nto false for initial docking purposes.", wingp->name); - // MessageBox(NULL, buf, "", MB_OK | MB_ICONEXCLAMATION); _viewport->dialogProvider->showButtonDialog(DialogType::Information, "Notice", buf, {DialogButton::Ok}); } for (i = 0; i < wingp->wave_count; i++) - reset_arrival_to_false(wingp->ship_index[i], false, _viewport); + resetArrivalToFalse(wingp->ship_index[i], false, _viewport); } } -bool set_cue_to_false(int* cue) + +bool setCueToFalse(int* cue) { // if the cue is not false, make it false. Be sure to set all ship editor dialog functions // to update data before and after we modify the cue. // Comment Above Prbably not nesscary due to new model if (*cue != Locked_sexp_false) { - free_sexp2(*cue); *cue = Locked_sexp_false; - return true; } else return false; } // self-explanatory, really -void initial_status_unmark_dock_handled_flag(object* objp) +void initialStatusUnmarkDockHandledFlag(object* objp) { objp->flags.remove(Object::Object_Flags::Docked_already_handled); } -void initial_status_mark_dock_leader_helper(object* objp, dock_function_info* infop, EditorViewport* viewport) + +void initialStatusMarkDockLeaderHelper(object* objp, dock_function_info* infop, EditorViewport* viewport) { ship* shipp = &Ships[objp->instance]; int cue_to_check; @@ -87,13 +85,13 @@ void initial_status_mark_dock_leader_helper(object* objp, dock_function_info* in // keep existing leader if he has a higher priority than us if (ship_class_compare(shipp->ship_info_index, leader_shipp->ship_info_index) >= 0) { // set my arrival cue to false - reset_arrival_to_false(SHIP_INDEX(shipp), true, viewport); + resetArrivalToFalse(SHIP_INDEX(shipp), true, viewport); return; } // otherwise, unmark the existing leader and set his arrival cue to false leader_shipp->flags.remove(Ship::Ship_Flags::Dock_leader); - reset_arrival_to_false(SHIP_INDEX(leader_shipp), true, viewport); + resetArrivalToFalse(SHIP_INDEX(leader_shipp), true, viewport); } // mark and save me as the leader @@ -101,6 +99,7 @@ void initial_status_mark_dock_leader_helper(object* objp, dock_function_info* in infop->maintained_variables.objp_value = objp; } } + ShipInitialStatusDialogModel::ShipInitialStatusDialogModel(QObject* parent, EditorViewport* viewport, bool multi) : AbstractDialogModel(parent, viewport) { @@ -109,164 +108,164 @@ ShipInitialStatusDialogModel::ShipInitialStatusDialogModel(QObject* parent, Edit void ShipInitialStatusDialogModel::initializeData(bool multi) { - m_multi_edit = multi; + _multiEdit = multi; int vflag, sflag, hflag; object* objp = nullptr; - m_ship = _editor->cur_ship; - if (m_ship == -1) { + _ship = _editor->cur_ship; + if (_ship == -1) { Assert( (Objects[_editor->currentObject].type == OBJ_SHIP) || (Objects[_editor->currentObject].type == OBJ_START)); - m_ship = get_ship_from_obj(_editor->currentObject); - Assert(m_ship >= 0); + _ship = get_ship_from_obj(_editor->currentObject); + Assert(_ship >= 0); } - m_move_ships_when_undocking = _viewport->Move_ships_when_undocking; + _moveShipsWhenUndocking = _viewport->Move_ships_when_undocking; // initialize dockpoint stuff - if (!m_multi_edit) { - num_dock_points = model_get_num_dock_points(Ship_info[Ships[m_ship].ship_info_index].model_num); - dockpoint_array = new dockpoint_information[num_dock_points]; - objp = &Objects[Ships[m_ship].objnum]; - for (int i = 0; i < num_dock_points; i++) { + if (!_multiEdit) { + _numDockPoints = model_get_num_dock_points(Ship_info[Ships[_ship].ship_info_index].model_num); + _dockpointArray = new dockpoint_information[_numDockPoints]; + objp = &Objects[Ships[_ship].objnum]; + for (int i = 0; i < _numDockPoints; i++) { object* docked_objp = dock_find_object_at_dockpoint(objp, i); if (docked_objp != nullptr) { - dockpoint_array[i].dockee_shipnum = docked_objp->instance; - dockpoint_array[i].dockee_point = dock_find_dockpoint_used_by_object(docked_objp, objp); + _dockpointArray[i].dockee_shipnum = docked_objp->instance; + _dockpointArray[i].dockee_point = dock_find_dockpoint_used_by_object(docked_objp, objp); // NOLINT(readability-suspicious-call-argument) } else { - dockpoint_array[i].dockee_shipnum = -1; - dockpoint_array[i].dockee_point = -1; + _dockpointArray[i].dockee_shipnum = -1; + _dockpointArray[i].dockee_point = -1; } } } vflag = sflag = hflag = 0; - m_velocity = static_cast(Objects[_editor->currentObject].phys_info.speed); - m_shields = static_cast(Objects[_editor->currentObject].shield_quadrant[0]); - m_hull = static_cast(Objects[_editor->currentObject].hull_strength); - guardian_threshold = Ships[m_ship].ship_guardian_threshold; + _velocity = static_cast(Objects[_editor->currentObject].phys_info.speed); + _shields = static_cast(Objects[_editor->currentObject].shield_quadrant[0]); + _hull = static_cast(Objects[_editor->currentObject].hull_strength); + _guardianThreshold = Ships[_ship].ship_guardian_threshold; if (Objects[_editor->currentObject].flags[Object::Object_Flags::No_shields]) - m_has_shields = Qt::Unchecked; + _hasShields = Qt::Unchecked; else - m_has_shields = Qt::Checked; + _hasShields = Qt::Checked; - if (Ships[m_ship].flags[Ship::Ship_Flags::Force_shields_on]) - m_force_shields = Qt::Checked; + if (Ships[_ship].flags[Ship::Ship_Flags::Force_shields_on]) + _forceShields = Qt::Checked; else - m_force_shields = Qt::Unchecked; + _forceShields = Qt::Unchecked; - if (Ships[m_ship].flags[Ship::Ship_Flags::Ship_locked]) - m_ship_locked = Qt::Checked; + if (Ships[_ship].flags[Ship::Ship_Flags::Ship_locked]) + _shipLocked = Qt::Checked; else - m_ship_locked = Qt::Unchecked; + _shipLocked = Qt::Unchecked; - if (Ships[m_ship].flags[Ship::Ship_Flags::Weapons_locked]) - m_weapons_locked = Qt::Checked; + if (Ships[_ship].flags[Ship::Ship_Flags::Weapons_locked]) + _weaponsLocked = Qt::Checked; else - m_weapons_locked = Qt::Unchecked; + _weaponsLocked = Qt::Unchecked; // Lock primaries - if (Ships[m_ship].flags[Ship::Ship_Flags::Primaries_locked]) { - m_primaries_locked = Qt::Checked; + if (Ships[_ship].flags[Ship::Ship_Flags::Primaries_locked]) { + _primariesLocked = Qt::Checked; } else { - m_primaries_locked = Qt::Unchecked; + _primariesLocked = Qt::Unchecked; } // Lock secondaries - if (Ships[m_ship].flags[Ship::Ship_Flags::Secondaries_locked]) { - m_secondaries_locked = Qt::Checked; + if (Ships[_ship].flags[Ship::Ship_Flags::Secondaries_locked]) { + _secondariesLocked = Qt::Checked; } else { - m_secondaries_locked = Qt::Unchecked; + _secondariesLocked = Qt::Unchecked; } // Lock turrets - if (Ships[m_ship].flags[Ship::Ship_Flags::Lock_all_turrets_initially]) { - m_turrets_locked = Qt::Checked; + if (Ships[_ship].flags[Ship::Ship_Flags::Lock_all_turrets_initially]) { + _turretsLocked = Qt::Checked; } else { - m_turrets_locked = Qt::Unchecked; + _turretsLocked = Qt::Unchecked; } - if (Ships[m_ship].flags[Ship::Ship_Flags::Afterburner_locked]) { - m_afterburner_locked = Qt::Checked; + if (Ships[_ship].flags[Ship::Ship_Flags::Afterburner_locked]) { + _afterburnerLocked = Qt::Checked; } else { - m_afterburner_locked = Qt::Unchecked; + _afterburnerLocked = Qt::Unchecked; } - if (m_multi_edit) { + if (_multiEdit) { objp = GET_FIRST(&obj_used_list); while (objp != END_OF_LIST(&obj_used_list)) { if (((objp->type == OBJ_SHIP) || (objp->type == OBJ_START)) && (objp->flags[Object::Object_Flags::Marked])) { - if (objp->phys_info.speed != m_velocity) + if (objp->phys_info.speed != _velocity) vflag = 1; - if (static_cast(objp->shield_quadrant[0]) != m_shields) + if (static_cast(objp->shield_quadrant[0]) != _shields) sflag = 1; - if (static_cast(objp->hull_strength) != m_hull) + if (static_cast(objp->hull_strength) != _hull) hflag = 1; if (objp->flags[Object::Object_Flags::No_shields]) { - if (m_has_shields) - m_has_shields = Qt::PartiallyChecked; + if (_hasShields) + _hasShields = Qt::PartiallyChecked; } else { - if (!m_has_shields) - m_has_shields = Qt::PartiallyChecked; + if (!_hasShields) + _hasShields = Qt::PartiallyChecked; } Assert((objp->type == OBJ_SHIP) || (objp->type == OBJ_START)); if (Ships[get_ship_from_obj(objp)].flags[Ship::Ship_Flags::Force_shields_on]) { - if (!m_force_shields) - m_force_shields = Qt::PartiallyChecked; + if (!_forceShields) + _forceShields = Qt::PartiallyChecked; } else { - if (m_force_shields) - m_force_shields = Qt::PartiallyChecked; + if (_forceShields) + _forceShields = Qt::PartiallyChecked; } if (Ships[get_ship_from_obj(objp)].flags[Ship::Ship_Flags::Ship_locked]) { - if (!m_ship_locked) - m_ship_locked = Qt::PartiallyChecked; + if (!_shipLocked) + _shipLocked = Qt::PartiallyChecked; } else { - if (m_ship_locked) - m_ship_locked = Qt::PartiallyChecked; + if (_shipLocked) + _shipLocked = Qt::PartiallyChecked; } if (Ships[get_ship_from_obj(objp)].flags[Ship::Ship_Flags::Weapons_locked]) { - if (!m_weapons_locked) - m_weapons_locked = Qt::PartiallyChecked; + if (!_weaponsLocked) + _weaponsLocked = Qt::PartiallyChecked; } else { - if (m_weapons_locked) - m_weapons_locked = Qt::PartiallyChecked; + if (_weaponsLocked) + _weaponsLocked = Qt::PartiallyChecked; } if (Ships[get_ship_from_obj(objp)].flags[Ship::Ship_Flags::Primaries_locked]) { - if (!m_primaries_locked) - m_primaries_locked = Qt::PartiallyChecked; + if (!_primariesLocked) + _primariesLocked = Qt::PartiallyChecked; } else { - if (m_primaries_locked) - m_primaries_locked = Qt::PartiallyChecked; + if (_primariesLocked) + _primariesLocked = Qt::PartiallyChecked; } if (Ships[get_ship_from_obj(objp)].flags[Ship::Ship_Flags::Secondaries_locked]) { - if (!m_secondaries_locked) - m_secondaries_locked = Qt::PartiallyChecked; + if (!_secondariesLocked) + _secondariesLocked = Qt::PartiallyChecked; } else { - if (m_secondaries_locked) - m_secondaries_locked = Qt::PartiallyChecked; + if (_secondariesLocked) + _secondariesLocked = Qt::PartiallyChecked; } if (Ships[get_ship_from_obj(objp)].flags[Ship::Ship_Flags::Lock_all_turrets_initially]) { - if (!m_turrets_locked) - m_turrets_locked = Qt::PartiallyChecked; + if (!_turretsLocked) + _turretsLocked = Qt::PartiallyChecked; } else { - if (m_turrets_locked) - m_turrets_locked = Qt::PartiallyChecked; + if (_turretsLocked) + _turretsLocked = Qt::PartiallyChecked; } if (Ships[get_ship_from_obj(objp)].flags[Ship::Ship_Flags::Afterburner_locked]) { - if (!m_afterburner_locked) - m_afterburner_locked = Qt::PartiallyChecked; + if (!_afterburnerLocked) + _afterburnerLocked = Qt::PartiallyChecked; } else { - if (m_afterburner_locked) - m_afterburner_locked = Qt::PartiallyChecked; + if (_afterburnerLocked) + _afterburnerLocked = Qt::PartiallyChecked; } } @@ -274,47 +273,48 @@ void ShipInitialStatusDialogModel::initializeData(bool multi) } } - if (Ship_info[Ships[m_ship].ship_info_index].uses_team_colors) { - m_use_teams = true; - m_team_color_setting = Ships[m_ship].team_name; + if (Ship_info[Ships[_ship].ship_info_index].uses_team_colors) { + _useTeams = true; + _teamColorSetting = Ships[_ship].team_name; } - change_subsys(0); + changeSubsys(0); if (vflag) { - m_velocity = BLANKFIELD; + _velocity = BLANKFIELD; } if (vflag) { - m_velocity = BLANKFIELD; + _velocity = BLANKFIELD; } if (vflag) { - m_velocity = BLANKFIELD; + _velocity = BLANKFIELD; } + _modified = false; modelChanged(); _modified = false; } -void ShipInitialStatusDialogModel::update_docking_info() +void ShipInitialStatusDialogModel::updateDockingInfo() { int i; - object* objp = &Objects[Ships[m_ship].objnum]; - for (i = 0; i < num_dock_points; i++) { + object* objp = &Objects[Ships[_ship].objnum]; + for (i = 0; i < _numDockPoints; i++) { // see if the object at this point is no longer there object* dockee_objp = dock_find_object_at_dockpoint(objp, i); if (dockee_objp != nullptr) { // check if the dockee ship thinks that this ship is docked to this dock point - if (objp != dock_find_object_at_dockpoint(dockee_objp, dockpoint_array[i].dockee_point)) { + if (objp != dock_find_object_at_dockpoint(dockee_objp, _dockpointArray[i].dockee_point)) { // undock it undock(objp, dockee_objp); } } } // add new info - for (i = 0; i < num_dock_points; i++) { + for (i = 0; i < _numDockPoints; i++) { // see if there is an object at this point that wasn't there before - if (dockpoint_array[i].dockee_shipnum >= 0) { + if (_dockpointArray[i].dockee_shipnum >= 0) { if (dock_find_object_at_dockpoint(objp, i) == nullptr) { - object* dockee_objp = &Objects[Ships[dockpoint_array[i].dockee_shipnum].objnum]; - int dockee_point = dockpoint_array[i].dockee_point; + object* dockee_objp = &Objects[Ships[_dockpointArray[i].dockee_shipnum].objnum]; + int dockee_point = _dockpointArray[i].dockee_point; // dock it dock(objp, i, dockee_objp, dockee_point); @@ -324,6 +324,7 @@ void ShipInitialStatusDialogModel::update_docking_info() _editor->missionChanged(); } + void ShipInitialStatusDialogModel::undock(object* objp1, object* objp2) { vec3d v; @@ -335,7 +336,7 @@ void ShipInitialStatusDialogModel::undock(object* objp1, object* objp2) ship_num = get_ship_from_obj(OBJ_INDEX(objp1)); other_ship_num = get_ship_from_obj(OBJ_INDEX(objp2)); - if (m_move_ships_when_undocking) { + if (_moveShipsWhenUndocking) { if (ship_class_compare(Ships[ship_num].ship_info_index, Ships[other_ship_num].ship_info_index) <= 0) { vm_vec_scale_add2(&objp2->pos, &v, @@ -367,36 +368,38 @@ void ShipInitialStatusDialogModel::undock(object* objp1, object* objp2) if (!object_is_docked(&Objects[Ships[other_ship_num].objnum])) Ships[other_ship_num].flags.remove(Ship::Ship_Flags::Dock_leader); } -void ShipInitialStatusDialogModel::dock(object* objp, int dockpoint, object* other_objp, int other_dockpoint) + +void ShipInitialStatusDialogModel::dock(object* objp, int dockpoint, object* otherObjp, int otherDockpoint) { - if (objp == nullptr || other_objp == nullptr) { + if (objp == nullptr || otherObjp == nullptr) { return; } - if (dockpoint < 0 || other_dockpoint < 0) { + if (dockpoint < 0 || otherDockpoint < 0) { return; } dock_function_info dfi; // do the docking (do it in reverse so that the current object stays put) - ai_dock_with_object(other_objp, other_dockpoint, objp, dockpoint, AIDO_DOCK_NOW); + ai_dock_with_object(otherObjp, otherDockpoint, objp, dockpoint, AIDO_DOCK_NOW); // unmark the handled flag in preparation for the next step - dock_evaluate_all_docked_objects(objp, &dfi, initial_status_unmark_dock_handled_flag); + dockEvaluateAllDockedObjects(objp, &dfi, initialStatusUnmarkDockHandledFlag); // move all other objects to catch up with it dock_move_docked_objects(objp); // set the dock leader - dock_evaluate_all_docked_objects(objp, &dfi, initial_status_mark_dock_leader_helper); + dockEvaluateAllDockedObjects(objp, &dfi, initialStatusMarkDockLeaderHelper); // if no leader, mark me if (dfi.maintained_variables.int_value == 0) Ships[objp->instance].flags.set(Ship::Ship_Flags::Dock_leader); } + // Duplicated from objectdock to handle scope errors -void ShipInitialStatusDialogModel::dock_evaluate_all_docked_objects(object* objp, +void ShipInitialStatusDialogModel::dockEvaluateAllDockedObjects(object* objp, dock_function_info* infop, void (*function)(object*, dock_function_info*, EditorViewport*)) { @@ -451,7 +454,7 @@ void ShipInitialStatusDialogModel::dock_evaluate_all_docked_objects(object* objp memset(visited_bitstring, 0, calculate_num_bytes(MAX_OBJECTS)); // start evaluating the tree - dock_evaluate_tree(objp, infop, function, visited_bitstring); + dockEvaluateTree(objp, infop, function, visited_bitstring); // destroy the bit array vm_free(visited_bitstring); @@ -459,7 +462,7 @@ void ShipInitialStatusDialogModel::dock_evaluate_all_docked_objects(object* objp } } -void ShipInitialStatusDialogModel::dock_evaluate_all_docked_objects(object* objp, +void ShipInitialStatusDialogModel::dockEvaluateAllDockedObjects(object* objp, dock_function_info* infop, void (*function)(object*)) { @@ -514,7 +517,7 @@ void ShipInitialStatusDialogModel::dock_evaluate_all_docked_objects(object* objp memset(visited_bitstring, 0, calculate_num_bytes(MAX_OBJECTS)); // start evaluating the tree - dock_evaluate_tree(objp, infop, function, visited_bitstring); + dockEvaluateTree(objp, infop, function, visited_bitstring); // destroy the bit array vm_free(visited_bitstring); @@ -522,7 +525,7 @@ void ShipInitialStatusDialogModel::dock_evaluate_all_docked_objects(object* objp } } -void ShipInitialStatusDialogModel::dock_evaluate_tree(object* objp, +void ShipInitialStatusDialogModel::dockEvaluateTree(object* objp, dock_function_info* infop, void (*function)(object*, dock_function_info*, EditorViewport*), ubyte* visited_bitstring) @@ -542,13 +545,13 @@ void ShipInitialStatusDialogModel::dock_evaluate_tree(object* objp, // iterate through all docked objects for (dock_instance* ptr = objp->dock_list; ptr != nullptr; ptr = ptr->next) { // start another tree with the docked object as the root, and return if instructed - dock_evaluate_tree(ptr->docked_objp, infop, function, visited_bitstring); + dockEvaluateTree(ptr->docked_objp, infop, function, visited_bitstring); if (infop->early_return_condition) return; } } -void ShipInitialStatusDialogModel::dock_evaluate_tree(object* objp, +void ShipInitialStatusDialogModel::dockEvaluateTree(object* objp, dock_function_info* infop, void (*function)(object*), ubyte* visited_bitstring) @@ -568,7 +571,7 @@ void ShipInitialStatusDialogModel::dock_evaluate_tree(object* objp, // iterate through all docked objects for (dock_instance* ptr = objp->dock_list; ptr != nullptr; ptr = ptr->next) { // start another tree with the docked object as the root, and return if instructed - dock_evaluate_tree(ptr->docked_objp, infop, function, visited_bitstring); + dockEvaluateTree(ptr->docked_objp, infop, function, visited_bitstring); if (infop->early_return_condition) return; } @@ -578,58 +581,58 @@ bool ShipInitialStatusDialogModel::apply() { object* objp; - change_subsys(0); - if (m_multi_edit) { + changeSubsys(0); + if (_multiEdit) { objp = GET_FIRST(&obj_used_list); while (objp != END_OF_LIST(&obj_used_list)) { if (((objp->type == OBJ_SHIP) || (objp->type == OBJ_START)) && (objp->flags[Object::Object_Flags::Marked])) { - modify(objp->phys_info.speed, (float)m_velocity); + modify(objp->phys_info.speed, (float)_velocity); - modify(objp->shield_quadrant[0], (float)m_shields); + modify(objp->shield_quadrant[0], (float)_shields); - modify(objp->hull_strength, (float)m_hull); + modify(objp->hull_strength, (float)_hull); - if (m_has_shields == Qt::Checked) { + if (_hasShields == Qt::Checked) { objp->flags.remove(Object::Object_Flags::No_shields); - } else if (m_has_shields == Qt::Unchecked) { + } else if (_hasShields == Qt::Unchecked) { objp->flags.set(Object::Object_Flags::No_shields); } auto shipp = &Ships[get_ship_from_obj(objp)]; - shipp->ship_guardian_threshold = guardian_threshold; + shipp->ship_guardian_threshold = _guardianThreshold; // We need to ensure that we handle the inconsistent "boolean" value correctly - handle_inconsistent_flag(shipp->flags, Ship::Ship_Flags::Force_shields_on, m_force_shields); - handle_inconsistent_flag(shipp->flags, Ship::Ship_Flags::Ship_locked, m_ship_locked); - handle_inconsistent_flag(shipp->flags, Ship::Ship_Flags::Weapons_locked, m_weapons_locked); - handle_inconsistent_flag(shipp->flags, Ship::Ship_Flags::Primaries_locked, m_primaries_locked); - handle_inconsistent_flag(shipp->flags, Ship::Ship_Flags::Secondaries_locked, m_secondaries_locked); - handle_inconsistent_flag(shipp->flags, Ship::Ship_Flags::Lock_all_turrets_initially, m_turrets_locked); - handle_inconsistent_flag(shipp->flags, Ship::Ship_Flags::Afterburner_locked, m_afterburner_locked); - shipp->team_name = m_team_color_setting; + handle_inconsistent_flag(shipp->flags, Ship::Ship_Flags::Force_shields_on, _forceShields); + handle_inconsistent_flag(shipp->flags, Ship::Ship_Flags::Ship_locked, _shipLocked); + handle_inconsistent_flag(shipp->flags, Ship::Ship_Flags::Weapons_locked, _weaponsLocked); + handle_inconsistent_flag(shipp->flags, Ship::Ship_Flags::Primaries_locked, _primariesLocked); + handle_inconsistent_flag(shipp->flags, Ship::Ship_Flags::Secondaries_locked, _secondariesLocked); + handle_inconsistent_flag(shipp->flags, Ship::Ship_Flags::Lock_all_turrets_initially, _turretsLocked); + handle_inconsistent_flag(shipp->flags, Ship::Ship_Flags::Afterburner_locked, _afterburnerLocked); + shipp->team_name = _teamColorSetting; } objp = GET_NEXT(objp); } } else { - modify(Objects[_editor->currentObject].phys_info.speed, (float)m_velocity); - modify(Objects[_editor->currentObject].shield_quadrant[0], (float)m_shields); - modify(Objects[_editor->currentObject].hull_strength, (float)m_hull); + modify(Objects[_editor->currentObject].phys_info.speed, (float)_velocity); + modify(Objects[_editor->currentObject].shield_quadrant[0], (float)_shields); + modify(Objects[_editor->currentObject].hull_strength, (float)_hull); - Objects[_editor->currentObject].flags.set(Object::Object_Flags::No_shields, m_has_shields == 0); - Ships[m_ship].ship_guardian_threshold = guardian_threshold; + Objects[_editor->currentObject].flags.set(Object::Object_Flags::No_shields, _hasShields == 0); + Ships[_ship].ship_guardian_threshold = _guardianThreshold; // We need to ensure that we handle the inconsistent "boolean" value correctly. Not strictly needed here but // just to be safe... - handle_inconsistent_flag(Ships[m_ship].flags, Ship::Ship_Flags::Force_shields_on, m_force_shields); - handle_inconsistent_flag(Ships[m_ship].flags, Ship::Ship_Flags::Ship_locked, m_ship_locked); - handle_inconsistent_flag(Ships[m_ship].flags, Ship::Ship_Flags::Weapons_locked, m_weapons_locked); - handle_inconsistent_flag(Ships[m_ship].flags, Ship::Ship_Flags::Primaries_locked, m_primaries_locked); - handle_inconsistent_flag(Ships[m_ship].flags, Ship::Ship_Flags::Secondaries_locked, m_secondaries_locked); - handle_inconsistent_flag(Ships[m_ship].flags, Ship::Ship_Flags::Lock_all_turrets_initially, m_turrets_locked); - handle_inconsistent_flag(Ships[m_ship].flags, Ship::Ship_Flags::Afterburner_locked, m_afterburner_locked); - Ships[m_ship].team_name = m_team_color_setting; - } - update_docking_info(); + handle_inconsistent_flag(Ships[_ship].flags, Ship::Ship_Flags::Force_shields_on, _forceShields); + handle_inconsistent_flag(Ships[_ship].flags, Ship::Ship_Flags::Ship_locked, _shipLocked); + handle_inconsistent_flag(Ships[_ship].flags, Ship::Ship_Flags::Weapons_locked, _weaponsLocked); + handle_inconsistent_flag(Ships[_ship].flags, Ship::Ship_Flags::Primaries_locked, _primariesLocked); + handle_inconsistent_flag(Ships[_ship].flags, Ship::Ship_Flags::Secondaries_locked, _secondariesLocked); + handle_inconsistent_flag(Ships[_ship].flags, Ship::Ship_Flags::Lock_all_turrets_initially, _turretsLocked); + handle_inconsistent_flag(Ships[_ship].flags, Ship::Ship_Flags::Afterburner_locked, _afterburnerLocked); + Ships[_ship].team_name = _teamColorSetting; + } + updateDockingInfo(); _editor->missionChanged(); return true; } @@ -638,190 +641,191 @@ void ShipInitialStatusDialogModel::reject() {} bool ShipInitialStatusDialogModel::getMoveShipsWhenUndocking() const { - return m_move_ships_when_undocking; + return _moveShipsWhenUndocking; } -void ShipInitialStatusDialogModel::setMoveShipsWhenUndocking(bool value) + +void ShipInitialStatusDialogModel::setMoveShipsWhenUndocking(bool moveShips) { - modify(m_move_ships_when_undocking, value); + modify(_moveShipsWhenUndocking, moveShips); } -void ShipInitialStatusDialogModel::setVelocity(const int value) +void ShipInitialStatusDialogModel::setVelocity(int velocity) { - modify(m_velocity, value); + modify(_velocity, velocity); } int ShipInitialStatusDialogModel::getVelocity() const { - return m_velocity; + return _velocity; } -void ShipInitialStatusDialogModel::setHull(const int value) +void ShipInitialStatusDialogModel::setHull(int hull) { - modify(m_hull, value); + modify(_hull, hull); } int ShipInitialStatusDialogModel::getHull() const { - return m_hull; + return _hull; } -void ShipInitialStatusDialogModel::setHasShield(const int state) +void ShipInitialStatusDialogModel::setHasShield(int hasShield) { - modify(m_has_shields, state); + modify(_hasShields, hasShield); } int ShipInitialStatusDialogModel::getHasShield() const { - return m_has_shields; + return _hasShields; } -void ShipInitialStatusDialogModel::setShieldHull(const int value) +void ShipInitialStatusDialogModel::setShieldHull(int shieldHull) { - modify(m_shields, value); + modify(_shields, shieldHull); } int ShipInitialStatusDialogModel::getShieldHull() const { - return m_shields; + return _shields; } -void ShipInitialStatusDialogModel::setForceShield(const int state) +void ShipInitialStatusDialogModel::setForceShield(int forceShield) { - modify(m_force_shields, state); + modify(_forceShields, forceShield); } int ShipInitialStatusDialogModel::getForceShield() const { - return m_force_shields; + return _forceShields; } -void ShipInitialStatusDialogModel::setShipLocked(const int state) +void ShipInitialStatusDialogModel::setShipLocked(int locked) { - modify(m_ship_locked, state); + modify(_shipLocked, locked); } int ShipInitialStatusDialogModel::getShipLocked() const { - return m_ship_locked; + return _shipLocked; } -void ShipInitialStatusDialogModel::setWeaponLocked(const int state) +void ShipInitialStatusDialogModel::setWeaponLocked(int locked) { - modify(m_weapons_locked, state); + modify(_weaponsLocked, locked); } int ShipInitialStatusDialogModel::getWeaponLocked() const { - return m_weapons_locked; + return _weaponsLocked; } -void ShipInitialStatusDialogModel::setPrimariesDisabled(const int state) +void ShipInitialStatusDialogModel::setPrimariesDisabled(int disabled) { - modify(m_primaries_locked, state); + modify(_primariesLocked, disabled); } int ShipInitialStatusDialogModel::getPrimariesDisabled() const { - return m_primaries_locked; + return _primariesLocked; } -void ShipInitialStatusDialogModel::setSecondariesDisabled(const int state) +void ShipInitialStatusDialogModel::setSecondariesDisabled(int disabled) { - modify(m_secondaries_locked, state); + modify(_secondariesLocked, disabled); } int ShipInitialStatusDialogModel::getSecondariesDisabled() const { - return m_secondaries_locked; + return _secondariesLocked; } -void ShipInitialStatusDialogModel::setTurretsDisabled(const int state) +void ShipInitialStatusDialogModel::setTurretsDisabled(int disabled) { - modify(m_turrets_locked, state); + modify(_turretsLocked, disabled); } int ShipInitialStatusDialogModel::getTurretsDisabled() const { - return m_turrets_locked; + return _turretsLocked; } -void ShipInitialStatusDialogModel::setAfterburnerDisabled(const int state) +void ShipInitialStatusDialogModel::setAfterburnerDisabled(int disabled) { - modify(m_afterburner_locked, state); + modify(_afterburnerLocked, disabled); } int ShipInitialStatusDialogModel::getAfterburnerDisabled() const { - return m_afterburner_locked; + return _afterburnerLocked; } -void ShipInitialStatusDialogModel::setDamage(const int value) +void ShipInitialStatusDialogModel::setDamage(int damage) { - modify(m_damage, value); + modify(_damage, damage); } int ShipInitialStatusDialogModel::getDamage() const { - return m_damage; + return _damage; } SCP_string ShipInitialStatusDialogModel::getCargo() const { - return m_cargo_name; + return _cargoName; } -void ShipInitialStatusDialogModel::setCargo(const SCP_string& text) +void ShipInitialStatusDialogModel::setCargo(const SCP_string& cargo) { - modify(m_cargo_name, text); + modify(_cargoName, cargo); } SCP_string ShipInitialStatusDialogModel::getCargoTitle() const { - return m_cargo_title; + return _cargoTitle; } -void ShipInitialStatusDialogModel::setCargoTitle(const SCP_string& text) +void ShipInitialStatusDialogModel::setCargoTitle(const SCP_string& cargoTitle) { - modify(m_cargo_title, text); + modify(_cargoTitle, cargoTitle); } -SCP_string ShipInitialStatusDialogModel::getColour() const +SCP_string ShipInitialStatusDialogModel::getColor() const { - return m_team_color_setting; + return _teamColorSetting; } -void ShipInitialStatusDialogModel::setColour(const SCP_string& text) +void ShipInitialStatusDialogModel::setColor(const SCP_string& color) { - modify(m_team_color_setting, text); + modify(_teamColorSetting, color); } -void ShipInitialStatusDialogModel::change_subsys(const int new_subsys) +void ShipInitialStatusDialogModel::changeSubsys(int subsysIndex) { int z, cargo_index; ship_subsys* ptr; // Goober5000 - ship_has_scannable_subsystems = Ship_info[Ships[m_ship].ship_info_index].is_huge_ship(); - if (Ships[m_ship].flags[Ship::Ship_Flags::Toggle_subsystem_scanning]) { - ship_has_scannable_subsystems = ~ship_has_scannable_subsystems; + _shipHasScannableSubsystems = Ship_info[Ships[_ship].ship_info_index].is_huge_ship(); + if (Ships[_ship].flags[Ship::Ship_Flags::Toggle_subsystem_scanning]) { + _shipHasScannableSubsystems = ~_shipHasScannableSubsystems; } - if (cur_subsys != -1) { - ptr = GET_FIRST(&Ships[m_ship].subsys_list); - while (cur_subsys--) { - Assert(ptr != END_OF_LIST(&Ships[m_ship].subsys_list)); + if (_curSubsys != -1) { + ptr = GET_FIRST(&Ships[_ship].subsys_list); + while (_curSubsys--) { + Assert(ptr != END_OF_LIST(&Ships[_ship].subsys_list)); ptr = GET_NEXT(ptr); } - ptr->current_hits = 100.0f - (float)m_damage; + ptr->current_hits = 100.0f - (float)_damage; // update cargo name - if (!m_cargo_name.empty()) { //-V805 - lcl_fred_replace_stuff(m_cargo_name); - cargo_index = string_lookup(m_cargo_name.c_str(), Cargo_names, Num_cargo); + if (!_cargoName.empty()) { //-V805 + lcl_fred_replace_stuff(_cargoName); + cargo_index = string_lookup(_cargoName.c_str(), Cargo_names, Num_cargo); if (cargo_index == -1) { if (Num_cargo < MAX_CARGO) { cargo_index = Num_cargo++; - strcpy(Cargo_names[cargo_index], m_cargo_name.c_str()); + strcpy(Cargo_names[cargo_index], _cargoName.c_str()); ptr->subsys_cargo_name = cargo_index; } else { SCP_string str; @@ -838,27 +842,27 @@ void ShipInitialStatusDialogModel::change_subsys(const int new_subsys) } // update cargo title - strcpy_s(ptr->subsys_cargo_title, m_cargo_title.c_str()); + strcpy_s(ptr->subsys_cargo_title, _cargoTitle.c_str()); } - cur_subsys = z = new_subsys; + _curSubsys = z = subsysIndex; if (z == -1) { - m_damage = 100; + _damage = 100; } else { - ptr = GET_FIRST(&Ships[m_ship].subsys_list); + ptr = GET_FIRST(&Ships[_ship].subsys_list); while (z--) { - Assert(ptr != END_OF_LIST(&Ships[m_ship].subsys_list)); + Assert(ptr != END_OF_LIST(&Ships[_ship].subsys_list)); ptr = GET_NEXT(ptr); } - m_damage = 100 - static_cast(ptr->current_hits); + _damage = 100 - static_cast(ptr->current_hits); if (ptr->subsys_cargo_name > 0) { - m_cargo_name = Cargo_names[ptr->subsys_cargo_name]; + _cargoName = Cargo_names[ptr->subsys_cargo_name]; } else { - m_cargo_name = ""; + _cargoName = ""; } - m_cargo_title = ptr->subsys_cargo_title; + _cargoTitle = ptr->subsys_cargo_title; } set_modified(); modelChanged(); @@ -866,58 +870,58 @@ void ShipInitialStatusDialogModel::change_subsys(const int new_subsys) int ShipInitialStatusDialogModel::getShip() const { - return m_ship; + return _ship; } -int ShipInitialStatusDialogModel::getnum_dock_points() const +int ShipInitialStatusDialogModel::getNumDockPoints() const { - return num_dock_points; + return _numDockPoints; } -int ShipInitialStatusDialogModel::getShip_has_scannable_subsystems() const +int ShipInitialStatusDialogModel::getShipHasScannableSubsystems() const { - return ship_has_scannable_subsystems; + return _shipHasScannableSubsystems; } -dockpoint_information* ShipInitialStatusDialogModel::getdockpoint_array() const +dockpoint_information* ShipInitialStatusDialogModel::getDockpointArray() const { - return dockpoint_array; + return _dockpointArray; } -void ShipInitialStatusDialogModel::setDockee(const int point, const int ship) +void ShipInitialStatusDialogModel::setDockee(int dockPointIndex, int dockeeShipnum) { - modify(dockpoint_array[point].dockee_shipnum, ship); - modify(dockpoint_array[point].dockee_point, -1); + modify(_dockpointArray[dockPointIndex].dockee_shipnum, dockeeShipnum); + modify(_dockpointArray[dockPointIndex].dockee_point, -1); } -void ShipInitialStatusDialogModel::setDockeePoint(const int dockPoint, const int dockeePoint) +void ShipInitialStatusDialogModel::setDockeePoint(int dockPointIndex, int dockeePoint) { - modify(dockpoint_array[dockPoint].dockee_point, dockeePoint); + modify(_dockpointArray[dockPointIndex].dockee_point, dockeePoint); } bool ShipInitialStatusDialogModel::getUseTeamcolours() const { - return m_use_teams; + return _useTeams; } -bool ShipInitialStatusDialogModel::getIfMultpleShips() const +bool ShipInitialStatusDialogModel::getIfMultipleShips() const { - return m_multi_edit; + return _multiEdit; } int ShipInitialStatusDialogModel::getGuardian() const { - return guardian_threshold; + return _guardianThreshold; } -void ShipInitialStatusDialogModel::setGuardian(int value) +void ShipInitialStatusDialogModel::setGuardian(int guardian) { - modify(guardian_threshold, value); + modify(_guardianThreshold, guardian); } bool ShipInitialStatusDialogModel::getToggleSubsystemScanning() const { - return Ships[m_ship].flags[Ship::Ship_Flags::Toggle_subsystem_scanning]; + return Ships[_ship].flags[Ship::Ship_Flags::Toggle_subsystem_scanning]; } bool ShipInitialStatusDialogModel::getUseNewScanningBehavior() @@ -925,4 +929,4 @@ bool ShipInitialStatusDialogModel::getUseNewScanningBehavior() return Use_new_scanning_behavior; } -} // namespace fso::fred::dialogs \ No newline at end of file +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.h index f62ec91e214..0e6060f7c40 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.h @@ -5,149 +5,137 @@ #include #include -namespace fso { -namespace fred { -namespace dialogs { -typedef struct dockpoint_information { +namespace fso::fred::dialogs { + +struct dockpoint_information { int dockee_shipnum; int dockee_point; -} dockpoint_information; +}; + constexpr auto BLANKFIELD = 101; -void initial_status_mark_dock_leader_helper(object* objp, dock_function_info* infop, EditorViewport*); -void initial_status_unmark_dock_handled_flag(object* objp); -void reset_arrival_to_false(int shipnum, bool reset_wing, EditorViewport*); -bool set_cue_to_false(int* cue); -class ShipInitialStatusDialogModel : public AbstractDialogModel { - private: - - int guardian_threshold; - int m_ship; - int cur_subsys = -1; - int m_damage; - int m_shields; - int m_force_shields; - int m_velocity; - int m_hull; - int m_has_shields; - int m_ship_locked; - int m_weapons_locked; - SCP_string m_cargo_name; - SCP_string m_cargo_title; - int m_primaries_locked; - int m_secondaries_locked; - int m_turrets_locked; - int m_afterburner_locked; - SCP_string m_team_color_setting; - - int ship_has_scannable_subsystems; - - int num_dock_points; - - dockpoint_information* dockpoint_array; - - - void update_docking_info(); - void undock(object*, object*); - void dock(object*, int, object*, int); - void dock_evaluate_all_docked_objects(object* objp, - dock_function_info* infop, - void (*function)(object*, dock_function_info*, EditorViewport*)); - void dock_evaluate_all_docked_objects(object* objp, dock_function_info* infop, void (*function)(object*)); - void dock_evaluate_tree(object* objp, - dock_function_info* infop, - void (*function)(object*, dock_function_info*, EditorViewport*), - ubyte* visited_bitstring); - void - dock_evaluate_tree(object* objp, dock_function_info* infop, void (*function)(object*), ubyte* visited_bitstring); - bool m_multi_edit; - bool m_use_teams = false; - bool m_move_ships_when_undocking = true; +void initialStatusMarkDockLeaderHelper(object* objp, dock_function_info* infop, EditorViewport* viewport); +void initialStatusUnmarkDockHandledFlag(object* objp); +void resetArrivalToFalse(int shipnum, bool resetWing, EditorViewport* viewport); +bool setCueToFalse(int* cue); +class ShipInitialStatusDialogModel : public AbstractDialogModel { + Q_OBJECT public: ShipInitialStatusDialogModel(QObject* parent, EditorViewport* viewport, bool multi); - void initializeData(bool); bool apply() override; void reject() override; - void setVelocity(const int); + void setVelocity(int velocity); int getVelocity() const; - void setHull(const int); + void setHull(int hull); int getHull() const; - void setHasShield(const int); + void setHasShield(int hasShield); int getHasShield() const; - void setShieldHull(const int); + void setShieldHull(int shieldHull); int getShieldHull() const; - void setForceShield(const int); + void setForceShield(int forceShield); int getForceShield() const; - void setShipLocked(const int); + void setShipLocked(int locked); int getShipLocked() const; - void setWeaponLocked(const int); + void setWeaponLocked(int locked); int getWeaponLocked() const; - void setPrimariesDisabled(const int); + void setPrimariesDisabled(int disabled); int getPrimariesDisabled() const; - void setSecondariesDisabled(const int); + void setSecondariesDisabled(int disabled); int getSecondariesDisabled() const; - void setTurretsDisabled(const int); + void setTurretsDisabled(int disabled); int getTurretsDisabled() const; - void setAfterburnerDisabled(const int); + void setAfterburnerDisabled(int disabled); int getAfterburnerDisabled() const; - void setDamage(const int); + void setDamage(int damage); int getDamage() const; SCP_string getCargo() const; - void setCargo(const SCP_string&); + void setCargo(const SCP_string& cargo); SCP_string getCargoTitle() const; - void setCargoTitle(const SCP_string&); + void setCargoTitle(const SCP_string& cargoTitle); - SCP_string getColour() const; - void setColour(const SCP_string&); + SCP_string getColor() const; + void setColor(const SCP_string& color); - void change_subsys(const int); + void changeSubsys(int subsysIndex); int getShip() const; - int getnum_dock_points() const; - int getShip_has_scannable_subsystems() const; - dockpoint_information* getdockpoint_array() const; - void setDockee(const int, const int); - void setDockeePoint(const int, const int); + int getNumDockPoints() const; + int getShipHasScannableSubsystems() const; + dockpoint_information* getDockpointArray() const; + void setDockee(int dockPointIndex, int dockeeShipnum); + void setDockeePoint(int dockPointIndex, int dockeePoint); bool getUseTeamcolours() const; - bool getIfMultpleShips() const; + bool getIfMultipleShips() const; bool getToggleSubsystemScanning() const; static bool getUseNewScanningBehavior(); int getGuardian() const; - void setGuardian(int); + void setGuardian(int guardian); bool getMoveShipsWhenUndocking() const; - void setMoveShipsWhenUndocking(bool); + void setMoveShipsWhenUndocking(bool moveShips); + + private: // NOLINT(readability-redundant-access-specifiers) + void initializeData(bool multi); + void updateDockingInfo(); + void undock(object* objp1, object* objp2); + void dock(object* objp, int dockpoint, object* otherObjp, int otherDockpoint); + void dockEvaluateAllDockedObjects(object* objp, + dock_function_info* infop, + void (*function)(object*, dock_function_info*, EditorViewport*)); + void dockEvaluateAllDockedObjects(object* objp, dock_function_info* infop, void (*function)(object*)); + void dockEvaluateTree(object* objp, + dock_function_info* infop, + void (*function)(object*, dock_function_info*, EditorViewport*), + ubyte* visited_bitstring); + void dockEvaluateTree(object* objp, dock_function_info* infop, void (*function)(object*), ubyte* visited_bitstring); + + int _guardianThreshold; + int _ship; + int _curSubsys = -1; + int _damage; + int _shields; + int _forceShields; + int _velocity; + int _hull; + int _hasShields; + int _shipLocked; + int _weaponsLocked; + SCP_string _cargoName; + SCP_string _cargoTitle; + int _primariesLocked; + int _secondariesLocked; + int _turretsLocked; + int _afterburnerLocked; + SCP_string _teamColorSetting; + int _shipHasScannableSubsystems; + int _numDockPoints; + dockpoint_information* _dockpointArray; + bool _multiEdit; + bool _useTeams = false; + bool _moveShipsWhenUndocking = true; }; -/** - * @brief Handles setting a flag on a flagset when the value is inconsistent - * - * This is necessary in case multiple ships with inconsistent object flags have been selected in which case - * that flag may not be edited since it would corrupt the value of that flag. This function simplifies handling - * that case. - * @warning Contains QT code. Will need refactor if migrated to non QT environment - */ template -static void handle_inconsistent_flag(flagset& flags, T flag, int value) +void handle_inconsistent_flag(flagset& flags, T flag, int value) { if (value == CheckState::Checked) { flags.set(flag); @@ -155,6 +143,5 @@ static void handle_inconsistent_flag(flagset& flags, T flag, int value) flags.remove(flag); } } -} // namespace dialogs -} // namespace fred -} // namespace fso \ No newline at end of file + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipInitialStatusDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipInitialStatusDialog.cpp index cc87db32013..5125ce587f7 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipInitialStatusDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipInitialStatusDialog.cpp @@ -24,9 +24,9 @@ ShipInitialStatusDialog::ShipInitialStatusDialog(QDialog* parent, EditorViewport ui->cargoEdit->setMaxLength(NAME_LENGTH - 1); ui->cargoTitleEdit->setMaxLength(NAME_LENGTH - 1); - connect(_model.get(), &AbstractDialogModel::modelChanged, this, &ShipInitialStatusDialog::updateUI); + connect(_model.get(), &AbstractDialogModel::modelChanged, this, &ShipInitialStatusDialog::updateUi); - updateUI(); + updateUi(); // Resize the dialog to the minimum size resize(QDialog::sizeHint()); @@ -65,25 +65,25 @@ void ShipInitialStatusDialog::on_dockpointList_currentItemChanged(QListWidgetIte { if (!current) return; - cur_docker_point = current->data(Qt::UserRole).toInt(); - updateUI(); + _curDockerPoint = current->data(Qt::UserRole).toInt(); + updateUi(); } void ShipInitialStatusDialog::on_dockeeComboBox_currentIndexChanged(int index) { auto dockeeData = ui->dockeeComboBox->itemData(index).toInt(); - cur_dockee = dockeeData; - _model->setDockee(cur_docker_point, dockeeData); + _curDockee = dockeeData; + _model->setDockee(_curDockerPoint, dockeeData); - if (cur_dockee >= 0 && ui->dockeePointComboBox->count() > 0) { - cur_dockee_point = ui->dockeePointComboBox->itemData(0).toInt(); - _model->setDockeePoint(cur_docker_point, cur_dockee_point); + if (_curDockee >= 0 && ui->dockeePointComboBox->count() > 0) { + _curDockeePoint = ui->dockeePointComboBox->itemData(0).toInt(); + _model->setDockeePoint(_curDockerPoint, _curDockeePoint); } } void ShipInitialStatusDialog::on_dockeePointComboBox_currentIndexChanged(int index) { auto dockeeData = ui->dockeePointComboBox->itemData(index).toInt(); - cur_dockee_point = dockeeData; - _model->setDockeePoint(cur_docker_point, dockeeData); + _curDockeePoint = dockeeData; + _model->setDockeePoint(_curDockerPoint, dockeeData); } void ShipInitialStatusDialog::on_hullSpinBox_valueChanged(int value) { @@ -127,7 +127,7 @@ void ShipInitialStatusDialog::on_afterburnerLockCheckBox_stateChanged(int state) } void ShipInitialStatusDialog::on_subsystemList_currentRowChanged(int index) { - _model->change_subsys(index); + _model->changeSubsys(index); } void ShipInitialStatusDialog::on_subIntegritySpinBox_valueChanged(int value) { @@ -145,8 +145,8 @@ void ShipInitialStatusDialog::on_cargoTitleEdit_editingFinished() } void ShipInitialStatusDialog::on_colourComboBox_currentIndexChanged(int index) { - SCP_string colour = ui->colourComboBox->itemText(index).toUtf8().constData(); - _model->setColour(colour); + SCP_string color = ui->colourComboBox->itemText(index).toUtf8().constData(); + _model->setColor(color); } void ShipInitialStatusDialog::on_okPushButton_clicked() { @@ -163,7 +163,7 @@ void ShipInitialStatusDialog::on_moveShipsCheckBox_toggled(bool value) { _model->setMoveShipsWhenUndocking(value); } -void ShipInitialStatusDialog::updateUI() +void ShipInitialStatusDialog::updateUi() { util::SignalBlockers blockers(this); auto value = _model->getVelocity(); @@ -207,7 +207,7 @@ void ShipInitialStatusDialog::updateUI() ui->colourComboBox->addItem(Team_Name.c_str(), i); i++; } - auto currentText = _model->getColour(); + auto currentText = _model->getColor(); ui->colourComboBox->setCurrentIndex(ui->colourComboBox->findText(currentText.c_str())); } } @@ -232,8 +232,8 @@ void ShipInitialStatusDialog::updateDocks() { int row = ui->dockpointList->currentRow(); ui->dockpointList->clear(); - if (!_model->getIfMultpleShips()) { - for (int dockpoint = 0; dockpoint < _model->getnum_dock_points(); dockpoint++) { + if (!_model->getIfMultipleShips()) { + for (int dockpoint = 0; dockpoint < _model->getNumDockPoints(); dockpoint++) { auto newItem = new QListWidgetItem; newItem->setText( model_get_dock_name(Ship_info[Ships[_model->getShip()].ship_info_index].model_num, dockpoint)); @@ -243,20 +243,20 @@ void ShipInitialStatusDialog::updateDocks() } if (row >= 0 && row < ui->dockpointList->count()) ui->dockpointList->setCurrentRow(row); - if (cur_docker_point < 0) { + if (_curDockerPoint < 0) { // clear the dropdowns - list_dockees(-1); - list_dockee_points(-1); + listDockees(-1); + listDockeePoints(-1); } else { // populate with all possible dockees - list_dockees( - model_get_dock_index_type(Ship_info[Ships[_model->getShip()].ship_info_index].model_num, cur_docker_point)); + listDockees( + model_get_dock_index_type(Ship_info[Ships[_model->getShip()].ship_info_index].model_num, _curDockerPoint)); // see if there's a dockee here - if (_model->getdockpoint_array()[cur_docker_point].dockee_shipnum >= 0) { + if (_model->getDockpointArray()[_curDockerPoint].dockee_shipnum >= 0) { // select the dockee ui->dockeeComboBox->setCurrentIndex(ui->dockeeComboBox->findText( - Ships[_model->getdockpoint_array()[cur_docker_point].dockee_shipnum].ship_name)); + Ships[_model->getDockpointArray()[_curDockerPoint].dockee_shipnum].ship_name)); } else { ui->dockeeComboBox->setCurrentIndex(0); } @@ -264,27 +264,27 @@ void ShipInitialStatusDialog::updateDocks() } void ShipInitialStatusDialog::updateDockee() { - if (cur_dockee < 0) { + if (_curDockee < 0) { // clear the dropdown - list_dockee_points(-1); + listDockeePoints(-1); } else { // populate with dockee points - list_dockee_points(cur_dockee); - if (_model->getdockpoint_array()[cur_docker_point].dockee_point < 0) { + listDockeePoints(_curDockee); + if (_model->getDockpointArray()[_curDockerPoint].dockee_point < 0) { // select the dockpoint ui->dockeePointComboBox->setCurrentIndex(0); } // see if there's a dockpoint here - if (_model->getdockpoint_array()[cur_docker_point].dockee_point >= 0) { + if (_model->getDockpointArray()[_curDockerPoint].dockee_point >= 0) { // select the dockpoint ui->dockeePointComboBox->setCurrentIndex(ui->dockeePointComboBox->findText( - model_get_dock_name(Ship_info[Ships[cur_dockee].ship_info_index].model_num, - _model->getdockpoint_array()[cur_docker_point].dockee_point))); + model_get_dock_name(Ship_info[Ships[_curDockee].ship_info_index].model_num, + _model->getDockpointArray()[_curDockerPoint].dockee_point))); } } } -void ShipInitialStatusDialog::list_dockees(int dock_types) +void ShipInitialStatusDialog::listDockees(int dock_types) { // enable/disable dropdown ui->dockeeComboBox->setEnabled((dock_types >= 0)); @@ -310,14 +310,14 @@ void ShipInitialStatusDialog::list_dockees(int dock_types) // mustn't also be docked elsewhere bool docked_elsewhere = false; - for (int i = 0; i < _model->getnum_dock_points(); i++) { + for (int i = 0; i < _model->getNumDockPoints(); i++) { // don't erroneously check the same point - if (i == cur_docker_point) { + if (i == _curDockerPoint) { continue; } // see if this ship is also on a different dockpoint - if (_model->getdockpoint_array()[i].dockee_shipnum == ship) { + if (_model->getDockpointArray()[i].dockee_shipnum == ship) { docked_elsewhere = true; break; } @@ -342,7 +342,7 @@ void ShipInitialStatusDialog::list_dockees(int dock_types) } } } -void ShipInitialStatusDialog::list_dockee_points(int shipnum) +void ShipInitialStatusDialog::listDockeePoints(int shipnum) { // enable/disable dropdown ui->dockeePointComboBox->setEnabled((shipnum >= 0)); @@ -359,7 +359,7 @@ void ShipInitialStatusDialog::list_dockee_points(int shipnum) ship* other_shipp = &Ships[shipnum]; // get the required dock type(s) - int dock_type = model_get_dock_index_type(Ship_info[shipp->ship_info_index].model_num, cur_docker_point); + int dock_type = model_get_dock_index_type(Ship_info[shipp->ship_info_index].model_num, _curDockerPoint); // populate with the right kind of dockee points for (int i = 0; i < model_get_num_dock_points(Ship_info[other_shipp->ship_info_index].model_num); i++) { @@ -383,10 +383,10 @@ void ShipInitialStatusDialog::updateSubsystems() auto index = ui->subsystemList->currentIndex(); ui->subsystemList->clear(); - bool multiEdit = _model->getIfMultpleShips(); + bool multiEdit = _model->getIfMultipleShips(); bool useNewScanning = _model->getUseNewScanningBehavior(); bool toggleSet = _model->getToggleSubsystemScanning(); - bool scannable = _model->getShip_has_scannable_subsystems(); + bool scannable = _model->getShipHasScannableSubsystems(); if (!multiEdit) { ui->subsystemList->setEnabled(true); diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipInitialStatusDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipInitialStatusDialog.h index 434c6c3664f..a3347683337 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipInitialStatusDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipInitialStatusDialog.h @@ -1,5 +1,4 @@ -#ifndef SHIPINITIALSTATUSDIALOG_H -#define SHIPINITIALSTATUSDIALOG_H +#pragma once #include @@ -54,18 +53,16 @@ class ShipInitialStatusDialog : public QDialog { std::unique_ptr _model; EditorViewport* _viewport; - void updateUI(); + void updateUi(); void updateFlags(); void updateDocks(); void updateDockee(); - void list_dockees(int); - void list_dockee_points(int); + void listDockees(int); + void listDockeePoints(int); void updateSubsystems(); - int cur_docker_point = -1; - int cur_dockee = -1; - int cur_dockee_point = -1; + int _curDockerPoint = -1; + int _curDockee = -1; + int _curDockeePoint = -1; }; } // namespace fso::fred::dialogs - -#endif // !SHIPINITIALSTATUSDIALOG_H From 946d15c1e56fff592f973643978537aa612820f8 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Fri, 15 May 2026 06:33:53 -0500 Subject: [PATCH 54/65] ship special stats stylization pass (#7440) --- .../ShipSpecialStatsDialogModel.cpp | 570 +++++++++--------- .../ShipEditor/ShipSpecialStatsDialogModel.h | 116 ++-- .../ShipEditor/ShipSpecialStatsDialog.cpp | 6 +- .../ShipEditor/ShipSpecialStatsDialog.h | 4 +- 4 files changed, 341 insertions(+), 355 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipSpecialStatsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipSpecialStatsDialogModel.cpp index aa1e6a60cc4..93bc0f5c88d 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipSpecialStatsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipSpecialStatsDialogModel.cpp @@ -1,318 +1,308 @@ #include "ShipSpecialStatsDialogModel.h" -namespace fso { - namespace fred { - namespace dialogs { - ShipSpecialStatsDialogModel::ShipSpecialStatsDialogModel(QObject* parent, EditorViewport* viewport) - : AbstractDialogModel(parent, viewport) - { - initializeData(); - } - void ShipSpecialStatsDialogModel::initializeData() - { - object* objp; - num_selected_ships = 0; - m_selected_ships.resize(MAX_SHIPS); - - objp = GET_FIRST(&obj_used_list); - while (objp != END_OF_LIST(&obj_used_list)) { - if ((objp->type == OBJ_START) || (objp->type == OBJ_SHIP)) { - if (objp->flags[Object::Object_Flags::Marked]) { - m_selected_ships[num_selected_ships] = objp->instance; - num_selected_ships++; - } - } - objp = GET_NEXT(objp); - } +namespace fso::fred::dialogs { - m_ship = _editor->cur_ship; - //Special Hits - if (Ships[m_ship].special_hitpoints) { - m_hull = Ships[m_ship].special_hitpoints; - m_special_hitpoints_enabled = true; - } - else { - ship_info* sip; - sip = &Ship_info[Ships[m_ship].ship_info_index]; +ShipSpecialStatsDialogModel::ShipSpecialStatsDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ + initializeData(); +} - m_hull = static_cast(sip->max_hull_strength); - m_special_hitpoints_enabled = false; +void ShipSpecialStatsDialogModel::initializeData() +{ + object* objp; + _numSelectedShips = 0; + _selectedShips.resize(MAX_SHIPS); - if (m_hull < 1) { m_hull = 10; } - } + objp = GET_FIRST(&obj_used_list); + while (objp != END_OF_LIST(&obj_used_list)) { + if ((objp->type == OBJ_START) || (objp->type == OBJ_SHIP)) { + if (objp->flags[Object::Object_Flags::Marked]) { + _selectedShips[_numSelectedShips] = objp->instance; + _numSelectedShips++; + } + } + objp = GET_NEXT(objp); + } - if (Ships[m_ship].special_shield > 0) { - m_shields = Ships[m_ship].special_shield; - m_special_shield_enabled = true; - } - else { - // get default_table_values - ship_info* sip; - sip = &Ship_info[Ships[m_ship].ship_info_index]; + _ship = _editor->cur_ship; - m_shields = static_cast(sip->max_shield_strength); - m_special_shield_enabled = false; + if (Ships[_ship].special_hitpoints) { + _hull = Ships[_ship].special_hitpoints; + _specialHitpointsEnabled = true; + } else { + ship_info* sip; + sip = &Ship_info[Ships[_ship].ship_info_index]; - if (m_shields < 0) { - m_shields = 0; - } - } + _hull = static_cast(sip->max_hull_strength); + _specialHitpointsEnabled = false; - //Special Explosions - if (!(Ships[m_ship].use_special_explosion)) { - // get default_table_values - ship_info* sip; - sip = &Ship_info[Ships[m_ship].ship_info_index]; - - m_inner_rad = static_cast(sip->shockwave.inner_rad); - m_outer_rad = static_cast(sip->shockwave.outer_rad); - m_damage = static_cast(sip->shockwave.damage); - m_blast = static_cast(sip->shockwave.blast); - m_shockwave = static_cast(sip->explosion_propagates); - m_shock_speed = static_cast(sip->shockwave.speed); - m_deathRoll = false; - m_duration = 0; - m_special_exp = false; - - if (m_inner_rad < 1) { - m_inner_rad = 1; - } - if (m_outer_rad < 2) { - m_outer_rad = 2; - } - if (m_shock_speed < 1) { - m_shock_speed = 1; - } - } - else { - m_inner_rad = Ships[m_ship].special_exp_inner; - m_outer_rad = Ships[m_ship].special_exp_outer; - m_damage = Ships[m_ship].special_exp_damage; - m_blast = Ships[m_ship].special_exp_blast; - m_shockwave = Ships[m_ship].use_shockwave; - m_shock_speed = Ships[m_ship].special_exp_shockwave_speed; - m_deathRoll = (Ships[m_ship].special_exp_deathroll_time > 0); - m_duration = Ships[m_ship].special_exp_deathroll_time; - m_special_exp = true; - } - modelChanged(); - _modified = false; - } + if (_hull < 1) { _hull = 10; } + } - bool ShipSpecialStatsDialogModel::apply() - { - float temp_max_hull_strength; - int new_shield_strength, new_hull_strength; - if (m_special_hitpoints_enabled) { + if (Ships[_ship].special_shield > 0) { + _shields = Ships[_ship].special_shield; + _specialShieldEnabled = true; + } else { + ship_info* sip; + sip = &Ship_info[Ships[_ship].ship_info_index]; - // Don't update anything if the hull strength is invalid - if (m_hull < 1) { - return false; - } + _shields = static_cast(sip->max_shield_strength); + _specialShieldEnabled = false; - // set to update + if (_shields < 0) { + _shields = 0; + } + } - new_hull_strength = m_hull; - //Ships[m_ship_num].special_hitpoints = m_hull; + if (!(Ships[_ship].use_special_explosion)) { + ship_info* sip; + sip = &Ship_info[Ships[_ship].ship_info_index]; - } - else { - // set to update + _innerRad = static_cast(sip->shockwave.inner_rad); + _outerRad = static_cast(sip->shockwave.outer_rad); + _damage = static_cast(sip->shockwave.damage); + _blast = static_cast(sip->shockwave.blast); + _shockwave = static_cast(sip->explosion_propagates); + _shockSpeed = static_cast(sip->shockwave.speed); + _deathRoll = false; + _duration = 0; + _specialExp = false; - new_hull_strength = 0; - } + if (_innerRad < 1) { + _innerRad = 1; + } + if (_outerRad < 2) { + _outerRad = 2; + } + if (_shockSpeed < 1) { + _shockSpeed = 1; + } + } else { + _innerRad = Ships[_ship].special_exp_inner; + _outerRad = Ships[_ship].special_exp_outer; + _damage = Ships[_ship].special_exp_damage; + _blast = Ships[_ship].special_exp_blast; + _shockwave = Ships[_ship].use_shockwave; + _shockSpeed = Ships[_ship].special_exp_shockwave_speed; + _deathRoll = (Ships[_ship].special_exp_deathroll_time > 0); + _duration = Ships[_ship].special_exp_deathroll_time; + _specialExp = true; + } + modelChanged(); + _modified = false; +} - if (m_special_shield_enabled) { +bool ShipSpecialStatsDialogModel::apply() +{ + float temp_max_hull_strength; + int new_shield_strength, new_hull_strength; + if (_specialHitpointsEnabled) { + if (_hull < 1) { + return false; + } + new_hull_strength = _hull; + } else { + new_hull_strength = 0; + } - // Don't update anything if the hull strength is invalid - if (m_shields < 0) { - return false; - } + if (_specialShieldEnabled) { + if (_shields < 0) { + return false; + } + new_shield_strength = _shields; + } else { + new_shield_strength = -1; + } - // set to update + if (_innerRad > _outerRad) { + auto button = _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Invalid Entry", "Inner radius must be less than outer radius", + { DialogButton::Ok }); + if (button == DialogButton::Ok) { + return false; + } + } - new_shield_strength = m_shields; - //Ships[m_ship_num].special_shield = m_shields; + for (auto& shipp : _selectedShips) { + Ships[shipp].special_hitpoints = new_hull_strength; + Ships[shipp].special_shield = new_shield_strength; - } - else { - // set to update + if (Ships[shipp].special_hitpoints) { + temp_max_hull_strength = (float)Ships[shipp].special_hitpoints; + } else { + temp_max_hull_strength = Ship_info[Ships[shipp].ship_info_index].max_hull_strength; + } - new_shield_strength = -1; - } + Ai_info[Ships[shipp].ai_index].kamikaze_damage = std::min(1000, 200 + static_cast(temp_max_hull_strength / 4.0f)); - if (m_inner_rad > m_outer_rad) { - auto button = _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Invalid Entry", "Inner radius must be less than outer radius", + if (_specialExp) { + Ships[shipp].use_special_explosion = true; + Ships[shipp].special_exp_inner = _innerRad; + Ships[shipp].special_exp_outer = _outerRad; + Ships[shipp].special_exp_damage = _damage; + Ships[shipp].special_exp_blast = _blast; + Ships[shipp].use_shockwave = (_shockwave ? 1 : 0); + if (_shockSpeed) { + if (_shockSpeed < 1) { + _shockSpeed = 1; + _viewport->dialogProvider->showButtonDialog(DialogType::Warning, "Invalid Entry", "Shockwave speed must be defined! Setting this to 1 now", { DialogButton::Ok }); - if (button == DialogButton::Ok) { - return false; - } - } - - for (auto& shipp : m_selected_ships) { - // set the special hitpoints/shield - Ships[shipp].special_hitpoints = new_hull_strength; - Ships[shipp].special_shield = new_shield_strength; - - // calc kamikaze stuff - if (Ships[shipp].special_hitpoints) - { - temp_max_hull_strength = (float)Ships[shipp].special_hitpoints; - } - else - { - temp_max_hull_strength = Ship_info[Ships[shipp].ship_info_index].max_hull_strength; - } - - Ai_info[Ships[shipp].ai_index].kamikaze_damage = std::min(1000, 200 + static_cast(temp_max_hull_strength / 4.0f)); - - if (m_special_exp) { - // set em - Ships[shipp].use_special_explosion = true; - Ships[shipp].special_exp_inner = m_inner_rad; - Ships[shipp].special_exp_outer = m_outer_rad; - Ships[shipp].special_exp_damage = m_damage; - Ships[shipp].special_exp_blast = m_blast; - Ships[shipp].use_shockwave = (m_shockwave ? 1 : 0); - if (m_shock_speed) { - if (m_shock_speed < 1) { - m_shock_speed = 1; - _viewport->dialogProvider->showButtonDialog(DialogType::Warning, "Invalid Entry", "Shockwave speed must be defined! Setting this to 1 now", - { DialogButton::Ok }); - } - Ships[shipp].special_exp_shockwave_speed = m_shock_speed; - } - if (m_duration) { - if (m_duration < 2) { - m_duration = 2; - _viewport->dialogProvider->showButtonDialog(DialogType::Warning, "Invalid Entry", "Death roll time must be at least 2 milliseconds Setting this to 2 now", - { DialogButton::Ok }); - } - Ships[shipp].special_exp_deathroll_time = m_duration; - } - } - else { - Ships[shipp].use_special_explosion = false; - Ships[shipp].special_exp_inner = -1; - Ships[shipp].special_exp_outer = -1; - Ships[shipp].special_exp_damage = -1; - Ships[shipp].special_exp_blast = -1; - Ships[shipp].use_shockwave = false; - Ships[shipp].special_exp_shockwave_speed = -1; - Ships[shipp].special_exp_deathroll_time = 0; - } } - _editor->missionChanged(); - return true; - } - void ShipSpecialStatsDialogModel::reject() - { - } - bool ShipSpecialStatsDialogModel::getSpecialExp() const - { - return m_special_exp; - } - void ShipSpecialStatsDialogModel::setSpecialExp(const bool value) - { - modify(m_special_exp, value); - } - bool ShipSpecialStatsDialogModel::getShockwave() const - { - return m_shockwave; - } - void ShipSpecialStatsDialogModel::setShockwave(const bool value) - { - modify(m_shockwave, value); - } - bool ShipSpecialStatsDialogModel::getDeathRoll() const - { - return m_deathRoll; - } - void ShipSpecialStatsDialogModel::setDeathRoll(const bool value) - { - modify(m_deathRoll, value); - } - int ShipSpecialStatsDialogModel::getDamage() const - { - return m_damage; - } - void ShipSpecialStatsDialogModel::setDamage(const int value) - { - modify(m_damage, value); - } - int ShipSpecialStatsDialogModel::getBlast() const - { - return m_blast; - } - void ShipSpecialStatsDialogModel::setBlast(const int value) - { - modify(m_blast, value); - } - int ShipSpecialStatsDialogModel::getInnerRadius() const - { - return m_inner_rad; - } - void ShipSpecialStatsDialogModel::setInnerRadius(const int value) - { - modify(m_inner_rad, value); + Ships[shipp].special_exp_shockwave_speed = _shockSpeed; } - int ShipSpecialStatsDialogModel::getOuterRadius() const - { - return m_outer_rad; - } - void ShipSpecialStatsDialogModel::setOuterRadius(const int value) - { - modify(m_outer_rad, value); - } - int ShipSpecialStatsDialogModel::getShockwaveSpeed() const - { - return m_shock_speed; - } - void ShipSpecialStatsDialogModel::setShockwaveSpeed(const int value) - { - modify(m_shock_speed, value); - } - int ShipSpecialStatsDialogModel::getRollDuration() const - { - return m_duration; - } - void ShipSpecialStatsDialogModel::setRollDuration(const int value) - { - modify(m_duration, value); - } - bool ShipSpecialStatsDialogModel::getSpecialShield() const - { - return m_special_shield_enabled; - } - void ShipSpecialStatsDialogModel::setSpecialShield(const bool value) - { - modify(m_special_shield_enabled, value); - } - int ShipSpecialStatsDialogModel::getShield() const - { - return m_shields; - } - void ShipSpecialStatsDialogModel::setShield(const int value) - { - modify(m_shields, value); - } - bool ShipSpecialStatsDialogModel::getSpecialHull() const - { - return m_special_hitpoints_enabled; - } - void ShipSpecialStatsDialogModel::setSpecialHull(const bool value) - { - modify(m_special_hitpoints_enabled, value); - } - int ShipSpecialStatsDialogModel::getHull() const - { - return m_hull; - } - void ShipSpecialStatsDialogModel::setHull(const int value) - { - modify(m_hull, value); + if (_duration) { + if (_duration < 2) { + _duration = 2; + _viewport->dialogProvider->showButtonDialog(DialogType::Warning, "Invalid Entry", "Death roll time must be at least 2 milliseconds Setting this to 2 now", + { DialogButton::Ok }); + } + Ships[shipp].special_exp_deathroll_time = _duration; } + } else { + Ships[shipp].use_special_explosion = false; + Ships[shipp].special_exp_inner = -1; + Ships[shipp].special_exp_outer = -1; + Ships[shipp].special_exp_damage = -1; + Ships[shipp].special_exp_blast = -1; + Ships[shipp].use_shockwave = false; + Ships[shipp].special_exp_shockwave_speed = -1; + Ships[shipp].special_exp_deathroll_time = 0; } - } -} \ No newline at end of file + _editor->missionChanged(); + return true; +} + +void ShipSpecialStatsDialogModel::reject() {} + +bool ShipSpecialStatsDialogModel::getSpecialExp() const +{ + return _specialExp; +} + +void ShipSpecialStatsDialogModel::setSpecialExp(bool specialExp) +{ + modify(_specialExp, specialExp); +} + +bool ShipSpecialStatsDialogModel::getShockwave() const +{ + return _shockwave; +} + +void ShipSpecialStatsDialogModel::setShockwave(bool shockwave) +{ + modify(_shockwave, shockwave); +} + +bool ShipSpecialStatsDialogModel::getDeathRoll() const +{ + return _deathRoll; +} + +void ShipSpecialStatsDialogModel::setDeathRoll(bool deathRoll) +{ + modify(_deathRoll, deathRoll); +} + +int ShipSpecialStatsDialogModel::getDamage() const +{ + return _damage; +} + +void ShipSpecialStatsDialogModel::setDamage(int damage) +{ + modify(_damage, damage); +} + +int ShipSpecialStatsDialogModel::getBlast() const +{ + return _blast; +} + +void ShipSpecialStatsDialogModel::setBlast(int blast) +{ + modify(_blast, blast); +} + +int ShipSpecialStatsDialogModel::getInnerRadius() const +{ + return _innerRad; +} + +void ShipSpecialStatsDialogModel::setInnerRadius(int innerRadius) +{ + modify(_innerRad, innerRadius); +} + +int ShipSpecialStatsDialogModel::getOuterRadius() const +{ + return _outerRad; +} + +void ShipSpecialStatsDialogModel::setOuterRadius(int outerRadius) +{ + modify(_outerRad, outerRadius); +} + +int ShipSpecialStatsDialogModel::getShockwaveSpeed() const +{ + return _shockSpeed; +} + +void ShipSpecialStatsDialogModel::setShockwaveSpeed(int speed) +{ + modify(_shockSpeed, speed); +} + +int ShipSpecialStatsDialogModel::getRollDuration() const +{ + return _duration; +} + +void ShipSpecialStatsDialogModel::setRollDuration(int duration) +{ + modify(_duration, duration); +} + +bool ShipSpecialStatsDialogModel::getSpecialShield() const +{ + return _specialShieldEnabled; +} + +void ShipSpecialStatsDialogModel::setSpecialShield(bool specialShield) +{ + modify(_specialShieldEnabled, specialShield); +} + +int ShipSpecialStatsDialogModel::getShield() const +{ + return _shields; +} + +void ShipSpecialStatsDialogModel::setShield(int shield) +{ + modify(_shields, shield); +} + +bool ShipSpecialStatsDialogModel::getSpecialHull() const +{ + return _specialHitpointsEnabled; +} + +void ShipSpecialStatsDialogModel::setSpecialHull(bool specialHull) +{ + modify(_specialHitpointsEnabled, specialHull); +} + +int ShipSpecialStatsDialogModel::getHull() const +{ + return _hull; +} + +void ShipSpecialStatsDialogModel::setHull(int hull) +{ + modify(_hull, hull); +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipSpecialStatsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipSpecialStatsDialogModel.h index 0656c4f0783..fdef763b75c 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipSpecialStatsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipSpecialStatsDialogModel.h @@ -2,69 +2,65 @@ #include "../AbstractDialogModel.h" -namespace fso { - namespace fred { - namespace dialogs { - class ShipSpecialStatsDialogModel : public AbstractDialogModel { - private: - int m_ship; +namespace fso::fred::dialogs { - int num_selected_ships; - SCP_vector m_selected_ships; - //Special Explosion - bool m_special_exp; - bool m_shockwave; - bool m_deathRoll; - int m_inner_rad; - int m_outer_rad; - int m_damage; - int m_shock_speed; - int m_duration; - int m_blast; +class ShipSpecialStatsDialogModel : public AbstractDialogModel { + Q_OBJECT + public: + ShipSpecialStatsDialogModel(QObject* parent, EditorViewport* viewport); - //Special Hits - bool m_special_hitpoints_enabled; - bool m_special_shield_enabled; - int m_shields; - int m_hull; + bool apply() override; + void reject() override; + bool getSpecialExp() const; + void setSpecialExp(bool specialExp); + bool getShockwave() const; + void setShockwave(bool shockwave); + bool getDeathRoll() const; + void setDeathRoll(bool deathRoll); + int getDamage() const; + void setDamage(int damage); + int getBlast() const; + void setBlast(int blast); + int getInnerRadius() const; + void setInnerRadius(int innerRadius); + int getOuterRadius() const; + void setOuterRadius(int outerRadius); + int getShockwaveSpeed() const; + void setShockwaveSpeed(int speed); + int getRollDuration() const; + void setRollDuration(int duration); - public: - ShipSpecialStatsDialogModel(QObject* parent, EditorViewport* viewport); - void initializeData(); - bool apply() override; - void reject() override; + bool getSpecialShield() const; + void setSpecialShield(bool specialShield); + int getShield() const; + void setShield(int shield); + bool getSpecialHull() const; + void setSpecialHull(bool specialHull); + int getHull() const; + void setHull(int hull); - //Exp Get/Setters - bool getSpecialExp() const; - void setSpecialExp(const bool); - bool getShockwave() const; - void setShockwave(const bool); - bool getDeathRoll() const; - void setDeathRoll(const bool); - int getDamage() const; - void setDamage(const int); - int getBlast() const; - void setBlast(const int); - int getInnerRadius() const; - void setInnerRadius(const int); - int getOuterRadius() const; - void setOuterRadius(const int); - int getShockwaveSpeed() const; - void setShockwaveSpeed(const int); - int getRollDuration() const; - void setRollDuration(const int); + private: // NOLINT(readability-redundant-access-specifiers) + void initializeData(); - //Hit Get/Setters - bool getSpecialShield() const; - void setSpecialShield(const bool); - int getShield() const; - void setShield(const int); - bool getSpecialHull() const; - void setSpecialHull(const bool); - int getHull() const; - void setHull(const int); - }; - } - } -} \ No newline at end of file + int _ship; + int _numSelectedShips; + SCP_vector _selectedShips; + + bool _specialExp; + bool _shockwave; + bool _deathRoll; + int _innerRad; + int _outerRad; + int _damage; + int _shockSpeed; + int _duration; + int _blast; + + bool _specialHitpointsEnabled; + bool _specialShieldEnabled; + int _shields; + int _hull; +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipSpecialStatsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipSpecialStatsDialog.cpp index 9367795c23d..e905c4e5397 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipSpecialStatsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipSpecialStatsDialog.cpp @@ -15,9 +15,9 @@ ShipSpecialStatsDialog::ShipSpecialStatsDialog(QWidget* parent, EditorViewport* { ui->setupUi(this); - connect(_model.get(), &AbstractDialogModel::modelChanged, this, [this]() { updateUI(false); }); + connect(_model.get(), &AbstractDialogModel::modelChanged, this, [this]() { updateUi(false); }); - updateUI(true); + updateUi(true); // Resize the dialog to the minimum size resize(QDialog::sizeHint()); @@ -109,7 +109,7 @@ void ShipSpecialStatsDialog::on_explodeCheckBox_toggled(bool value) { _model->setSpecialExp(value); } -void ShipSpecialStatsDialog::updateUI(bool first) +void ShipSpecialStatsDialog::updateUi(bool first) { util::SignalBlockers blockers(this); if (first) { diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipSpecialStatsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipSpecialStatsDialog.h index 38615dca89f..61219674a8d 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipSpecialStatsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipSpecialStatsDialog.h @@ -42,11 +42,11 @@ class ShipSpecialStatsDialog : public QDialog { void on_specialHullCheckbox_toggled(bool); void on_hullSpinBox_valueChanged(int); - private:// NOLINT(readability-redundant-access-specifiers) + private: // NOLINT(readability-redundant-access-specifiers) std::unique_ptr ui; std::unique_ptr _model; EditorViewport* _viewport; - void updateUI(bool first = false); + void updateUi(bool first = false); }; } // namespace fso::fred::dialogs \ No newline at end of file From 0018f55c55c4321c1c9813b962d3518acad83d1e Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Fri, 15 May 2026 08:46:28 -0400 Subject: [PATCH 55/65] Fix edge case in switching from None AA to MSAA (#7454) If you set AA level to None, save, then close the game, then re-enter the game and switch the AA mode to an MSAA mode, then the screen will just be black until the game is restarted. This does not happen when switching from None to FXAA because FXAA works because its post-pass uses `Scene_luminance_texture` and `Scene_ldr_texture`, which are always created as part of standard scene setup. There are two ways to fix this, either require AA mode to require a game restart (though that may complicate the lab) or allow resource allocation for SMAA always. Honestly I like the idea of keeping the lower-end machines optimized and not allocating for SMAA but simply running it always but for this PR have just allowed it always run. Happy to discuss whichever option folks think is best! --- code/graphics/opengl/gropenglpostprocessing.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/code/graphics/opengl/gropenglpostprocessing.cpp b/code/graphics/opengl/gropenglpostprocessing.cpp index bd4c8264fd5..d3b6bb607be 100644 --- a/code/graphics/opengl/gropenglpostprocessing.cpp +++ b/code/graphics/opengl/gropenglpostprocessing.cpp @@ -1111,9 +1111,11 @@ static bool opengl_post_init_framebuffer() opengl_setup_bloom_textures(); - if (Gr_aa_mode != AntiAliasMode::None) { + // Always set up SMAA resources so the user can switch to an SMAA preset + // at runtime even when starting with a non-SMAA AA mode, such as None. + //if (Gr_aa_mode != AntiAliasMode::None) { setup_smaa_resources(); - } + //} GL_state.BindFrameBuffer(0); From 1b8a79c64eaacaa762cb8cf278d2e890a2e3ac98 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Fri, 15 May 2026 21:00:30 -0400 Subject: [PATCH 56/65] two fixes for issue 6793 1. more robust handling of missing fireball graphics 2. fix handling of error dialogs on Linux so that the engine doesn't silently continue on an error! Fixes #6793. --- code/fireball/fireballs.cpp | 29 +++++++++++++------------ code/osapi/dialogs.cpp | 43 +++++++++++++++++++++++++------------ 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/code/fireball/fireballs.cpp b/code/fireball/fireballs.cpp index c786b57dcb2..43962ac1dff 100644 --- a/code/fireball/fireballs.cpp +++ b/code/fireball/fireballs.cpp @@ -920,19 +920,7 @@ int fireball_create(vec3d *pos, int fireball_type, int render_type, int parent_o return -1; } - - if (!Unused_fireball_indices.empty()) { - n = Unused_fireball_indices.back(); - Unused_fireball_indices.pop_back(); - } - else { - n = static_cast(Fireballs.size()); - Fireballs.emplace_back(); - } - - fireball* new_fireball = &Fireballs[n]; - - // get an lod to use + // get an lod to use fb_lod = fireball_get_lod(pos, fd, size); // change lod if low res is desired @@ -947,8 +935,21 @@ int fireball_create(vec3d *pos, int fireball_type, int render_type, int parent_o } fl = &fd->lod[fb_lod]; - new_fireball->lod = (char)fb_lod; + // don't create a fireball without usable graphics + if (fl->bitmap_id < 0 || fl->fps <= 0 || fl->num_frames <= 0) { + return -1; + } + + if (!Unused_fireball_indices.empty()) { + n = Unused_fireball_indices.back(); + Unused_fireball_indices.pop_back(); + } else { + n = sz2i(Fireballs.size()); + Fireballs.emplace_back(); + } + auto new_fireball = &Fireballs[n]; + new_fireball->lod = (char)fb_lod; new_fireball->flags = extra_flags; new_fireball->warp_open_sound_index = warp_open_sound; new_fireball->warp_close_sound_index = warp_close_sound; diff --git a/code/osapi/dialogs.cpp b/code/osapi/dialogs.cpp index 287880b94d6..308146c4dff 100644 --- a/code/osapi/dialogs.cpp +++ b/code/osapi/dialogs.cpp @@ -238,22 +238,23 @@ namespace os gr_activate(0); - int buttonId; + int buttonId = -1; // if dialog is silently suppressed if (SDL_ShowMessageBox(&boxData, &buttonId) < 0) { // Call failed - buttonId = 1; // No action + buttonId = 1; } switch (buttonId) { - case 2: + case 2: // Exit abort(); - case 0: + case 0: // Debug Int3(); break; + case 1: // Continue default: break; } @@ -283,6 +284,12 @@ namespace os { mprintf(("\n%s\n", text)); + // also output to stderr so the message is visible if the dialog is suppressed + // (e.g., the SDL message box is dismissed silently by the window manager when the + // game is fullscreen on Linux) + fprintf(stderr, "\n%s\n", text); + fflush(stderr); + if (running_unittests) { throw ErrorException(text); } @@ -322,21 +329,22 @@ namespace os gr_activate(0); - int buttonId; + int buttonId = -1; // if dialog is silently suppressed if (SDL_ShowMessageBox(&boxData, &buttonId) < 0) { // Call failed - abort(); + buttonId = 1; } switch (buttonId) { - case 1: - abort(); - - default: + case 0: // Debug Int3(); break; + + case 1: // Exit + default: + abort(); } gr_activate(1); } @@ -349,6 +357,12 @@ namespace os // output to the debug log before anything else (so that we have a complete record) mprintf(("WARNING: \"%s\" at %s:%d\n", text.c_str(), filename, line)); + // also output to stderr so the message is visible if the dialog is suppressed + // (e.g., the SDL message box is dismissed silently by the window manager when the + // game is fullscreen on Linux) + fprintf(stderr, "WARNING: \"%s\" at %s:%d\n", text.c_str(), filename, line); + fflush(stderr); + if (running_unittests) { throw WarningException(text); } @@ -390,22 +404,23 @@ namespace os gr_activate(0); - int buttonId; + int buttonId = -1; // if dialog is silently suppressed if (SDL_ShowMessageBox(&boxData, &buttonId) < 0) { // Call failed - buttonId = 1; // No action + buttonId = 1; } switch (buttonId) { - case 2: + case 2: // Exit abort(); - case 0: + case 0: // Debug Int3(); break; + case 1: // Continue default: break; } From b4c5c7b781b77370e42ec633a1202e4a2405ba83 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 16 May 2026 09:41:48 -0500 Subject: [PATCH 57/65] Qtfred ship weapons dialog (#7453) * Release Build Safety * Move to using standard models. * Avoid using .indexes() to fix heap coruption * More work on selection function * major cleanup and refactor * enable drag and drop * multi edit support * cleanup * fix build issue on linux * revert non-weapons dialog changes * clang * clang * update help doc * better model/dialog code split * further cleanup * static * keep primary and secondary banks aligned --------- Co-authored-by: The Force <2040992+TheForce172@users.noreply.github.com> --- .../doc/dialogs/ShipWeaponsDialog.html | 111 ++- qtfred/source_groups.cmake | 4 - .../ShipEditor/ShipWeaponsDialogModel.cpp | 499 ++++++++---- .../ShipEditor/ShipWeaponsDialogModel.h | 191 ++--- .../src/ui/dialogs/ShipEditor/BankModel.cpp | 404 ---------- qtfred/src/ui/dialogs/ShipEditor/BankModel.h | 94 --- .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 748 ++++++++++++++---- .../ui/dialogs/ShipEditor/ShipWeaponsDialog.h | 87 +- qtfred/src/ui/widgets/bankTree.cpp | 199 +++-- qtfred/src/ui/widgets/bankTree.h | 28 +- qtfred/src/ui/widgets/weaponList.cpp | 102 --- qtfred/src/ui/widgets/weaponList.h | 38 - qtfred/ui/ShipWeaponsDialog.ui | 436 ++++++---- 13 files changed, 1606 insertions(+), 1335 deletions(-) delete mode 100644 qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp delete mode 100644 qtfred/src/ui/dialogs/ShipEditor/BankModel.h delete mode 100644 qtfred/src/ui/widgets/weaponList.cpp delete mode 100644 qtfred/src/ui/widgets/weaponList.h diff --git a/qtfred/help-src/doc/dialogs/ShipWeaponsDialog.html b/qtfred/help-src/doc/dialogs/ShipWeaponsDialog.html index 32182e89bf1..9ba65772726 100644 --- a/qtfred/help-src/doc/dialogs/ShipWeaponsDialog.html +++ b/qtfred/help-src/doc/dialogs/ShipWeaponsDialog.html @@ -9,23 +9,106 @@

Ship Weapons

Accessed via the Weapons button in the Ship Editor.

-

Assigns weapons to the ship's banks and turrets for this mission instance, -overriding the ship class defaults.

+

Assigns weapons, starting ammo, and per-turret AI class to the ship's banks and +turrets for this mission instance, overriding the ship class defaults.

+

All changes are buffered. Nothing is written to the mission until you click +OK; Cancel and closing the dialog discard +everything.

-

Mode

-

Select Primary, Secondary, or -Turret to switch which set of banks is shown. The -Tertiary option is not currently implemented.

+

Layout

+

Weapons are split across two tabs:

+
    +
  • Primary - primary weapon banks for the pilot and any + turret with a primary slot.
  • +
  • Secondary - secondary banks for the pilot and any + turret with a secondary slot.
  • +
+

The dialog opens on whichever tab has banks first; if a ship has only secondaries, +the Secondary tab is shown.

+

Each tab contains:

+
    +
  • A Weapons list on the left showing weapons valid for this + bank type and ship size. Weapons that are not in the ship class's allowed + loadout are shown in gray with a tooltip explaining why; you can still assign + them in the editor.
  • +
  • A Banks tree on the right showing Pilot and each + turret as collapsible groups, with their bank slots and current ammo + underneath.
  • +
  • An AI selector beneath the tree for changing a turret's AI + class.
  • +

Assigning weapons

-

The right panel lists the banks or turrets for the selected mode. Select the bank -or turret you want to change. Then select the desired weapon from the left panel and -click Set Selected to assign it.

-

Where a weapon uses ammo, the ammo count appears as a second column next to the -weapon name. Click the value to edit the starting ammo for that bank directly.

+

Several ways:

+
    +
  • Drag from the weapons list onto a bank slot. The slot is set + to the dropped weapon, and ammo capacity is recalculated.
  • +
  • Drag from one bank slot to another. By default the weapon is + moved (the source slot becomes empty). Hold Ctrl while dropping to + copy instead.
  • +
  • Select one or more bank slots, pick a weapon from the list, and + click Set Selected. The chosen weapon is assigned to every selected + slot at once. Set Selected is enabled only when slot rows + (not turret-group rows) are selected.
  • +
+

Selection within the Banks tree is restricted to one row type at a time: clicking +a turret-group row clears any selected slot rows, and vice versa. This keeps the +AI controls and Set Selected controls clearly applicable to whatever is currently +highlighted.

-

Turret AI class

-

When in Turret mode, a dropdown and button at the bottom allow the AI class of the -selected turret to be changed independently of the ship's overall AI class.

+

Editing ammo

+

Where a weapon uses ammo, the slot's row has a second column showing +current/max. Click the value to edit; a spinbox appears, clamped to the +maximum capacity for that weapon and bank. The display refreshes as soon as the +edit is committed.

+ +

View Table

+

With a weapon highlighted in the list, the View Table button +opens a viewer for that weapon's weapons.tbl entry (including any +modular -wep.tbm overrides). Useful for quickly checking damage, +ammo capacity, or flags without leaving the dialog.

+ +

AI class

+

Each turret has its own AI class, independent of the ship's overall AI class. +Select one or more turret-group rows (the parent rows, not the individual +slots), pick an AI class from the combo, and click Change AI. The +new value is applied to every selected turret on every marked ship.

+

The combo reflects the selection:

+
    +
  • All selected turrets share the same AI - the combo shows that value.
  • +
  • Selected turrets disagree (within one ship, or across ships in multi-edit) + - the combo is blank; pick a value and click Change AI to set them + all to that value.
  • +
+

The Pilot row is shown for context but its AI is owned by the +Ship Editor and cannot be changed here. When +only the Pilot row is selected the AI controls are disabled.

+ +

Editing multiple ships at once

+

If multiple ships of the same class are marked when the dialog is opened, +edits apply to all of them. The dialog reconciles per-slot state from the marked +ships at open time:

+
    +
  • Slots where every marked ship has the same weapon and ammo are shown + normally.
  • +
  • Slots where ships disagree on the weapon show + CONFLICT. Leaving the slot as CONFLICT preserves each + ship's existing weapon on OK; assigning a weapon overwrites all of them.
  • +
  • Slots where ships agree on the weapon but disagree on ammo show + --/max. Leaving it preserves each ship's existing ammo; entering + a value applies that value to all marked ships.
  • +
  • Turret AI is reconciled similarly: matching AIs show in the combo, mixed + AIs show as (Mixed AI) in the turret label and leave the combo + blank.
  • +
+

Multi-edit is only available when every marked ship belongs to the same ship +class. The Ship Editor's Weapons button is disabled +otherwise.

+ +

OK and Cancel

+

OK commits every change (weapons, ammo, and AI) to every +relevant ship and closes the dialog. Cancel, closing the dialog +window, or pressing Esc discards every change; no mission data +is touched. If unsaved changes exist when closing, you are prompted to keep them.

diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 6b3704bfa26..95331e243a3 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -261,8 +261,6 @@ add_file_folder("Source/UI/Dialogs/ShipEditor" src/ui/dialogs/ShipEditor/ShipTextureReplacementDialog.cpp src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h - src/ui/dialogs/ShipEditor/BankModel.cpp - src/ui/dialogs/ShipEditor/BankModel.h src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.h src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.cpp src/ui/dialogs/ShipEditor/ShipAltShipClass.h @@ -330,8 +328,6 @@ add_file_folder("Source/UI/Widgets" src/ui/widgets/ShipFlagCheckbox.cpp src/ui/widgets/SimpleListSelectDialog.cpp src/ui/widgets/SimpleListSelectDialog.h - src/ui/widgets/weaponList.cpp - src/ui/widgets/weaponList.h src/ui/widgets/MusicComboWidget.cpp src/ui/widgets/MusicComboWidget.h ) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp index cfaff25571b..43dfc52cec3 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -1,22 +1,82 @@ #include "ShipWeaponsDialogModel.h" + +#include + namespace fso::fred { -Banks::Banks(SCP_string _name, int aiIndex, int _ship, int multiedit, ship_subsys* _subsys) - : m_isMultiEdit(multiedit), name(std::move(_name)), subsys(_subsys), initalAI(aiIndex), ship(_ship) +namespace { +// Compare one slot of the "tracking" Bank (from the first ship's view) against the corresponding +// slot of a subsequent ship. Mark the bank as CONFLICT (-2) where they disagree, but never +// clobber an already-CONFLICT marker. +void reconcileSlot(Bank* bank, int otherWeaponId, int otherAmmoPct) { - aiClass = aiIndex; + if (bank->getWeaponId() == -2) { + return; + } + if (bank->getWeaponId() != otherWeaponId) { + bank->setWeapon(-2); + return; + } + if (bank->getAmmo() == -2 || bank->getMaxAmmo() <= 0) { + return; + } + const int otherAmmo = fl2ir(otherAmmoPct * bank->getMaxAmmo() / 100.0f); + if (bank->getAmmo() != otherAmmo) { + bank->setAmmo(-2); + } } -void Banks::add(Bank* bank) + +Banks* findBanksByName(const SCP_vector>& banks, const SCP_string& name) { - banks.push_back(bank); + for (const auto& b : banks) { + if (b->getName() == name) { + return b.get(); + } + } + return nullptr; } -Bank* Banks::getByBankId(const int id) + +ship_subsys* findTurretByName(int inst, const SCP_string& name) { - for (auto bank : banks) { - if (id == bank->getWeaponId()) - return bank; + ship_subsys* ssl = &Ships[inst].subsys_list; + for (ship_subsys* pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { + if (pss->system_info->type == SUBSYSTEM_TURRET && + SCP_string(pss->system_info->subobj_name) == name) { + return pss; + } } return nullptr; } + +void saveSlotsTo(ship_weapon& target, const SCP_vector& bankList, bool isPrimary) +{ + int* weapons = isPrimary ? target.primary_bank_weapons : target.secondary_bank_weapons; + int* ammo = isPrimary ? target.primary_bank_ammo : target.secondary_bank_ammo; + for (Bank* bank : bankList) { + if (bank->getWeaponId() == -2) { + continue; // weapon CONFLICT — preserve per-ship weapon + } + weapons[bank->getBankId()] = bank->getWeaponId(); + if (bank->getAmmo() != -2) { + ammo[bank->getBankId()] = bank->getMaxAmmo() ? fl2ir(bank->getAmmo() * 100.0f / bank->getMaxAmmo()) : 0; + } + // else: ammo CONFLICT — preserve per-ship ammo + } +} +} // namespace + +Banks::Banks(SCP_string _name, int aiIndex, int _ship, int _id, ship_subsys* _subsys) + : name(std::move(_name)), subsys(_subsys), currentAi(aiIndex), ship(_ship), id(_id) +{ +} +Banks::~Banks() = default; +int Banks::getId() const +{ + return id; +} +void Banks::add(std::unique_ptr bank) +{ + banks.push_back(std::move(bank)); +} SCP_string Banks::getName() const { return name; @@ -35,42 +95,34 @@ bool Banks::empty() const } SCP_vector Banks::getBanks() const { - return banks; + SCP_vector result; + result.reserve(banks.size()); + for (const auto& b : banks) { + result.push_back(b.get()); + } + return result; } int Banks::getAiClass() const { - if (name == "Pilot") { - return Ships[ship].weapons.ai_class; - } else { - return subsys->weapons.ai_class; - } + return currentAi; } void Banks::setAiClass(int newClass) { - if (m_isMultiEdit) { - object* ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { - int inst = ptr->instance; - if (name == "Pilot") { - Ships[inst].ai_index = newClass; - } else { - subsys->weapons.ai_class = newClass; - } - } - ptr = GET_NEXT(ptr); - } - } else { - if (name == "Pilot") { - Ships[ship].weapons.ai_class = newClass; - } else { - subsys->weapons.ai_class = newClass; - } + currentAi = newClass; + aiClassDirty = true; +} +void Banks::reconcileAiClass(int otherAi) +{ + if (currentAi == -1) { + return; + } + if (currentAi != otherAi) { + currentAi = -1; } } -int Banks::getInitalAI() const +bool Banks::isAiClassDirty() const { - return initalAI; + return aiClassDirty; } Bank::Bank(const int _weaponId, const int _bankId, const int _ammoMax, const int _ammo, Banks* _parent) { @@ -99,19 +151,27 @@ int Bank::getMaxAmmo() const void Bank::setWeapon(const int id) { weaponId = id; + if (id < 0) { + // "None" or CONFLICT placeholder... no weapon assigned, no ammo capacity. + ammoMax = 0; + ammo = 0; + return; + } + const int shipClass = Ships[parent->getShip()].ship_info_index; if (Weapon_info[id].subtype == WP_LASER || Weapon_info[id].subtype == WP_BEAM) { if (parent->getName() == "Pilot") { - ammoMax = get_max_ammo_count_for_primary_bank(parent->getShip(), bankId, id); + ammoMax = get_max_ammo_count_for_primary_bank(shipClass, bankId, id); } else { ammoMax = get_max_ammo_count_for_primary_turret_bank(&parent->getSubsys()->weapons, bankId, id); } } else { if (parent->getName() == "Pilot") { - ammoMax = get_max_ammo_count_for_bank(parent->getShip(), bankId, id); + ammoMax = get_max_ammo_count_for_bank(shipClass, bankId, id); } else { ammoMax = get_max_ammo_count_for_turret_bank(&parent->getSubsys()->weapons, bankId, id); } } + ammo = ammoMax; } void Bank::setAmmo(const int newAmmo) { @@ -123,6 +183,23 @@ ShipWeaponsDialogModel::ShipWeaponsDialogModel(QObject* parent, EditorViewport* { initializeData(isMultiEdit); } +ShipWeaponsDialogModel::~ShipWeaponsDialogModel() = default; +bool ShipWeaponsDialogModel::selectedShipsShareClass() +{ + int sharedClass = -1; + for (object* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if ((ptr->type != OBJ_SHIP && ptr->type != OBJ_START) || !ptr->flags[Object::Object_Flags::Marked]) { + continue; + } + const int cls = Ships[ptr->instance].ship_info_index; + if (sharedClass < 0) { + sharedClass = cls; + } else if (cls != sharedClass) { + return false; + } + } + return true; +} void ShipWeaponsDialogModel::initializeData(bool isMultiEdit) { m_isMultiEdit = isMultiEdit; @@ -130,8 +207,14 @@ void ShipWeaponsDialogModel::initializeData(bool isMultiEdit) SecondaryBanks.clear(); m_ship = _editor->cur_ship; - if (m_ship == -1) + if (m_ship == -1) { + Assertion(_editor->currentObject >= 0 && _editor->currentObject < MAX_OBJECTS, // NOLINT(readability-simplify-boolean-expr) + "ShipWeaponsDialog opened with no valid current ship and an out-of-range currentObject (%d)", + _editor->currentObject); m_ship = Objects[_editor->currentObject].instance; + } + Assertion(m_ship >= 0 && m_ship < MAX_SHIPS, // NOLINT(readability-simplify-boolean-expr) + "ShipWeaponsDialog resolved to invalid ship index %d", m_ship); if (m_isMultiEdit) { object* ptr = GET_FIRST(&obj_used_list); @@ -143,7 +226,6 @@ void ShipWeaponsDialogModel::initializeData(bool isMultiEdit) big = false; initPrimary(inst, first); initSecondary(inst, first); - // initTertiary(inst, first); first = false; } ptr = GET_NEXT(ptr); @@ -159,66 +241,72 @@ void ShipWeaponsDialogModel::initializeData(bool isMultiEdit) void ShipWeaponsDialogModel::initPrimary(int inst, bool first) { - auto pilotBank = new Banks("Pilot", Ships[inst].weapons.ai_class, inst, m_isMultiEdit); if (first) { + int id = 0; + auto pilotBank = std::make_unique("Pilot", Ships[inst].weapons.ai_class, inst, id); + id++; auto pilot = Ships[inst].weapons; - for (int i = 0; i < MAX_SHIP_PRIMARY_BANKS; i++) { - if (pilot.primary_bank_weapons[i] >= 0) { - const int maxAmmo = - get_max_ammo_count_for_primary_bank(Ships[inst].ship_info_index, i, pilot.primary_bank_weapons[i]); - const int ammo = fl2ir(pilot.primary_bank_ammo[i] * maxAmmo / 100.0f); - pilotBank->add(new Bank(pilot.primary_bank_weapons[i], i, maxAmmo, ammo, pilotBank)); + const int shipClass = Ships[inst].ship_info_index; + const int numPilotBanks = Ship_info[shipClass].num_primary_banks; + for (int i = 0; i < numPilotBanks; i++) { + const int weaponId = pilot.primary_bank_weapons[i]; + int maxAmmo = 0; + int ammo = 0; + if (weaponId >= 0) { + maxAmmo = get_max_ammo_count_for_primary_bank(shipClass, i, weaponId); + ammo = fl2ir(pilot.primary_bank_ammo[i] * maxAmmo / 100.0f); } + pilotBank->add(std::make_unique(weaponId, i, maxAmmo, ammo, pilotBank.get())); + } + if (!pilotBank->empty()) { + PrimaryBanks.push_back(std::move(pilotBank)); } - PrimaryBanks.push_back(pilotBank); ship_subsys* ssl = &Ships[inst].subsys_list; - ship_subsys* pss; - for (pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { + for (ship_subsys* pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { model_subsystem* psub = pss->system_info; if (psub->type == SUBSYSTEM_TURRET) { - auto turretBank = new Banks(psub->subobj_name, pss->weapons.ai_class, inst, m_isMultiEdit, pss); - for (int i = 0; i < MAX_SHIP_PRIMARY_BANKS; i++) { - if (pss->weapons.primary_bank_weapons[i] >= 0) { - const int maxAmmo = get_max_ammo_count_for_primary_turret_bank(&pss->weapons, - i, - pss->weapons.primary_bank_weapons[i]); - const int ammo = fl2ir(pss->weapons.primary_bank_ammo[i] * maxAmmo / 100.0f); - turretBank->add(new Bank(pss->weapons.primary_bank_weapons[i], i, maxAmmo, ammo, turretBank)); + auto turretBank = std::make_unique(psub->subobj_name, pss->weapons.ai_class, inst, id, pss); + const int numTurretBanks = pss->weapons.num_primary_banks; + for (int i = 0; i < numTurretBanks; i++) { + const int weaponId = pss->weapons.primary_bank_weapons[i]; + int maxAmmo = 0; + int ammo = 0; + if (weaponId >= 0) { + maxAmmo = get_max_ammo_count_for_primary_turret_bank(&pss->weapons, i, weaponId); + ammo = fl2ir(pss->weapons.primary_bank_ammo[i] * maxAmmo / 100.0f); } + turretBank->add(std::make_unique(weaponId, i, maxAmmo, ammo, turretBank.get())); } if (!turretBank->empty()) { - PrimaryBanks.push_back(turretBank); - } else { - delete turretBank; + PrimaryBanks.push_back(std::move(turretBank)); + id++; } } } } else { - for (int i = 0; i < static_cast(PrimaryBanks[0]->getBanks().size()); i++) { - if (PrimaryBanks[0]->getBanks()[i]->getWeaponId() != Ships[inst].weapons.primary_bank_weapons[i]) { - PrimaryBanks[0]->getBanks()[i]->setWeapon(-2); - } - if (PrimaryBanks[0]->getBanks()[i]->getAmmo() != Ships[inst].weapons.primary_bank_ammo[i]) { - PrimaryBanks[0]->getBanks()[i]->setAmmo(-2); + // Subsequent ship: reconcile each slot against the tracking Banks built from the first ship. + if (Banks* tracking = findBanksByName(PrimaryBanks, "Pilot")) { + tracking->reconcileAiClass(Ships[inst].weapons.ai_class); + const auto bankList = tracking->getBanks(); + for (size_t i = 0; i < bankList.size(); i++) { + reconcileSlot(bankList[i], Ships[inst].weapons.primary_bank_weapons[i], + Ships[inst].weapons.primary_bank_ammo[i]); } } ship_subsys* ssl = &Ships[inst].subsys_list; - ship_subsys* pss; - for (pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { - model_subsystem* psub = pss->system_info; - if (psub->type == SUBSYSTEM_TURRET) { - for (auto banks : PrimaryBanks) { - if (banks->getSubsys() == pss) { - for (int i = 0; i < static_cast(banks->getBanks().size()); i++) { - if (banks->getBanks()[i]->getWeaponId() != pss->weapons.primary_bank_weapons[i]) { - banks->getBanks()[i]->setWeapon(-2); - } - if (banks->getBanks()[i]->getAmmo() != pss->weapons.primary_bank_ammo[i]) { - banks->getBanks()[i]->setAmmo(-2); - } - } - } - } + for (ship_subsys* pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { + if (pss->system_info->type != SUBSYSTEM_TURRET) { + continue; + } + Banks* tracking = findBanksByName(PrimaryBanks, pss->system_info->subobj_name); + if (tracking == nullptr) { + continue; + } + tracking->reconcileAiClass(pss->weapons.ai_class); + const auto bankList = tracking->getBanks(); + for (size_t i = 0; i < bankList.size(); i++) { + reconcileSlot(bankList[i], pss->weapons.primary_bank_weapons[i], + pss->weapons.primary_bank_ammo[i]); } } } @@ -226,111 +314,97 @@ void ShipWeaponsDialogModel::initPrimary(int inst, bool first) void ShipWeaponsDialogModel::initSecondary(int inst, bool first) { - auto pilotBank = new Banks("Pilot", Ships[inst].weapons.ai_class, inst, m_isMultiEdit); if (first) { + int id = 0; + auto pilotBank = std::make_unique("Pilot", Ships[inst].weapons.ai_class, inst, id); + id++; auto pilot = Ships[inst].weapons; - for (int i = 0; i < MAX_SHIP_SECONDARY_BANKS; i++) { - if (pilot.secondary_bank_weapons[i] >= 0) { - const int maxAmmo = - get_max_ammo_count_for_bank(Ships[inst].ship_info_index, i, pilot.secondary_bank_weapons[i]); - const int ammo = fl2ir(pilot.secondary_bank_ammo[i] * maxAmmo / 100.0f); - pilotBank->add(new Bank(pilot.secondary_bank_weapons[i], i, maxAmmo, ammo, pilotBank)); + const int shipClass = Ships[inst].ship_info_index; + const int numPilotBanks = Ship_info[shipClass].num_secondary_banks; + for (int i = 0; i < numPilotBanks; i++) { + const int weaponId = pilot.secondary_bank_weapons[i]; + int maxAmmo = 0; + int ammo = 0; + if (weaponId >= 0) { + maxAmmo = get_max_ammo_count_for_bank(shipClass, i, weaponId); + ammo = fl2ir(pilot.secondary_bank_ammo[i] * maxAmmo / 100.0f); } + pilotBank->add(std::make_unique(weaponId, i, maxAmmo, ammo, pilotBank.get())); + } + if (!pilotBank->empty()) { + SecondaryBanks.push_back(std::move(pilotBank)); } - SecondaryBanks.push_back(pilotBank); ship_subsys* ssl = &Ships[inst].subsys_list; - ship_subsys* pss; - for (pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { + for (ship_subsys* pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { model_subsystem* psub = pss->system_info; if (psub->type == SUBSYSTEM_TURRET) { - auto turretBank = new Banks(psub->subobj_name, pss->weapons.ai_class, inst, m_isMultiEdit, pss); - for (int i = 0; i < MAX_SHIP_SECONDARY_BANKS; i++) { - if (pss->weapons.secondary_bank_weapons[i] >= 0) { - const int maxAmmo = get_max_ammo_count_for_turret_bank(&pss->weapons, - i, - pss->weapons.secondary_bank_weapons[i]); - const int ammo = fl2ir(pss->weapons.secondary_bank_ammo[i] * maxAmmo / 100.0f); - turretBank->add(new Bank(pss->weapons.secondary_bank_weapons[i], i, maxAmmo, ammo, turretBank)); + auto turretBank = std::make_unique(psub->subobj_name, pss->weapons.ai_class, inst, id, pss); + const int numTurretBanks = pss->weapons.num_secondary_banks; + for (int i = 0; i < numTurretBanks; i++) { + const int weaponId = pss->weapons.secondary_bank_weapons[i]; + int maxAmmo = 0; + int ammo = 0; + if (weaponId >= 0) { + maxAmmo = get_max_ammo_count_for_turret_bank(&pss->weapons, i, weaponId); + ammo = fl2ir(pss->weapons.secondary_bank_ammo[i] * maxAmmo / 100.0f); } + turretBank->add(std::make_unique(weaponId, i, maxAmmo, ammo, turretBank.get())); } if (!turretBank->empty()) { - SecondaryBanks.push_back(turretBank); - } else { - delete turretBank; + SecondaryBanks.push_back(std::move(turretBank)); + id++; } } } } else { - for (int i = 0; i < static_cast(SecondaryBanks[0]->getBanks().size()); i++) { - if (SecondaryBanks[0]->getBanks()[i]->getWeaponId() != Ships[inst].weapons.secondary_bank_weapons[i]) { - SecondaryBanks[0]->getBanks()[i]->setWeapon(-2); - } - if (SecondaryBanks[0]->getBanks()[i]->getAmmo() != Ships[inst].weapons.secondary_bank_ammo[i]) { - SecondaryBanks[0]->getBanks()[i]->setAmmo(-2); + if (Banks* tracking = findBanksByName(SecondaryBanks, "Pilot")) { + tracking->reconcileAiClass(Ships[inst].weapons.ai_class); + const auto bankList = tracking->getBanks(); + for (size_t i = 0; i < bankList.size(); i++) { + reconcileSlot(bankList[i], Ships[inst].weapons.secondary_bank_weapons[i], + Ships[inst].weapons.secondary_bank_ammo[i]); } } ship_subsys* ssl = &Ships[inst].subsys_list; - ship_subsys* pss; - for (pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { - model_subsystem* psub = pss->system_info; - if (psub->type == SUBSYSTEM_TURRET) { - for (auto banks : SecondaryBanks) { - if (banks->getSubsys() == pss) { - for (int i = 0; i < static_cast(banks->getBanks().size()); i++) { - if (banks->getByBankId(i)->getWeaponId() != pss->weapons.secondary_bank_weapons[i]) { - banks->getByBankId(i)->setWeapon(-2); - } - if (banks->getByBankId(i)->getAmmo() != pss->weapons.secondary_bank_ammo[i]) { - banks->getByBankId(i)->setAmmo(-2); - } - } - } - } + for (ship_subsys* pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { + if (pss->system_info->type != SUBSYSTEM_TURRET) { + continue; + } + Banks* tracking = findBanksByName(SecondaryBanks, pss->system_info->subobj_name); + if (tracking == nullptr) { + continue; + } + tracking->reconcileAiClass(pss->weapons.ai_class); + const auto bankList = tracking->getBanks(); + for (size_t i = 0; i < bankList.size(); i++) { + reconcileSlot(bankList[i], pss->weapons.secondary_bank_weapons[i], + pss->weapons.secondary_bank_ammo[i]); } } } } void ShipWeaponsDialogModel::saveShip(int inst) { - for (auto Turret : PrimaryBanks) { - if (Turret->getName() == "Pilot") { - for (auto bank : Turret->getBanks()) { - if (bank->getWeaponId() != -2) { - Ships[inst].weapons.primary_bank_weapons[bank->getBankId()] = bank->getWeaponId(); - Ships[inst].weapons.primary_bank_ammo[bank->getBankId()] = - bank->getMaxAmmo() ? fl2ir(bank->getAmmo() * 100.0f / bank->getMaxAmmo()) : 0; - } - } - } else { - ship_subsys* pss = Turret->getSubsys(); - for (auto bank : Turret->getBanks()) { - if (bank->getWeaponId() != -2) { - pss->weapons.primary_bank_weapons[bank->getBankId()] = bank->getWeaponId(); - pss->weapons.primary_bank_ammo[bank->getBankId()] = - bank->getMaxAmmo() ? fl2ir(bank->getAmmo() * 100.0f / bank->getMaxAmmo()) : 0; - } - } + auto saveBank = [&](Banks* turret, bool isPrimary) { + ship_weapon* target = nullptr; + if (turret->getName() == "Pilot") { + target = &Ships[inst].weapons; + } else if (ship_subsys* pss = findTurretByName(inst, turret->getName())) { + target = &pss->weapons; } - } - for (auto Turret : SecondaryBanks) { - if (Turret->getName() == "Pilot") { - for (auto bank : Turret->getBanks()) { - if (bank->getWeaponId() != -2) { - Ships[inst].weapons.secondary_bank_weapons[bank->getBankId()] = bank->getWeaponId(); - Ships[inst].weapons.secondary_bank_ammo[bank->getBankId()] = - bank->getMaxAmmo() ? fl2ir(bank->getAmmo() * 100.0f / bank->getMaxAmmo()) : 0; - } - } - } else { - ship_subsys* pss = Turret->getSubsys(); - for (auto bank : Turret->getBanks()) { - if (bank->getWeaponId() != -2) { - pss->weapons.secondary_bank_weapons[bank->getBankId()] = bank->getWeaponId(); - pss->weapons.secondary_bank_ammo[bank->getBankId()] = - bank->getMaxAmmo() ? fl2ir(bank->getAmmo() * 100.0f / bank->getMaxAmmo()) : 0; - } - } + if (target == nullptr) { + return; } + saveSlotsTo(*target, turret->getBanks(), isPrimary); + if (turret->isAiClassDirty()) { + target->ai_class = turret->getAiClass(); + } + }; + for (const auto& turret : PrimaryBanks) { + saveBank(turret.get(), true); + } + for (const auto& turret : SecondaryBanks) { + saveBank(turret.get(), false); } } bool ShipWeaponsDialogModel::apply() @@ -352,24 +426,101 @@ bool ShipWeaponsDialogModel::apply() } void ShipWeaponsDialogModel::reject() { - for (auto Turret : PrimaryBanks) { - Turret->setAiClass(Turret->getInitalAI()); - } - for (auto Turret : SecondaryBanks) { - Turret->setAiClass(Turret->getInitalAI()); - } + // Weapons, ammo, and AI class are all buffered in the Banks/Bank model and only written + // to Ships[] in apply(). Cancel/close is therefore a true no-op as far as mission data + // is concerned: the next dialog open re-reads ship state from scratch. } SCP_vector ShipWeaponsDialogModel::getPrimaryBanks() const { - return PrimaryBanks; + SCP_vector result; + result.reserve(PrimaryBanks.size()); + for (const auto& b : PrimaryBanks) { + result.push_back(b.get()); + } + return result; } SCP_vector ShipWeaponsDialogModel::getSecondaryBanks() const { - return SecondaryBanks; + SCP_vector result; + result.reserve(SecondaryBanks.size()); + for (const auto& b : SecondaryBanks) { + result.push_back(b.get()); + } + return result; } -/* void ShipWeaponsDialogModel::initTertiary(int inst, bool first) { +SCP_vector ShipWeaponsDialogModel::getAvailableWeapons(WeaponListType type) const +{ + SCP_vector result; + result.push_back(WeaponItem{-1, "None", true}); + const int shipClass = getShipClass(); + const bool haveShipInfo = shipClass >= 0 && shipClass < ship_info_size(); + // allowed_weapons is a player-loadout concept and only meaningful for fighters/bombers. + // On other ship classes (capships, support, etc.) every weapon is rendered as normal. + const bool applyAllowedTint = haveShipInfo && Ship_info[shipClass].is_fighter_bomber(); + + const bool isPrimary = (type == WeaponListType::Primary); + const int wantedSubtype = isPrimary ? WP_LASER : WP_MISSILE; + const bool acceptBeams = isPrimary; + + for (int i = 0; i < static_cast(Weapon_info.size()); i++) { + const auto& w = Weapon_info[i]; + if (w.wi_flags[Weapon::Info_Flags::No_fred]) { + continue; + } + if (w.wi_flags[Weapon::Info_Flags::Child]) { + continue; + } + const bool subtypeMatches = (w.subtype == wantedSubtype) || (acceptBeams && w.subtype == WP_BEAM); + if (!subtypeMatches) { + continue; + } + if (!big && w.wi_flags[Weapon::Info_Flags::Big_only]) { + continue; + } + const bool allowed = !applyAllowedTint || Ship_info[shipClass].allowed_weapons[i] != 0; + result.push_back(WeaponItem{i, w.name, allowed}); + } + return result; +} +SCP_string ShipWeaponsDialogModel::getWeaponName(int weaponId) +{ + if (weaponId == -2) { + return "CONFLICT"; + } + if (weaponId < 0 || weaponId >= static_cast(Weapon_info.size())) { + return "None"; + } + return Weapon_info[weaponId].name; +} +SCP_vector ShipWeaponsDialogModel::getAiClassNames() +{ + SCP_vector result; + result.reserve(Num_ai_classes); + for (int i = 0; i < Num_ai_classes; i++) { + result.emplace_back(Ai_class_names[i]); + } + return result; +} +SCP_string ShipWeaponsDialogModel::getAiClassName(int aiClass) +{ + if (aiClass < 0 || aiClass >= Num_ai_classes) { + return ""; + } + return Ai_class_names[aiClass]; +} +int ShipWeaponsDialogModel::getShipClass() const +{ + return Ships[m_ship].ship_info_index; +} +bool ShipWeaponsDialogModel::isBigShip() const +{ + return big; +} +void ShipWeaponsDialogModel::notifyChanged() +{ + set_modified(); + modelChanged(); } -*/ } // namespace dialogs -} // namespace fso::fred \ No newline at end of file +} // namespace fso::fred diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h index d63908e1c6b..0add1b6f608 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h @@ -1,88 +1,103 @@ -#pragma once - -#include "../AbstractDialogModel.h" - -#include - -namespace fso::fred { -struct Bank; -struct Banks { - Banks(SCP_string name, int aiIndex, int ship, int multiedit, ship_subsys* subsys = nullptr); - - public: - void add(Bank*); - Bank* getByBankId(const int id); - SCP_string getName() const; - int getShip() const; - ship_subsys* getSubsys() const; - bool empty() const; - SCP_vector getBanks() const; - int getAiClass() const; - void setAiClass(int); - bool m_isMultiEdit; - int getInitalAI() const; - - private: - SCP_string name; - ship_subsys* subsys; - int aiClass; - int initalAI; - SCP_vector banks; - int ship; -}; -struct Bank { - public: - Bank(const int weaponId, const int bankId, const int ammoMax, const int ammo, Banks* parent); - - int getWeaponId() const; - int getAmmo() const; - int getBankId() const; - int getMaxAmmo() const; - - void setWeapon(const int id); - void setAmmo(const int ammo); - - private: - int weaponId; - int bankId; - int ammo; - int ammoMax; - Banks* parent; -}; -namespace dialogs { -/** - * @brief QTFred's Weapons Editor Model - */ -class ShipWeaponsDialogModel : public AbstractDialogModel { - public: - /** - * @brief QTFred's Weapons Editor Model Constructer. - * @param [in/out] parent The dialogs parent. - * @param [in/out] viewport Editor viewport. - * @param [in] multi If editing multiple ships. - */ - ShipWeaponsDialogModel(QObject* parent, EditorViewport* viewport, bool multi); - - // void initTertiary(int inst, bool first); - - bool apply() override; - void reject() override; - SCP_vector getPrimaryBanks() const; - SCP_vector getSecondaryBanks() const; - // SCP_vector getTertiaryBanks() const; - - private: - void saveShip(int inst); - void initPrimary(const int inst, bool first); - - void initSecondary(int inst, bool first); - void initializeData(bool multi); - bool m_isMultiEdit; - int m_ship; - bool big = true; - SCP_vector PrimaryBanks; - SCP_vector SecondaryBanks; - // SCP_vector TertiaryBanks; -}; -} // namespace dialogs -} // namespace fso::fred \ No newline at end of file +#pragma once + +#include "../AbstractDialogModel.h" + +#include + +namespace fso::fred { +struct WeaponItem { + int id; + SCP_string name; + bool allowed; +}; + +enum class WeaponListType { Primary, Secondary /*, Tertiary */ }; + +// Bank/Banks are buffered representations of a ship's weapon banks. Mutations through +// their setters (Bank::setWeapon, Bank::setAmmo, Banks::setAiClass) update only this +// in-memory state. They do not mark the model dirty or emit modelChanged(). Callers +// must call ShipWeaponsDialogModel::notifyChanged() after a batch of edits. +struct Bank; +struct Banks { + Banks(SCP_string name, int aiIndex, int ship, int _id, ship_subsys* subsys = nullptr); + ~Banks(); + + public: + int getId() const; + void add(std::unique_ptr); + SCP_string getName() const; + int getShip() const; + ship_subsys* getSubsys() const; + bool empty() const; + SCP_vector getBanks() const; + // Returns the cached AI class for this bank-set; -1 if multi-edit and ships disagree. + int getAiClass() const; + void setAiClass(int); + // Called per-additional-ship during multi-edit init: marks currentAi mixed (-1) if otherAi + // differs from the cached value. Does not mark the bank dirty. + void reconcileAiClass(int otherAi); + bool isAiClassDirty() const; + + private: + SCP_string name; + ship_subsys* subsys; + int currentAi; + bool aiClassDirty = false; + SCP_vector> banks; + int ship; + int id; +}; +struct Bank { + public: + Bank(const int weaponId, const int bankId, const int ammoMax, const int ammo, Banks* parent); + + int getWeaponId() const; + int getAmmo() const; + int getBankId() const; + int getMaxAmmo() const; + + void setWeapon(const int id); + void setAmmo(const int ammo); + + private: + int weaponId; + int bankId; + int ammo; + int ammoMax; + Banks* parent; +}; +namespace dialogs { +class ShipWeaponsDialogModel : public AbstractDialogModel { + public: + ShipWeaponsDialogModel(QObject* parent, EditorViewport* viewport, bool multi); + ~ShipWeaponsDialogModel() override; + + // True iff all currently-marked ships share the same ship_info_index. Used to gate multi-edit. + static bool selectedShipsShareClass(); + + bool apply() override; + void reject() override; + SCP_vector getPrimaryBanks() const; + SCP_vector getSecondaryBanks() const; + SCP_vector getAvailableWeapons(WeaponListType type) const; + // "None" for -1, "CONFLICT" for -2, otherwise the weapon table name. + static SCP_string getWeaponName(int weaponId); + static SCP_vector getAiClassNames(); + static SCP_string getAiClassName(int aiClass); + int getShipClass() const; + bool isBigShip() const; + void notifyChanged(); + + private: + void saveShip(int inst); + void initPrimary(int inst, bool first); + void initSecondary(int inst, bool first); + void initializeData(bool isMultiEdit); + bool m_isMultiEdit; + int m_ship; + bool big = true; + SCP_vector> PrimaryBanks; + SCP_vector> SecondaryBanks; +}; +} // namespace dialogs +} // namespace fso::fred diff --git a/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp b/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp deleted file mode 100644 index e5d6f148004..00000000000 --- a/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp +++ /dev/null @@ -1,404 +0,0 @@ -#include "ShipWeaponsDialog.h" - -#include -#include -namespace fso::fred { -BankTreeItem::BankTreeItem(BankTreeItem* parentItem, QString inName) : name(std::move(inName)), m_parentItem(parentItem) {} -BankTreeItem::~BankTreeItem() -{ - qDeleteAll(m_childItems); -} -void BankTreeItem::appendChild(BankTreeItem* item) -{ - m_childItems.append(item); -} -BankTreeItem* BankTreeItem::child(int row) const -{ - if (row < 0 || row >= m_childItems.size()) - return nullptr; - return m_childItems.at(row); -} -int BankTreeItem::childCount() const -{ - return m_childItems.count(); -} -int BankTreeItem::childNumber() const -{ - if (m_parentItem) - return m_parentItem->m_childItems.indexOf(const_cast(this)); - return 0; -} -BankTreeItem* BankTreeItem::parentItem() -{ - return m_parentItem; -} - -bool BankTreeItem::insertLabel(int position, const QString& newName, Banks* newBanks) -{ - if (position < 0 || position > m_childItems.size()) - return false; - - auto* item = new BankTreeLabel(newName, newBanks, this); - m_childItems.insert(position, item); - - return true; -} - -bool BankTreeItem::insertBank(int position, Bank* newBank) -{ - if (position < 0 || position > m_childItems.size()) - return false; - - auto* item = new BankTreeBank(newBank, this); - m_childItems.insert(position, item); - - return true; -} - -QString BankTreeItem::getName() const -{ - return name; -} - -int BankTreeBank::getId() const -{ - return bank->getWeaponId(); -} - -BankTreeBank::BankTreeBank(Bank* inBank, BankTreeItem* parentItem) : BankTreeItem(parentItem), bank(inBank) -{ - switch (bank->getWeaponId()) { - case -2: - this->name = "CONFLICT"; - break; - case -1: - this->name = "None"; - break; - default: - this->name = Weapon_info[bank->getWeaponId()].name; - } -} - -QVariant BankTreeBank::data(int column) const -{ - switch (column) { - case 0: - return name; - break; - case 1: - return bank->getAmmo(); - break; - default: - return {}; - } -} - -Qt::ItemFlags BankTreeBank::getFlags(int column) const -{ - switch (column) { - case 0: - return Qt::ItemIsDropEnabled | Qt::ItemIsSelectable; - break; - case 1: - return Qt::ItemIsEditable; - break; - default: - return {}; - } -} - -void BankTreeBank::setWeapon(int id) -{ - bank->setWeapon(id); - if (id == -1) { - name = "None"; - } else { - name = Weapon_info[id].name; - } -} - -void BankTreeBank::setAmmo(int value) -{ - Assert(bank != nullptr); - bank->setAmmo(value); -} - -BankTreeLabel::BankTreeLabel(const QString& inName, Banks* inBanks, BankTreeItem* parentItem) - : BankTreeItem(parentItem, inName), banks(inBanks) -{ -} - -QVariant BankTreeLabel::data(int column) const -{ - switch (column) { - case 0: - return name + " (" + Ai_class_names[banks->getAiClass()] + ")"; - break; - default: - return {}; - } -} - -Qt::ItemFlags BankTreeLabel::getFlags(int column) const -{ - Q_UNUSED(column); - return Qt::ItemIsSelectable; -} - -void BankTreeLabel::setAIClass(int value) -{ - Assert(banks != nullptr); - banks->setAiClass(value); -} - -bool BankTreeLabel::setData(int column, const QVariant& value) -{ - Q_UNUSED(column); - setAIClass(value.toInt()); - return true; -} - -bool BankTreeBank::setData(int column, const QVariant& value) -{ - switch (column) { - case 1: - setAmmo(value.toInt()); - return true; - break; - default: - return false; - } -} -BankTreeModel::BankTreeModel(const SCP_vector& data, QObject* parent) : QAbstractItemModel(parent) -{ - rootItem = new BankTreeRoot(); - - setupModelData(data, rootItem); -} - -void BankTreeModel::setupModelData(const SCP_vector& data, BankTreeItem* parent) -{ - for (auto banks : data) { - parent->insertLabel(parent->childCount(), banks->getName().c_str(), banks); - BankTreeItem* currentParent = parent->child(parent->childCount() - 1); - for (auto bank : banks->getBanks()) { - currentParent->insertBank(currentParent->childCount(), bank); - } - } -} - -QVariant BankTreeModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - Q_UNUSED(orientation); - if (role == Qt::DisplayRole) { - switch (section) { - case 0: - return tr("Bank Name/Weapon"); - case 1: - return tr("Ammo"); - default: - return QString(""); - } - } - return {}; -} - -BankTreeModel::~BankTreeModel() -{ - delete rootItem; -} - -int BankTreeModel::columnCount(const QModelIndex& parent) const -{ - Q_UNUSED(parent); - return 2; -} - -QVariant BankTreeModel::data(const QModelIndex& index, int role) const -{ - if (!index.isValid()) { - return {}; - } - - if (role != Qt::DisplayRole && role != Qt::EditRole) - return {}; - - BankTreeItem* item = getItem(index); - - return item->data(index.column()); -} - -BankTreeItem* BankTreeModel::getItem(const QModelIndex index) const -{ - if (index.isValid()) { - auto* item = static_cast(index.internalPointer()); - if (item) - return item; - } - return rootItem; -} - -bool BankTreeModel::setData(const QModelIndex& index, const QVariant& value, int role) -{ - if (role != Qt::EditRole) - return false; - - BankTreeItem* item = getItem(index); // getItem(index); - if (!item) { - return false; - } - bool result = item->setData(index.column(), value); - QVector roles; - roles.append(role); - QAbstractItemModel::dataChanged(index, index, roles); - return result; -} - -int BankTreeModel::rowCount(const QModelIndex& parent) const -{ - if (parent.isValid() && parent.column() > 0) - return 0; - - const BankTreeItem* parentItem = getItem(parent); - - return parentItem ? parentItem->childCount() : 0; -} - -Qt::ItemFlags BankTreeModel::flags(const QModelIndex& index) const -{ - Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); - defaultFlags.setFlag(Qt::ItemIsSelectable, false); - - if (index.isValid()) { - auto* item = static_cast(index.internalPointer()); - return item->getFlags(index.column()) | defaultFlags; - } else { - return Qt::NoItemFlags; - } -} - -QModelIndex BankTreeModel::index(int row, int column, const QModelIndex& parent) const -{ - if (parent.isValid() && parent.column() != 0) - return {}; - - BankTreeItem* parentItem = getItem(parent); - if (!parentItem) - return {}; - - BankTreeItem* childItem = parentItem->child(row); - if (childItem) - return createIndex(row, column, childItem); - return {}; -} -QModelIndex BankTreeModel::parent(const QModelIndex& index) const -{ - if (!index.isValid()) - return {}; - BankTreeItem* childItem = getItem(index); - BankTreeItem* parentItem = childItem ? childItem->parentItem() : nullptr; - - if (parentItem == rootItem || !parentItem) - return {}; - return createIndex(parentItem->childNumber(), 0, parentItem); -} -QStringList BankTreeModel::mimeTypes() const -{ - QStringList types; - types << "application/weaponid"; - return types; -} - -bool BankTreeModel::canDropMimeData(const QMimeData* data, - Qt::DropAction action, - int row, - int column, - const QModelIndex& parent) const -{ - Q_UNUSED(action); - Q_UNUSED(row); - Q_UNUSED(parent); - - if (!data->hasFormat("application/weaponid")) - return false; - BankTreeItem* item = this->getItem(parent); - Qt::ItemFlags flags = item->getFlags(column); - return flags.testFlag(Qt::ItemIsDropEnabled); -} -bool BankTreeModel::dropMimeData(const QMimeData* data, - Qt::DropAction action, - int row, - int column, - const QModelIndex& parent) -{ - if (!canDropMimeData(data, action, row, column, parent)) - return false; - - if (action == Qt::IgnoreAction) - return true; - - if (row == -1 && !parent.isValid()) - return false; - - QByteArray encodedData = data->data("application/weaponid"); - QDataStream stream(&encodedData, QIODevice::ReadOnly); - while (!stream.atEnd()) { - int id = 0; - stream >> id; - setWeapon(parent, id); - } - return true; -} - -void BankTreeModel::setWeapon(const QModelIndex& index, int data) -{ - auto item = dynamic_cast(this->getItem(index)); - Assert(item != nullptr); - if (item != nullptr) { - item->setWeapon(data); - QVector roles; - QAbstractItemModel::dataChanged(index, index, roles); - } -} - -bool BankTreeRoot::setData(int column, const QVariant& value) -{ - Q_UNUSED(column); - Q_UNUSED(value); - return false; -} -QVariant BankTreeRoot::data(int column) const -{ - switch (column) { - case 0: - return "Name/Weapon"; - break; - case 1: - return "Ammo"; - break; - default: - return {}; - } -} -Qt::ItemFlags BankTreeRoot::getFlags(int column) const -{ - Q_UNUSED(column); - return {}; -} - -int BankTreeModel::checktype(const QModelIndex index) const -{ - int type; - BankTreeItem* item = getItem(index); - auto bankTest = dynamic_cast(item); - auto labelTest = dynamic_cast(item); - if (bankTest) { - type = 0; - } else if (labelTest) { - type = 1; - } else { - type = -1; - } - return type; -} -} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/BankModel.h b/qtfred/src/ui/dialogs/ShipEditor/BankModel.h deleted file mode 100644 index a0b42d475e4..00000000000 --- a/qtfred/src/ui/dialogs/ShipEditor/BankModel.h +++ /dev/null @@ -1,94 +0,0 @@ -#pragma once -#include "mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h" - -#include -namespace fso::fred { -class BankTreeItem { - public: - explicit BankTreeItem(BankTreeItem* parentItem = nullptr, QString inName = ""); - virtual ~BankTreeItem(); - virtual QVariant data(int column) const = 0; - void appendChild(BankTreeItem* child); - BankTreeItem* child(int row) const; - int childCount() const; - int childNumber() const; - BankTreeItem* parentItem(); - bool insertLabel(int position, const QString& name, Banks* banks); - bool insertBank(int position, Bank* banks); - - QString getName() const; - virtual bool setData(int column, const QVariant& value) = 0; - virtual Qt::ItemFlags getFlags(int column) const = 0; - QList m_childItems; - - protected: - QString name; - - private: - BankTreeItem* m_parentItem; -}; -class BankTreeRoot : public BankTreeItem { - bool setData(int column, const QVariant& value) override; - QVariant data(int column) const override; - Qt::ItemFlags getFlags(int column) const override; -}; -class BankTreeBank : public BankTreeItem { - public: - explicit BankTreeBank(Bank* inBank, BankTreeItem* parentItem = nullptr); - void setWeapon(int id); - void setAmmo(int value); - int getId() const; - bool setData(int column, const QVariant& value) override; - QVariant data(int column) const override; - Qt::ItemFlags getFlags(int column) const override; - - private: - Bank* bank; -}; -class BankTreeLabel : public BankTreeItem { - public: - explicit BankTreeLabel(const QString& name, Banks* banks, BankTreeItem* parentItem = nullptr); - void setAIClass(int value); - bool setData(int column, const QVariant& value) override; - QVariant data(int column) const override; - Qt::ItemFlags getFlags(int column) const override; - - private: - Banks* banks; -}; - -class BankTreeModel : public QAbstractItemModel { - Q_OBJECT - public: - BankTreeModel(const SCP_vector& data, QObject* parent = nullptr); - ~BankTreeModel() override; - int columnCount(const QModelIndex& parent) const override; - QVariant data(const QModelIndex& index, int role) const override; - - QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; - QModelIndex parent(const QModelIndex& index) const override; - - int rowCount(const QModelIndex& parent = QModelIndex()) const override; - - Qt::ItemFlags flags(const QModelIndex& index) const override; - - QStringList mimeTypes() const override; - bool canDropMimeData(const QMimeData* data, - Qt::DropAction action, - int row, - int column, - const QModelIndex& parent) const override; - bool - dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; - void setWeapon(const QModelIndex& index, int data); - QVariant headerData(int section, Qt::Orientation orientation, int role) const override; - - bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; - int checktype(const QModelIndex index) const; - - private: - BankTreeItem* rootItem; - BankTreeItem* getItem(const QModelIndex index) const; - static void setupModelData(const SCP_vector& data, BankTreeItem* parent); -}; -} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index 025fe86c9bb..cb05100761c 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -1,92 +1,279 @@ #include "ShipWeaponsDialog.h" -#include #include "ui_ShipWeaponsDialog.h" #include +#include #include #include +#include #include -#include +#include +#include +#include +#include + namespace fso::fred::dialogs { + +namespace { +// QStandardItem collapses Qt::EditRole into Qt::DisplayRole, so we can't have a formatted string +// in DisplayRole alongside an int in EditRole. Use a custom role for the spinbox's value. +constexpr int AmmoValueRole = Qt::UserRole + 7; + +QString formatAmmoDisplay(int current, int max) +{ + return QString::number(current) + "/" + QString::number(max); +} + +QString formatAmmoConflict(int max) +{ + return QStringLiteral("--/") + QString::number(max); +} + +// bankTree's drop handler expects the "application/weaponid" MIME type with a single int payload. +// QStandardItemModel's built-in mimeData uses application/x-qabstractitemmodeldatalist instead, so +// we override here to keep the existing contract. +class WeaponListModel : public QStandardItemModel { + public: + using QStandardItemModel::QStandardItemModel; + + QStringList mimeTypes() const override + { + return {QLatin1String(MIME_WEAPON_ID)}; + } + + QMimeData* mimeData(const QModelIndexList& indexes) const override + { + auto* mime = new QMimeData(); + QByteArray bytes; + QDataStream stream(&bytes, QIODevice::WriteOnly); + for (const QModelIndex& index : indexes) { + if (index.isValid()) { + stream << index.data(Qt::UserRole).toInt(); + } + } + mime->setData(QLatin1String(MIME_WEAPON_ID), bytes); + return mime; + } +}; + +class AmmoSpinBoxDelegate : public QStyledItemDelegate { + public: + using QStyledItemDelegate::QStyledItemDelegate; + + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& /*option*/, + const QModelIndex& index) const override + { + auto* editor = new QSpinBox(parent); + editor->setMinimum(0); + const QModelIndex col0 = index.sibling(index.row(), 0); + editor->setMaximum(col0.data(BankItemMaxAmmoRole).toInt()); + editor->setFrame(false); + editor->setAutoFillBackground(true); + return editor; + } + + void setEditorData(QWidget* editor, const QModelIndex& index) const override + { + static_cast(editor)->setValue(index.data(AmmoValueRole).toInt()); + } + + void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override + { + auto* spin = static_cast(editor); + spin->interpretText(); + model->setData(index, spin->value(), AmmoValueRole); + } +}; +} // namespace + ShipWeaponsDialog::ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, bool isMultiEdit) - : QDialog(parent), ui(new Ui::ShipWeaponsDialog()), _model(new ShipWeaponsDialogModel(this, viewport, isMultiEdit)), - _viewport(viewport) + : QDialog(parent), ui(new Ui::ShipWeaponsDialog()), + _model(new ShipWeaponsDialogModel(this, viewport, isMultiEdit)), _viewport(viewport) { ui->setupUi(this); - // connect(this, &QDialog::accepted, _model.get(), &ShipWeaponsDialogModel::apply); - - // Build the model of ship weapons and set inital mode. - if (!_model->getPrimaryBanks().empty()) { - const util::SignalBlockers blockers(this); - bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); - ui->radioPrimary->setChecked(true); - dialogMode = 0; - weapons = new WeaponModel(0); - } else if (!_model->getSecondaryBanks().empty()) { - const util::SignalBlockers blockers(this); - bankModel = new BankTreeModel(_model->getSecondaryBanks(), this); - ui->radioSecondary->setChecked(true); - dialogMode = 1; - weapons = new WeaponModel(1); - } else { + if (_model->getPrimaryBanks().empty() && _model->getSecondaryBanks().empty()) { Error("No Valid Weapon banks on ship"); } - ui->treeBanks->setModel(bankModel); - ui->listWeapons->setModel(weapons); - - connect(ui->treeBanks->selectionModel()->model(), - &QAbstractItemModel::dataChanged, - this, - &ShipWeaponsDialog::updateUI); - // Update the UI whenever selections change - connect(ui->treeBanks->selectionModel(), - &QItemSelectionModel::selectionChanged, - this, - &ShipWeaponsDialog::updateUI); - connect(ui->listWeapons->selectionModel(), - &QItemSelectionModel::selectionChanged, - this, - &ShipWeaponsDialog::updateUI); - - // Setup ai combo box - // connect(ui->AICombo, - // static_cast(&QComboBox::currentIndexChanged), - // this, - //&ShipWeaponsDialog::aiClassChanged); - - // Resize Bank view - ui->treeBanks->expandAll(); - ui->treeBanks->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + + // Tertiary banks are not implemented in the game engine yet, but the UI scaffolding (the + // tertiaryPage in the .ui file, the Mode::Tertiary enum value, the default branch in + // banksForMode) is intentionally preserved so that wiring the engine side later is a small + // follow-up rather than a re-build. + ui->tabWidget->removeTab(2); + + initTab(_primary, Primary); + initTab(_secondary, Secondary); + + // Default to the first tab that has banks. + if (_model->getPrimaryBanks().empty() && !_model->getSecondaryBanks().empty()) { + ui->tabWidget->setCurrentIndex(1); + } + updateUI(); } -ShipWeaponsDialog::~ShipWeaponsDialog() +ShipWeaponsDialog::~ShipWeaponsDialog() = default; + +void ShipWeaponsDialog::initTab(TabState& tab, Mode mode) { - delete bankModel; - delete weapons; + tab.mode = mode; + + if (mode == Primary) { + tab.tree = ui->primaryTreeBanks; + tab.list = ui->primaryListWeapons; + tab.setAllButton = ui->primarySetAllButton; + tab.tblButton = ui->primaryTblButton; + tab.aiButton = ui->primaryAiButton; + tab.aiCombo = ui->primaryAiCombo; + tab.aiGroup = ui->primaryAiGroup; + } else { + tab.tree = ui->secondaryTreeBanks; + tab.list = ui->secondaryListWeapons; + tab.setAllButton = ui->secondarySetAllButton; + tab.tblButton = ui->secondaryTblButton; + tab.aiButton = ui->secondaryAiButton; + tab.aiCombo = ui->secondaryAiCombo; + tab.aiGroup = ui->secondaryAiGroup; + } + + const util::SignalBlockers blockers(this); + + tab.bankModel = new QStandardItemModel(this); + tab.weapons = new WeaponListModel(this); + loadBankModel(tab); + loadWeaponList(tab); + tab.tree->setModel(tab.bankModel); + tab.list->setModel(tab.weapons); + tab.tree->expandAll(); + tab.tree->setHeaderHidden(true); + tab.tree->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + tab.tree->setItemDelegateForColumn(1, new AmmoSpinBoxDelegate(this)); + + tab.aiCombo->clear(); + const auto aiNames = _model->getAiClassNames(); + for (int i = 0; i < static_cast(aiNames.size()); i++) { + tab.aiCombo->addItem(QString::fromUtf8(aiNames[i].c_str()), QVariant(i)); + } + + connect(tab.tree->selectionModel(), &QItemSelectionModel::selectionChanged, this, + [this, &tab](const QItemSelection&, const QItemSelection&) { updateTabUI(tab); }); + connect(tab.list->selectionModel(), &QItemSelectionModel::selectionChanged, this, + [this, &tab](const QItemSelection&, const QItemSelection&) { updateTabUI(tab); }); + connect(tab.setAllButton, &QPushButton::clicked, this, [this, &tab]() { onSetAllClicked(tab); }); + connect(tab.aiButton, &QPushButton::clicked, this, [this, &tab]() { onAiButtonClicked(tab); }); + connect(tab.tblButton, &QPushButton::clicked, this, [this, &tab]() { onTblButtonClicked(tab); }); + connect(tab.bankModel, &QStandardItemModel::itemChanged, this, + [this, &tab](QStandardItem* item) { onBankItemChanged(tab, item); }); + connect(tab.tree, &bankTree::weaponDroppedFromList, this, + [this, &tab](const QModelIndex& target, int weaponId) { + onWeaponDroppedFromList(tab, target, weaponId); + }); + connect(tab.tree, &bankTree::weaponMoved, this, + [this, &tab](const QModelIndex& target, int weaponId, int sourceBanksId, int sourceBankId, bool isCopy) { + onWeaponMoved(tab, target, weaponId, sourceBanksId, sourceBankId, isCopy); + }); + + const auto banks = banksForMode(mode); + const int tabIndex = (mode == Primary) ? 0 : 1; + ui->tabWidget->setTabEnabled(tabIndex, !banks.empty()); +} + +SCP_vector ShipWeaponsDialog::banksForMode(Mode mode) const +{ + switch (mode) { + case Primary: + return _model->getPrimaryBanks(); + case Secondary: + return _model->getSecondaryBanks(); + default: + // Tertiary banks are unsupported by the engine; placeholder mode returns no banks. + return {}; + } +} + +SCP_string ShipWeaponsDialog::banksLabel(const Banks* banks) const +{ + if (banks->getName() == "Pilot") { + return banks->getName(); + } + const int ai = banks->getAiClass(); + if (ai < 0) { + return banks->getName() + " (Mixed AI)"; + } + return banks->getName() + " ( " + _model->getAiClassName(ai) + " ) "; +} + +void ShipWeaponsDialog::loadBankModel(TabState& tab) +{ + const util::SignalBlockers blockers(this); + tab.internalUpdate = true; + tab.bankModel->removeRows(0, tab.bankModel->rowCount()); + tab.bankModel->setColumnCount(2); + for (auto banks : banksForMode(tab.mode)) { + auto nameItem = new QStandardItem(); + const SCP_string name = banksLabel(banks); + nameItem->setData(name.c_str(), Qt::DisplayRole); + nameItem->setData(true, BankItemIsLabelRole); + nameItem->setData(banks->getId(), BankItemIdRole); + nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable); + auto labelAmmoItem = new QStandardItem(); + labelAmmoItem->setFlags(Qt::NoItemFlags); + tab.bankModel->appendRow({nameItem, labelAmmoItem}); + for (auto bank : banks->getBanks()) { + auto weaponItem = new QStandardItem(); + const SCP_string weaponName = _model->getWeaponName(bank->getWeaponId()); + weaponItem->setData(QString::fromUtf8(weaponName.c_str()), Qt::DisplayRole); + weaponItem->setData(bank->getWeaponId(), Qt::UserRole); + weaponItem->setData(false, BankItemIsLabelRole); + weaponItem->setData(bank->getBankId(), BankItemIdRole); + weaponItem->setData(bank->getMaxAmmo(), BankItemMaxAmmoRole); + Qt::ItemFlags weaponFlags = weaponItem->flags() & ~Qt::ItemIsEditable; + // Only slots that have a real weapon can be dragged out. + if (bank->getWeaponId() >= 0) { + weaponFlags |= Qt::ItemIsDragEnabled; + } else { + weaponFlags &= ~Qt::ItemIsDragEnabled; + } + weaponItem->setFlags(weaponFlags); + + auto ammoItem = new QStandardItem(); + Qt::ItemFlags ammoFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; + if (bank->getMaxAmmo() > 0) { + if (bank->getAmmo() == -2) { + // Multi-edit ammo CONFLICT: show --/max, still editable so user can resolve. + ammoItem->setData(formatAmmoConflict(bank->getMaxAmmo()), Qt::DisplayRole); + ammoItem->setData(0, AmmoValueRole); + } else { + ammoItem->setData(formatAmmoDisplay(bank->getAmmo(), bank->getMaxAmmo()), Qt::DisplayRole); + ammoItem->setData(bank->getAmmo(), AmmoValueRole); + } + ammoFlags |= Qt::ItemIsEditable; + } else { + ammoItem->setData(QString(), Qt::DisplayRole); + ammoItem->setData(QVariant(), AmmoValueRole); + } + ammoItem->setFlags(ammoFlags); + nameItem->appendRow({weaponItem, ammoItem}); + } + } + tab.internalUpdate = false; } void ShipWeaponsDialog::accept() { - // If apply() returns true, close the dialog if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don't close } void ShipWeaponsDialog::reject() { - // Asks the user if they want to save changes, if any - // If they do, it runs _model->apply() and returns the success value - // If they don't, it runs _model->reject() and returns true if (rejectOrCloseHandler(this, _model.get(), _viewport)) { - QDialog::reject(); // actually close + QDialog::reject(); } - // else: do nothing, don't close } void ShipWeaponsDialog::closeEvent(QCloseEvent* event) @@ -94,132 +281,397 @@ void ShipWeaponsDialog::closeEvent(QCloseEvent* event) reject(); event->ignore(); } -void ShipWeaponsDialog::on_setAllButton_clicked() + +void ShipWeaponsDialog::on_okAndCancelButtons_accepted() +{ + accept(); +} + +void ShipWeaponsDialog::on_okAndCancelButtons_rejected() +{ + reject(); +} + +void ShipWeaponsDialog::onSetAllClicked(TabState& tab) { - for (auto& index : ui->treeBanks->selectionModel()->selectedIndexes()) { - bankModel->setWeapon(index, ui->listWeapons->currentIndex().data(Qt::UserRole).toInt()); + const QModelIndex weaponIdx = tab.list->currentIndex(); + if (!weaponIdx.isValid()) { + return; + } + const int weaponId = weaponIdx.data(Qt::UserRole).toInt(); + bool anyChanged = false; + for (const QModelIndex& index : tab.tree->selectionModel()->selectedIndexes()) { + Bank* bank = bankForIndex(tab, index); + if (bank == nullptr) { + continue; + } + if (bank->getWeaponId() == weaponId) { + continue; + } + bank->setWeapon(weaponId); + refreshBankItem(tab, index); + anyChanged = true; + } + if (anyChanged) { + _model->notifyChanged(); + } +} + +void ShipWeaponsDialog::onAiButtonClicked(TabState& tab) +{ + const int comboIdx = tab.aiCombo->currentIndex(); + if (comboIdx < 0) { + return; // No AI picked in the combo (still blank from a mixed selection). + } + const int newAi = tab.aiCombo->itemData(comboIdx).toInt(); + TabState& otherTab = (&tab == &_primary) ? _secondary : _primary; + bool anyChanged = false; + for (const QModelIndex& index : tab.tree->selectionModel()->selectedIndexes()) { + if (index.column() != 0) { + continue; + } + Banks* banks = banksForIndex(tab, index); + if (banks == nullptr || banks->getName() == "Pilot") { + continue; + } + if (banks->getAiClass() == newAi) { + continue; + } + banks->setAiClass(newAi); + refreshBankItem(tab, index); + // A turret with both primary and secondary banks has a Banks in each tab pointing at the + // same ship_subsys::weapons.ai_class. Keep them in lockstep so saveShip() can't have one + // tab silently overwrite the other, and so the other tab's label stays accurate. + for (Banks* sibling : banksForMode(otherTab.mode)) { + if (sibling == nullptr || sibling->getName() != banks->getName()) { + continue; + } + if (sibling->getAiClass() != newAi) { + sibling->setAiClass(newAi); + } + const QModelIndex siblingIdx = indexForBanks(otherTab, sibling->getId()); + if (siblingIdx.isValid()) { + refreshBankItem(otherTab, siblingIdx); + } + break; + } + anyChanged = true; + } + if (anyChanged) { + updateTabUI(otherTab); + _model->notifyChanged(); } } -void ShipWeaponsDialog::on_tblButton_clicked() + +void ShipWeaponsDialog::onTblButtonClicked(TabState& tab) { - int wc = ui->listWeapons->currentIndex().data(Qt::UserRole).toInt(); + const int wc = tab.list->currentIndex().data(Qt::UserRole).toInt(); if (wc >= 0) { - auto dialog = new TableViewerDialog(this, _viewport, "Weapon TBL Data", - "weapons.tbl", "*-wep.tbm", Weapon_info[wc].name); + const SCP_string name = _model->getWeaponName(wc); + auto dialog = new TableViewerDialog(this, _viewport, "Weapon TBL Data", "weapons.tbl", "*-wep.tbm", name.c_str()); dialog->show(); } } -void ShipWeaponsDialog::on_radioPrimary_toggled(bool checked) + +void ShipWeaponsDialog::loadWeaponList(TabState& tab) { - modeChanged(checked, 0); + tab.weapons->clear(); + const auto listType = (tab.mode == Primary) ? WeaponListType::Primary : WeaponListType::Secondary; + for (const WeaponItem& item : _model->getAvailableWeapons(listType)) { + auto* row = new QStandardItem(); + row->setData(QString::fromUtf8(item.name.c_str()), Qt::DisplayRole); + row->setData(item.id, Qt::UserRole); + if (!item.allowed) { + row->setData(QBrush(Qt::gray), Qt::ForegroundRole); + row->setData(QStringLiteral("Not in this ship class's allowed weapons list."), Qt::ToolTipRole); + } + row->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); + tab.weapons->appendRow(row); + } } -void ShipWeaponsDialog::on_radioSecondary_toggled(bool checked) + +void ShipWeaponsDialog::updateUI() { - modeChanged(checked, 1); + updateTabUI(_primary); + updateTabUI(_secondary); +} + +void ShipWeaponsDialog::updateTabUI(TabState& tab) +{ + const util::SignalBlockers blockers(this); + + tab.tree->expandAll(); + + const auto selType = tab.tree->getSelectionType(); + tab.setAllButton->setEnabled(selType == bankTree::SelectionType::Weapon); + + // Pilot AI maps to Ships[].weapons.ai_class, which the Ship Editor also owns. Keep that single + // point of truth. Other selected banks (turrets) collectively determine the combo state: + // - all same AI -> show that value + // - any mixed across ships, or differing AIs across selected turrets -> combo blank, user picks + bool aiEditable = (selType == bankTree::SelectionType::Bank); + int displayedAi = -1; + bool combinedInitialized = false; + bool combinedMixed = false; + if (selType == bankTree::SelectionType::Bank) { + for (const QModelIndex& idx : tab.tree->selectionModel()->selectedIndexes()) { + if (idx.column() != 0) { + continue; + } + Banks* banks = banksForIndex(tab, idx); + if (banks == nullptr) { + continue; + } + if (banks->getName() == "Pilot") { + aiEditable = false; + continue; + } + const int ai = banks->getAiClass(); + if (ai < 0) { + combinedMixed = true; + } else if (!combinedInitialized) { + displayedAi = ai; + combinedInitialized = true; + } else if (displayedAi != ai) { + combinedMixed = true; + } + } + } + tab.aiGroup->setEnabled(aiEditable); + if (aiEditable && combinedInitialized && !combinedMixed) { + tab.aiCombo->setCurrentIndex(tab.aiCombo->findData(displayedAi)); + } else { + // Mixed across selection, or only Pilot selected (combo greyed): clear the combo. + tab.aiCombo->setCurrentIndex(-1); + } + + const bool hasWeaponSelection = tab.list->selectionModel()->hasSelection() && + tab.list->currentIndex().data(Qt::UserRole).toInt() != -1; + tab.tblButton->setEnabled(hasWeaponSelection); } -void ShipWeaponsDialog::on_radioTertiary_toggled(bool checked) + +Banks* ShipWeaponsDialog::banksForIndex(const TabState& tab, const QModelIndex& idx) const { - modeChanged(checked, 2); + if (!idx.isValid()) { + return nullptr; + } + if (!idx.data(BankItemIsLabelRole).toBool()) { + return nullptr; + } + const int banksId = idx.data(BankItemIdRole).toInt(); + for (Banks* banks : banksForMode(tab.mode)) { + if (banks->getId() == banksId) { + return banks; + } + } + return nullptr; } -void ShipWeaponsDialog::on_aiCombo_currentIndexChanged(int index) + +Bank* ShipWeaponsDialog::bankForIndex(const TabState& tab, const QModelIndex& idx) const { - aiClassChanged(index); + if (!idx.isValid()) { + return nullptr; + } + if (idx.data(BankItemIsLabelRole).toBool()) { + return nullptr; + } + const QModelIndex parent = idx.parent(); + if (!parent.isValid()) { + return nullptr; + } + Banks* parentBanks = banksForIndex(tab, parent); + if (parentBanks == nullptr) { + return nullptr; + } + const int bankId = idx.data(BankItemIdRole).toInt(); + for (Bank* bank : parentBanks->getBanks()) { + if (bank->getBankId() == bankId) { + return bank; + } + } + return nullptr; } -void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) + +void ShipWeaponsDialog::refreshBankItem(TabState& tab, const QModelIndex& idx) { - if (enabled) { - if (mode == 0) { - bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); - dialogMode = 0; - delete weapons; - weapons = new WeaponModel(0); - ui->listWeapons->setModel(weapons); - } else if (mode == 1) { - bankModel = new BankTreeModel(_model->getSecondaryBanks(), this); - dialogMode = 1; - delete weapons; - weapons = new WeaponModel(1); - ui->listWeapons->setModel(weapons); - } else if (mode == 2) { - // bankModel = new BankTreeModel(_model->getTertiaryBanks(), this); - dialogMode = 2; + if (!idx.isValid() || tab.bankModel == nullptr) { + return; + } + const QModelIndex col0 = idx.sibling(idx.row(), 0); + tab.internalUpdate = true; + if (col0.data(BankItemIsLabelRole).toBool()) { + Banks* banks = banksForIndex(tab, col0); + if (banks != nullptr) { + const SCP_string name = banksLabel(banks); + tab.bankModel->setData(col0, name.c_str(), Qt::DisplayRole); + } + tab.internalUpdate = false; + return; + } + Bank* bank = bankForIndex(tab, col0); + if (bank == nullptr) { + tab.internalUpdate = false; + return; + } + const SCP_string name = _model->getWeaponName(bank->getWeaponId()); + tab.bankModel->setData(col0, QString::fromUtf8(name.c_str()), Qt::DisplayRole); + tab.bankModel->setData(col0, bank->getWeaponId(), Qt::UserRole); + tab.bankModel->setData(col0, bank->getMaxAmmo(), BankItemMaxAmmoRole); + if (QStandardItem* weaponItem = tab.bankModel->itemFromIndex(col0)) { + Qt::ItemFlags f = weaponItem->flags(); + if (bank->getWeaponId() >= 0) { + f |= Qt::ItemIsDragEnabled; } else { - _viewport->dialogProvider->showButtonDialog(DialogType::Error, - "Illegal Mode", - "Somehow an Illegal mode has been set. Get a coder.\n Illegal mode is " + std::to_string(mode), - {DialogButton::Ok}); - ui->radioPrimary->toggled(true); - bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); - dialogMode = 0; - } - // Reconnect beacuse the model has changed - connect(ui->treeBanks->selectionModel()->model(), - &QAbstractItemModel::dataChanged, - this, - &ShipWeaponsDialog::updateUI); - connect(ui->treeBanks->selectionModel(), - &QItemSelectionModel::selectionChanged, - this, - &ShipWeaponsDialog::updateUI); - connect(ui->listWeapons->selectionModel(), - &QItemSelectionModel::selectionChanged, - this, - &ShipWeaponsDialog::updateUI); - ui->treeBanks->setModel(bankModel); - ui->treeBanks->expandAll(); + f &= ~Qt::ItemIsDragEnabled; + } + weaponItem->setFlags(f); } - updateUI(); + + const QModelIndex col1 = idx.sibling(idx.row(), 1); + if (col1.isValid()) { + QStandardItem* ammoItem = tab.bankModel->itemFromIndex(col1); + if (ammoItem != nullptr) { + Qt::ItemFlags ammoFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; + if (bank->getMaxAmmo() > 0) { + if (bank->getAmmo() == -2) { + ammoItem->setData(formatAmmoConflict(bank->getMaxAmmo()), Qt::DisplayRole); + ammoItem->setData(0, AmmoValueRole); + } else { + ammoItem->setData(formatAmmoDisplay(bank->getAmmo(), bank->getMaxAmmo()), Qt::DisplayRole); + ammoItem->setData(bank->getAmmo(), AmmoValueRole); + } + ammoFlags |= Qt::ItemIsEditable; + } else { + ammoItem->setData(QString(), Qt::DisplayRole); + ammoItem->setData(QVariant(), AmmoValueRole); + } + ammoItem->setFlags(ammoFlags); + } + } + tab.internalUpdate = false; } -void ShipWeaponsDialog::updateUI() + +void ShipWeaponsDialog::onBankItemChanged(TabState& tab, QStandardItem* item) { - const util::SignalBlockers blockers(this); - // Radio Buttons - ui->radioPrimary->setEnabled(!_model->getPrimaryBanks().empty()); - ui->radioSecondary->setEnabled(!_model->getSecondaryBanks().empty()); - ui->radioTertiary->setEnabled(false); - - ui->treeBanks->expandAll(); - // Setall button - if (ui->treeBanks->getTypeSelected() == 0) { - ui->setAllButton->setEnabled(true); - } else { - ui->setAllButton->setEnabled(false); + if (item == nullptr || tab.internalUpdate) { + return; } - // Change AI Button - if (ui->treeBanks->getTypeSelected() == 1) { - ui->aiButton->setEnabled(true); - } else { - ui->aiButton->setEnabled(false); + if (item->column() != 1) { + return; } - // AI Combo Box - ui->aiCombo->clear(); - for (int i = 0; i < Num_ai_classes; i++) { - ui->aiCombo->addItem(Ai_class_names[i], QVariant(i)); + const QModelIndex col0 = item->index().sibling(item->row(), 0); + if (!col0.isValid() || col0.data(BankItemIsLabelRole).toBool()) { + return; } - ui->aiCombo->setCurrentIndex(ui->aiCombo->findData(m_currentAI)); - if (ui->listWeapons->selectionModel()->hasSelection() && - ui->listWeapons->currentIndex().data(Qt::UserRole).toInt() != -1) { - ui->tblButton->setEnabled(true); - } else { - ui->tblButton->setEnabled(false); + Bank* bank = bankForIndex(tab, col0); + if (bank == nullptr) { + return; + } + bool ok = false; + int requested = item->data(AmmoValueRole).toInt(&ok); + if (!ok) { + requested = bank->getAmmo(); + } + const int clamped = std::max(0, std::min(requested, bank->getMaxAmmo())); + if (clamped != bank->getAmmo()) { + bank->setAmmo(clamped); + _model->notifyChanged(); } + // Always write back the canonical value. This covers the case where the user typed an out-of-range + // number and our clamp differs from what they entered. + tab.internalUpdate = true; + item->setData(formatAmmoDisplay(clamped, bank->getMaxAmmo()), Qt::DisplayRole); + item->setData(clamped, AmmoValueRole); + tab.internalUpdate = false; } -void ShipWeaponsDialog::aiClassChanged(const int index) +void ShipWeaponsDialog::onWeaponDroppedFromList(TabState& tab, const QModelIndex& target, int weaponId) { - m_currentAI = ui->aiCombo->itemData(index).toInt(); + Bank* bank = bankForIndex(tab, target); + if (bank == nullptr) { + return; + } + if (bank->getWeaponId() == weaponId) { + return; + } + bank->setWeapon(weaponId); + refreshBankItem(tab, target); + _model->notifyChanged(); } -void ShipWeaponsDialog::on_aiButton_clicked() +void ShipWeaponsDialog::onWeaponMoved(TabState& tab, const QModelIndex& target, int weaponId, + int sourceBanksId, int sourceBankId, bool isCopy) { - for (auto& index : ui->treeBanks->selectionModel()->selectedIndexes()) { - bankModel->setData(index, m_currentAI); + Bank* targetBank = bankForIndex(tab, target); + if (targetBank == nullptr) { + return; } + + Bank* sourceBank = nullptr; + for (Banks* banks : banksForMode(tab.mode)) { + if (banks->getId() != sourceBanksId) { + continue; + } + for (Bank* b : banks->getBanks()) { + if (b->getBankId() == sourceBankId) { + sourceBank = b; + break; + } + } + break; + } + if (sourceBank == nullptr || sourceBank == targetBank) { + return; + } + + targetBank->setWeapon(weaponId); + if (!isCopy) { + sourceBank->setWeapon(-1); + } + + refreshBankItem(tab, target); + const QModelIndex sourceIdx = indexForBank(tab, sourceBanksId, sourceBankId); + if (sourceIdx.isValid()) { + refreshBankItem(tab, sourceIdx); + } + _model->notifyChanged(); } -void ShipWeaponsDialog::on_buttonClose_clicked() +QModelIndex ShipWeaponsDialog::indexForBank(const TabState& tab, int banksId, int bankId) { - accept(); + if (tab.bankModel == nullptr) { + return {}; + } + const int topRows = tab.bankModel->rowCount(); + for (int i = 0; i < topRows; i++) { + const QModelIndex parent = tab.bankModel->index(i, 0); + if (parent.data(BankItemIdRole).toInt() != banksId) { + continue; + } + const int childRows = tab.bankModel->rowCount(parent); + for (int j = 0; j < childRows; j++) { + const QModelIndex child = tab.bankModel->index(j, 0, parent); + if (child.data(BankItemIdRole).toInt() == bankId) { + return child; + } + } + } + return {}; +} + +QModelIndex ShipWeaponsDialog::indexForBanks(const TabState& tab, int banksId) +{ + if (tab.bankModel == nullptr) { + return {}; + } + const int topRows = tab.bankModel->rowCount(); + for (int i = 0; i < topRows; i++) { + const QModelIndex parent = tab.bankModel->index(i, 0); + if (parent.data(BankItemIdRole).toInt() == banksId) { + return parent; + } + } + return {}; } -} // namespace fso::fred::dialogs \ No newline at end of file +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h index 7b5efb83578..75b4e221ac6 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -1,33 +1,26 @@ -#ifndef SHIPWEAPONSDIALOG_H -#define SHIPWEAPONSDIALOG_H +#pragma once -#include "ui/dialogs/ShipEditor/BankModel.h" -#include "ui/widgets/weaponList.h" +#include "ui/widgets/bankTree.h" #include -#include +#include #include +#include #include +#include +#include namespace fso::fred::dialogs { namespace Ui { class ShipWeaponsDialog; } -/** - * @brief QTFred's Weapons Editor - */ + class ShipWeaponsDialog : public QDialog { Q_OBJECT public: - /** - * @brief QTFred's Weapons Editor Constructer. - * @param [in/out] parent The dialogs parent. - * @param [in/out] viewport Editor viewport. - * @param [in] isMultiEdit If editing multiple ships. - */ explicit ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, bool isMultiEdit); ~ShipWeaponsDialog() override; @@ -38,31 +31,55 @@ class ShipWeaponsDialog : public QDialog { void closeEvent(QCloseEvent*) override; private slots: - void on_buttonClose_clicked(); - void on_aiButton_clicked(); - void on_setAllButton_clicked(); - void on_tblButton_clicked(); - void on_radioPrimary_toggled(bool checked); - void on_radioSecondary_toggled(bool checked); - void on_radioTertiary_toggled(bool checked); - void on_aiCombo_currentIndexChanged(int index); + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); private: // NOLINT(readability-redundant-access-specifiers) + enum Mode { Primary = 0, Secondary = 1, Tertiary = 2 }; + + struct TabState { + Mode mode = Primary; + bankTree* tree = nullptr; + QListView* list = nullptr; + QPushButton* setAllButton = nullptr; + QPushButton* tblButton = nullptr; + QPushButton* aiButton = nullptr; + QComboBox* aiCombo = nullptr; + QWidget* aiGroup = nullptr; + QStandardItemModel* bankModel = nullptr; + QStandardItemModel* weapons = nullptr; + // Set while the dialog itself is writing into bankModel, so itemChanged handlers can ignore the resulting signals. + bool internalUpdate = false; + }; + + void initTab(TabState& tab, Mode mode); + void loadBankModel(TabState& tab); + void loadWeaponList(TabState& tab); + void updateTabUI(TabState& tab); + void updateUI(); + + void onSetAllClicked(TabState& tab); + void onAiButtonClicked(TabState& tab); + void onTblButtonClicked(TabState& tab); + void onBankItemChanged(TabState& tab, QStandardItem* item); + void onWeaponDroppedFromList(TabState& tab, const QModelIndex& target, int weaponId); + void onWeaponMoved(TabState& tab, const QModelIndex& target, int weaponId, int sourceBanksId, + int sourceBankId, bool isCopy); + + static QModelIndex indexForBank(const TabState& tab, int banksId, int bankId); + static QModelIndex indexForBanks(const TabState& tab, int banksId); + + Bank* bankForIndex(const TabState& tab, const QModelIndex& idx) const; + Banks* banksForIndex(const TabState& tab, const QModelIndex& idx) const; + void refreshBankItem(TabState& tab, const QModelIndex& idx); + SCP_vector banksForMode(Mode mode) const; + SCP_string banksLabel(const Banks* banks) const; + std::unique_ptr ui; std::unique_ptr _model; - /** - * @brief Changes current weapon type. - * @param [in] enabled Always True - * @param [in] mode The mode to change to. 0 = Primary, 1 = Secondary - */ - void modeChanged(const bool enabled, const int mode); EditorViewport* _viewport; - void updateUI(); - BankTreeModel* bankModel; - int dialogMode; - WeaponModel* weapons; - int m_currentAI = 0; - void aiClassChanged(const int index); + + TabState _primary; + TabState _secondary; }; } // namespace fso::fred::dialogs -#endif \ No newline at end of file diff --git a/qtfred/src/ui/widgets/bankTree.cpp b/qtfred/src/ui/widgets/bankTree.cpp index f2d28dd22ee..43b02f14fa9 100644 --- a/qtfred/src/ui/widgets/bankTree.cpp +++ b/qtfred/src/ui/widgets/bankTree.cpp @@ -1,103 +1,166 @@ #include "bankTree.h" + +#include + namespace fso::fred { + +namespace { +constexpr const char* MIME_BANK_TREE_WEAPON = "application/banktreeweapon"; +} // namespace + bankTree::bankTree(QWidget* parent) : QTreeView(parent) { setAcceptDrops(true); } + void bankTree::dragEnterEvent(QDragEnterEvent* event) { - if (event->mimeData()->hasFormat("application/weaponid")) { + if (event->mimeData()->hasFormat(MIME_WEAPON_ID) || event->mimeData()->hasFormat(MIME_BANK_TREE_WEAPON)) { event->acceptProposedAction(); + } else { + event->ignore(); + } +} + +void bankTree::dragMoveEvent(QDragMoveEvent* event) +{ + const QModelIndex index = indexAt(event->pos()); + if (!index.isValid() || index.data(BankItemIsLabelRole).toBool()) { + event->ignore(); + return; } + event->acceptProposedAction(); } + void bankTree::dropEvent(QDropEvent* event) { - auto item = indexAt(event->pos()); - if (!item.isValid()) { + QModelIndex target = indexAt(event->pos()); + if (!target.isValid() || target.data(BankItemIsLabelRole).toBool()) { + event->ignore(); return; } - bool accepted = model()->dropMimeData(event->mimeData(), Qt::CopyAction, -1, 0, item); - if (accepted) { + if (target.column() != 0) { + target = target.sibling(target.row(), 0); + } + + const QMimeData* mime = event->mimeData(); + if (mime->hasFormat(MIME_BANK_TREE_WEAPON)) { + QByteArray bytes = mime->data(MIME_BANK_TREE_WEAPON); + QDataStream stream(&bytes, QIODevice::ReadOnly); + int weaponId = 0; + int sourceBanksId = 0; + int sourceBankId = 0; + stream >> weaponId >> sourceBanksId >> sourceBankId; + const bool isCopy = (event->keyboardModifiers() & Qt::ControlModifier) != 0; + weaponMoved(target, weaponId, sourceBanksId, sourceBankId, isCopy); event->acceptProposedAction(); + } else if (mime->hasFormat(MIME_WEAPON_ID)) { + QByteArray bytes = mime->data(MIME_WEAPON_ID); + QDataStream stream(&bytes, QIODevice::ReadOnly); + int weaponId = 0; + stream >> weaponId; + weaponDroppedFromList(target, weaponId); + event->acceptProposedAction(); + } else { + event->ignore(); } } -void bankTree::dragMoveEvent(QDragMoveEvent* event) + +void bankTree::startDrag(Qt::DropActions /*supportedActions*/) { - auto pos = QCursor::pos(); - auto index = indexAt(pos); - if (!index.isValid()) { + QModelIndex source; + for (const QModelIndex& idx : selectionModel()->selectedIndexes()) { + if (idx.column() != 0 || !idx.isValid()) { + continue; + } + if (idx.data(BankItemIsLabelRole).toBool()) { + continue; + } + source = idx; + break; + } + if (!source.isValid()) { return; } - if (dynamic_cast(model())->checktype(index) == 0) { - event->accept(); - } else { - event->ignore(); + const int weaponId = source.data(Qt::UserRole).toInt(); + // "None" (-1) and "CONFLICT" (-2) slots have no weapon to drag. + if (weaponId < 0) { + return; } + + const QModelIndex parent = source.parent(); + if (!parent.isValid()) { + return; + } + const int sourceBanksId = parent.data(BankItemIdRole).toInt(); + const int sourceBankId = source.data(BankItemIdRole).toInt(); + + auto* mime = new QMimeData(); + QByteArray bytes; + QDataStream stream(&bytes, QIODevice::WriteOnly); + stream << weaponId << sourceBanksId << sourceBankId; + mime->setData(MIME_BANK_TREE_WEAPON, bytes); + + auto* drag = new QDrag(this); + drag->setMimeData(mime); + drag->exec(Qt::MoveAction | Qt::CopyAction, Qt::MoveAction); } + void bankTree::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { - QItemSelection newlySelected; - QItemSelection select; - QItemSelection deselect(deselected); - if (selected.empty()) { - QTreeView::selectionChanged(selected, deselected); - if (selectionModel()->selectedIndexes().empty()) { - typeSelected = -1; + QTreeView::selectionChanged(selected, deselected); + + if (m_autoFiltering) { + return; + } + + const auto newlySelected = selected.indexes(); + if (newlySelected.isEmpty()) { + return; + } + + QModelIndex pivot; + for (const QModelIndex& idx : newlySelected) { + if (idx.column() == 0 && idx.isValid()) { + pivot = idx; + break; } + } + if (!pivot.isValid()) { return; } - for (auto& sidx : selected.indexes()) { - bool match = false; - for (auto& didx : deselected.indexes()) { - if (sidx == didx) { - match = true; - break; - } + const bool pivotIsBank = pivot.data(BankItemIsLabelRole).toBool(); + + QItemSelectionModel* sm = selectionModel(); + QItemSelection toDeselect; + for (const QModelIndex& idx : sm->selectedIndexes()) { + if (idx.column() != 0) { + continue; } - if (!match) { - QItemSelectionRange selection(sidx); - newlySelected.append(selection); + if (idx.data(BankItemIsLabelRole).toBool() != pivotIsBank) { + toDeselect.select(idx, idx); } } - if (!newlySelected.empty()) { - if (typeSelected == -1) { - typeSelected = dynamic_cast(model())->checktype(newlySelected.indexes().first()); - for (auto& sidx : newlySelected.indexes()) { - if (dynamic_cast(model())->checktype(sidx) == typeSelected) { - QItemSelectionRange selection(sidx); - select.append(selection); - } - } - } else { - int type = dynamic_cast(model())->checktype(newlySelected.indexes().first()); - if (type != typeSelected) { - typeSelected = type; - for (auto& sidx : selected.indexes()) { - QItemSelectionRange selection(sidx); - deselect.append(selection); - } - for (auto& sidx : newlySelected.indexes()) { - if (dynamic_cast(model())->checktype(sidx) == typeSelected) { - QItemSelectionRange selection(sidx); - select.append(selection); - } - } - selectionModel()->clear(); - typeSelected = -1; - } else { - for (auto& sidx : newlySelected.indexes()) { - if (dynamic_cast(model())->checktype(sidx) == typeSelected) { - QItemSelectionRange selection(sidx); - select.append(selection); - } - } - } - } + + if (!toDeselect.isEmpty()) { + m_autoFiltering = true; + sm->select(toDeselect, QItemSelectionModel::Deselect | QItemSelectionModel::Rows); + m_autoFiltering = false; } - QTreeView::selectionChanged(select, deselect); } -int bankTree::getTypeSelected() const + +bankTree::SelectionType bankTree::getSelectionType() const { - return typeSelected; + const auto* sm = selectionModel(); + if (sm == nullptr) { + return SelectionType::None; + } + for (const QModelIndex& idx : sm->selectedIndexes()) { + if (idx.column() != 0 || !idx.isValid()) { + continue; + } + return idx.data(BankItemIsLabelRole).toBool() ? SelectionType::Bank : SelectionType::Weapon; + } + return SelectionType::None; } -} // namespace fso::fred \ No newline at end of file +} // namespace fso::fred diff --git a/qtfred/src/ui/widgets/bankTree.h b/qtfred/src/ui/widgets/bankTree.h index 81268d173cf..73963fccbfa 100644 --- a/qtfred/src/ui/widgets/bankTree.h +++ b/qtfred/src/ui/widgets/bankTree.h @@ -1,7 +1,4 @@ #pragma once -#include "ui/dialogs/ShipEditor/BankModel.h" - -#include #include #include @@ -9,17 +6,36 @@ #include #include namespace fso::fred { +// MIME type for drags originating from the weapons-list view into the bank tree. +// Payload: a single int (weapon id) written via QDataStream. +constexpr const char* MIME_WEAPON_ID = "application/weaponid"; + +// Custom data roles stored on items of the bank tree's QStandardItemModel. +// (Qt::UserRole itself is used for the weapon-id on weapon-slot rows.) +constexpr int BankItemIsLabelRole = Qt::UserRole + 2; // bool: true on bank-label rows, false on weapon-slot rows +constexpr int BankItemIdRole = Qt::UserRole + 3; // Banks::getId() on labels, Bank::getBankId() on slots +constexpr int BankItemMaxAmmoRole = Qt::UserRole + 5; // weapon's max ammo on the bank, 0 if not applicable + class bankTree : public QTreeView { Q_OBJECT public: + enum class SelectionType { None, Bank, Weapon }; + bankTree(QWidget*); void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) override; - int getTypeSelected() const; + SelectionType getSelectionType() const; + + signals: + void weaponDroppedFromList(const QModelIndex& target, int weaponId); + void weaponMoved(const QModelIndex& target, int weaponId, int sourceBanksId, int sourceBankId, bool isCopy); protected: void dragEnterEvent(QDragEnterEvent*) override; void dropEvent(QDropEvent* event) override; void dragMoveEvent(QDragMoveEvent*) override; - int typeSelected = -1; + void startDrag(Qt::DropActions supportedActions) override; + + private: + bool m_autoFiltering = false; }; -} // namespace fso::fred \ No newline at end of file +} // namespace fso::fred diff --git a/qtfred/src/ui/widgets/weaponList.cpp b/qtfred/src/ui/widgets/weaponList.cpp deleted file mode 100644 index 78f40afd018..00000000000 --- a/qtfred/src/ui/widgets/weaponList.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "weaponList.h" - -namespace fso::fred { -weaponList::weaponList(QWidget* parent) : QListView(parent) {} - -void weaponList::mousePressEvent(QMouseEvent* event) -{ - if (event->button() == Qt::LeftButton) { - dragStartPosition = event->pos(); - } - QListView::mousePressEvent(event); -} -void weaponList::mouseMoveEvent(QMouseEvent* event) -{ - if (!(event->buttons() & Qt::LeftButton)) - return; - if ((event->pos() - dragStartPosition).manhattanLength() < QApplication::startDragDistance()) - return; - QModelIndex idx = currentIndex(); - if (!idx.isValid()) { - return; - } - auto drag = new QDrag(this); - QModelIndexList idxs; - idxs.append(idx); - QMimeData* mimeData = model()->mimeData(idxs); - auto iconPixmap = new QPixmap(); - QPainter painter(iconPixmap); - painter.setFont(QFont("Arial")); - painter.drawText(QPoint(100, 100), model()->data(idx, Qt::DisplayRole).toString()); - drag->setPixmap(*iconPixmap); - drag->setMimeData(mimeData); - drag->exec(); -} - -WeaponModel::WeaponModel(int type) -{ - auto noWeapon = new WeaponItem(-1, "None"); - weapons.push_back(noWeapon); - if (type == 0) { - for (int i = 0; i < static_cast(Weapon_info.size()); i++) { - const auto& w = Weapon_info[i]; - if (w.subtype == WP_LASER || w.subtype == WP_BEAM) { - if (!w.wi_flags[Weapon::Info_Flags::No_fred]) { - auto newWeapon = new WeaponItem(i, w.name); - weapons.push_back(newWeapon); - } - } - } - } else if (type == 1) { - for (int i = 0; i < static_cast(Weapon_info.size()); i++) { - const auto& w = Weapon_info[i]; - if (w.subtype == WP_MISSILE) { - if (!w.wi_flags[Weapon::Info_Flags::No_fred]) { - auto newWeapon = new WeaponItem(i, w.name); - weapons.push_back(newWeapon); - } - } - } - } -} -WeaponModel::~WeaponModel() -{ - for (auto pointer : weapons) { - delete pointer; - } -} -int WeaponModel::rowCount(const QModelIndex& parent) const -{ - Q_UNUSED(parent); - return static_cast(weapons.size()); -} -QVariant WeaponModel::data(const QModelIndex& index, int role) const -{ - if (role == Qt::DisplayRole) { - const QString out = weapons[index.row()]->name; - return out; - } - if (role == Qt::UserRole) { - const int id = weapons[index.row()]->id; - return id; - } - return {}; -} -QMimeData* WeaponModel::mimeData(const QModelIndexList& indexes) const -{ - auto mimeData = new QMimeData(); - QByteArray encodedData; - QDataStream stream(&encodedData, QIODevice::WriteOnly); - for (auto& index : indexes) { - if (index.isValid()) { - int id = data(index, Qt::UserRole).toInt(); - stream << id; - } - } - - mimeData->setData("application/weaponid", encodedData); - - return mimeData; -} -WeaponItem::WeaponItem(const int inID, QString inName) : name(std::move(inName)), id(inID) {} -} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/widgets/weaponList.h b/qtfred/src/ui/widgets/weaponList.h deleted file mode 100644 index ed15b120798..00000000000 --- a/qtfred/src/ui/widgets/weaponList.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once -#include - -#include -#include -#include -#include -#include -#include -namespace fso::fred { -struct WeaponItem { - WeaponItem(const int id, QString name); - const QString name; - const int id; -}; -class WeaponModel : public QAbstractListModel { - Q_OBJECT - public: - WeaponModel(int type); - ~WeaponModel() override; - int rowCount(const QModelIndex& parent = QModelIndex()) const override; - QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - QMimeData* mimeData(const QModelIndexList& indexes) const override; - QVector weapons; -}; -class weaponList : public QListView { - Q_OBJECT - public: - weaponList(QWidget* parent); - - protected: - void mousePressEvent(QMouseEvent* event) override; - void mouseMoveEvent(QMouseEvent* event) override; - QPoint dragStartPosition; - - private: -}; -} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/ui/ShipWeaponsDialog.ui b/qtfred/ui/ShipWeaponsDialog.ui index 0d6329025fa..dc72c7e1cc0 100644 --- a/qtfred/ui/ShipWeaponsDialog.ui +++ b/qtfred/ui/ShipWeaponsDialog.ui @@ -18,181 +18,297 @@ - - - Mode + + + 0 - - - - - Primary - - - - - - - Secondary - - - - - - - Tertiary - - - - - - - - - - - - Weapons - - - - - - QAbstractScrollArea::AdjustIgnored + + + Primary + + + + + + + + Weapons + + + + + + QAbstractScrollArea::AdjustIgnored + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::DragOnly + + + + + + + View Table + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Set Selected + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Banks + + + + + + QAbstractScrollArea::AdjustToContentsOnFirstShow + + + QAbstractItemView::DragDrop + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectItems + + + false + + + + + + + + + + + + + 0 - - QAbstractItemView::NoEditTriggers + + 0 - - true + + 0 - - QAbstractItemView::DragOnly + + 0 - - - - - - View Table + + + + + + + Change AI + + + + + + + + + + + Secondary + + + + + + + + Weapons + + + + + + QAbstractScrollArea::AdjustIgnored + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::DragOnly + + + + + + + View Table + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Set Selected + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Banks + + + + + + QAbstractScrollArea::AdjustToContentsOnFirstShow + + + QAbstractItemView::DragDrop + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectItems + + + false + + + + + + + + + + + + + 0 - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Set Selected - - - false - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Banks - - - - - - QAbstractScrollArea::AdjustToContentsOnFirstShow + + 0 - - QAbstractItemView::DropOnly + + 0 - - QAbstractItemView::MultiSelection + + 0 - - QAbstractItemView::SelectItems - - - false - - - - - - - - - - - - - - - - - Change AI - - - - + + + + + + + Change AI + + + + + + + + + + + Tertiary + + + - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Close - - - - + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + - - fso::fred::weaponList - QListView -
ui/widgets/weaponList.h
-
fso::fred::bankTree QTreeView From 9de7315b383ae0d6622909bc699aa49d7a9a3863 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 16 May 2026 09:41:59 -0500 Subject: [PATCH 58/65] move waypoint and jump node to their own dropdown category (#7455) --- qtfred/help-src/doc/fundamentals.html | 10 ++-- qtfred/help-src/doc/general/Toolbars.html | 16 +++++-- qtfred/help-src/doc/general/Viewport.html | 9 ++-- qtfred/src/mission/Editor.cpp | 2 - qtfred/src/mission/Editor.h | 3 -- qtfred/src/mission/EditorViewport.cpp | 58 +++++++++++++---------- qtfred/src/mission/EditorViewport.h | 16 ++++++- qtfred/src/ui/FredView.cpp | 33 +++++++++++-- qtfred/src/ui/FredView.h | 3 ++ qtfred/src/ui/widgets/ObjectComboBox.cpp | 21 +++++--- qtfred/src/ui/widgets/ObjectComboBox.h | 6 +-- qtfred/src/ui/widgets/renderwidget.cpp | 11 ++++- 12 files changed, 126 insertions(+), 62 deletions(-) diff --git a/qtfred/help-src/doc/fundamentals.html b/qtfred/help-src/doc/fundamentals.html index c6eb1410efa..b7fdb082e68 100644 --- a/qtfred/help-src/doc/fundamentals.html +++ b/qtfred/help-src/doc/fundamentals.html @@ -67,11 +67,11 @@

2. Place the ships

arrive immediately and that is fine for now.

3. Add a waypoint path for the transport

-

Select the waypoint tool in the toolbar and Ctrl+click in the viewport -to lay out a short route from the transport's position toward the Command Ship. -Three or four points is plenty. Open the -Waypoint Editor and name the path -Transport Route.

+

In the Other dropdown on the toolbar, select Waypoint, +then Ctrl+Alt+click in the viewport to lay out a short route from the +transport's position toward the Command Ship. Three or four points is plenty. Open +the Waypoint Editor and name the +path Transport Route.

Open the Ship Editor for Transport 1, go to Initial Orders, and set its initial goal to diff --git a/qtfred/help-src/doc/general/Toolbars.html b/qtfred/help-src/doc/general/Toolbars.html index 4ffec618211..6c12a7b4186 100644 --- a/qtfred/help-src/doc/general/Toolbars.html +++ b/qtfred/help-src/doc/general/Toolbars.html @@ -134,15 +134,16 @@

Tools

Object creation dropdowns

-

Two dropdowns at the right end of the toolbar control what is created when you -click in the viewport.

+

Three dropdowns at the right end of the toolbar control what is created when you +click in the viewport. Each is paired with its own modifier chord, so you can place +ships, props, and waypoints or jump nodes intermixed without switching modes.

- + @@ -150,6 +151,13 @@

Object creation dropdowns

+ + + + +
DropdownUsed byDescription
Ships Ctrl+clickSelects the ship class, waypoint, or jump node to place. The object - created on Ctrl+click is determined by this selection.Selects the ship class to place. The ship created on Ctrl+click is + determined by this selection.
Props Selects the prop class to place. The prop created on Ctrl+Shift+click is determined by this selection.
OtherCtrl+Alt+clickSelects what non-ship, non-prop object to place - Waypoint or + Jump Node. The object created on Ctrl+Alt+click is determined by this + selection.

Context Bar

diff --git a/qtfred/help-src/doc/general/Viewport.html b/qtfred/help-src/doc/general/Viewport.html index 28ce1eb5626..e0cf299a74b 100644 --- a/qtfred/help-src/doc/general/Viewport.html +++ b/qtfred/help-src/doc/general/Viewport.html @@ -12,11 +12,12 @@

Viewport

Creating objects

The toolbar dropdowns determine what type of object is created when you click in -the viewport.

+the viewport. Each dropdown is paired with its own modifier chord:

    -
  • Ctrl+click - creates a ship, waypoint, or jump node, - depending on the toolbar selection.
  • -
  • Ctrl+Shift+click - creates a prop.
  • +
  • Ctrl+click - creates a ship from the Ships dropdown.
  • +
  • Ctrl+Shift+click - creates a prop from the Props dropdown.
  • +
  • Ctrl+Alt+click - creates a waypoint or jump node from the + Other dropdown.

Selecting objects

diff --git a/qtfred/src/mission/Editor.cpp b/qtfred/src/mission/Editor.cpp index 87aa811a3a2..32ee0ad5a15 100644 --- a/qtfred/src/mission/Editor.cpp +++ b/qtfred/src/mission/Editor.cpp @@ -656,8 +656,6 @@ void Editor::clearMission(bool fast_reload) { } void Editor::initialSetup() { - Id_select_type_waypoint = static_cast(Ship_info.size()); - Id_select_type_jump_node = static_cast(Ship_info.size() + 1); } void Editor::setupCurrentObjectIndices(int selectedObj) { diff --git a/qtfred/src/mission/Editor.h b/qtfred/src/mission/Editor.h index 08636d1d70c..e4fca653382 100644 --- a/qtfred/src/mission/Editor.h +++ b/qtfred/src/mission/Editor.h @@ -189,9 +189,6 @@ class Editor : public QObject { */ bool autoload(); - int Id_select_type_jump_node = 0; - int Id_select_type_waypoint = 0; - // object numbers for ships in a wing. int wing_objects[MAX_WINGS][MAX_SHIPS_PER_WING]; diff --git a/qtfred/src/mission/EditorViewport.cpp b/qtfred/src/mission/EditorViewport.cpp index 5cd10840271..28a916b27e8 100644 --- a/qtfred/src/mission/EditorViewport.cpp +++ b/qtfred/src/mission/EditorViewport.cpp @@ -1094,12 +1094,12 @@ void EditorViewport::drag_rotate_save_backup() { } int EditorViewport::create_object_on_grid(int x, int y, int waypoint_instance) { - return create_object_on_grid(x, y, waypoint_instance, false); + return create_object_on_grid(x, y, waypoint_instance, CreateKind::Ship); } -int EditorViewport::create_object_on_grid(int x, int y, int waypoint_instance, bool create_prop) { +int EditorViewport::create_object_on_grid(int x, int y, int waypoint_instance, CreateKind kind) { float fallbackDist = 200.0f; - if (create_prop) { + if (kind == CreateKind::Prop) { if (cur_prop_index >= 0 && cur_prop_index < prop_info_size()) { prop_info* pip = &Prop_info[cur_prop_index]; if (pip->model_num >= 0) { @@ -1112,16 +1112,14 @@ int EditorViewport::create_object_on_grid(int x, int y, int waypoint_instance, b } } } - } else if (cur_model_index >= 0 && cur_model_index < (int)Ship_info.size() && - cur_model_index != editor->Id_select_type_waypoint && - cur_model_index != editor->Id_select_type_jump_node && + } else if (kind == CreateKind::Ship && cur_model_index >= 0 && cur_model_index < (int)Ship_info.size() && Ship_info[cur_model_index].model_num >= 0) { fallbackDist = model_get_radius(Ship_info[cur_model_index].model_num) * 1.5f; } vec3d pos = getCreatePosition(x, y, fallbackDist); editor->unmark_all(); - int obj = create_object(&pos, waypoint_instance, create_prop); + int obj = create_object(&pos, waypoint_instance, kind); if (obj >= 0) { editor->markObject(obj); @@ -1134,10 +1132,10 @@ int EditorViewport::create_object_on_grid(int x, int y, int waypoint_instance, b return obj; } -int EditorViewport::create_object(vec3d* pos, int waypoint_instance, bool create_prop) { +int EditorViewport::create_object(vec3d* pos, int waypoint_instance, CreateKind kind) { int obj, n; - if (create_prop) { + if (kind == CreateKind::Prop) { if (cur_prop_index < 0 || cur_prop_index >= prop_info_size()) { return -1; } @@ -1146,17 +1144,26 @@ int EditorViewport::create_object(vec3d* pos, int waypoint_instance, bool create if (obj == -1) { return -1; } - } else { - - if (cur_model_index == editor->Id_select_type_waypoint) { + } else if (kind == CreateKind::Other) { + switch (cur_other_kind) { + case OtherKind::Waypoint: obj = editor->create_waypoint(pos, waypoint_instance); - } else if (cur_model_index == editor->Id_select_type_jump_node) { + break; + case OtherKind::JumpNode: { CJumpNode jnp(pos); obj = jnp.GetSCPObjectNumber(); Jump_nodes.push_back(std::move(jnp)); - } else if(Ship_info[cur_model_index].flags[Ship::Info_Flags::No_fred]){ + break; + } + default: obj = -1; - } else { // creating a ship + break; + } + } else { // CreateKind::Ship + if (cur_model_index < 0 || cur_model_index >= (int)Ship_info.size() || + Ship_info[cur_model_index].flags[Ship::Info_Flags::No_fred]) { + obj = -1; + } else { obj = editor->create_ship(nullptr, pos, cur_model_index); if (obj == -1) return -1; @@ -1193,7 +1200,7 @@ int EditorViewport::createShipAtScreenPos(int x, int y, int modelIndex) { } int savedModelIndex = cur_model_index; cur_model_index = modelIndex; - int obj = create_object_on_grid(x, y, -1, false); + int obj = create_object_on_grid(x, y, -1, CreateKind::Ship); cur_model_index = savedModelIndex; return obj; } @@ -1205,29 +1212,30 @@ int EditorViewport::createPropAtScreenPos(int x, int y, int propIndex) { } int savedPropIndex = cur_prop_index; cur_prop_index = propIndex; - int obj = create_object_on_grid(x, y, -1, true); + int obj = create_object_on_grid(x, y, -1, CreateKind::Prop); cur_prop_index = savedPropIndex; return obj; } int EditorViewport::createWaypointAtScreenPos(int x, int y, int waypoint_instance) { - int savedModelIndex = cur_model_index; - cur_model_index = editor->Id_select_type_waypoint; - int obj = create_object_on_grid(x, y, waypoint_instance, false); - cur_model_index = savedModelIndex; + OtherKind savedKind = cur_other_kind; + cur_other_kind = OtherKind::Waypoint; + int obj = create_object_on_grid(x, y, waypoint_instance, CreateKind::Other); + cur_other_kind = savedKind; return obj; } int EditorViewport::createJumpNodeAtScreenPos(int x, int y) { - int savedModelIndex = cur_model_index; - cur_model_index = editor->Id_select_type_jump_node; - int obj = create_object_on_grid(x, y, -1, false); - cur_model_index = savedModelIndex; + OtherKind savedKind = cur_other_kind; + cur_other_kind = OtherKind::JumpNode; + int obj = create_object_on_grid(x, y, -1, CreateKind::Other); + cur_other_kind = savedKind; return obj; } void EditorViewport::initialSetup() { cur_model_index = get_default_player_ship_index(); + cur_other_kind = OtherKind::Waypoint; for (int i = 0; i < prop_info_size(); ++i) { if (!Prop_info[i].flags[Prop::Info_Flags::No_fred]) { cur_prop_index = i; diff --git a/qtfred/src/mission/EditorViewport.h b/qtfred/src/mission/EditorViewport.h index bd12eae0624..ec4f4cd6850 100644 --- a/qtfred/src/mission/EditorViewport.h +++ b/qtfred/src/mission/EditorViewport.h @@ -17,6 +17,17 @@ struct Marking_box { int y2 = 0; }; +enum class CreateKind { + Ship, + Prop, + Other, +}; + +enum class OtherKind { + Waypoint, + JumpNode, +}; + struct ViewSettings { bool Universal_heading = false; bool Show_stars = true; @@ -129,9 +140,9 @@ class EditorViewport { void drag_rotate_save_backup(); int create_object_on_grid(int x, int y, int waypoint_instance); - int create_object_on_grid(int x, int y, int waypoint_instance, bool create_prop); + int create_object_on_grid(int x, int y, int waypoint_instance, CreateKind kind); - int create_object(vec3d *pos, int waypoint_instance = -1, bool create_prop = false); + int create_object(vec3d *pos, int waypoint_instance = -1, CreateKind kind = CreateKind::Ship); vec3d getCreatePosition(int x, int y, float fallbackDist); int createShipAtScreenPos(int x, int y, int modelIndex); @@ -170,6 +181,7 @@ class EditorViewport { int cur_model_index = 0; int cur_prop_index = -1; + OtherKind cur_other_kind = OtherKind::Waypoint; object_orient_pos rotation_backup[MAX_OBJECTS]; diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index de12a4aff5e..7a7a240da02 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -187,7 +187,8 @@ void FredView::setEditor(Editor* editor, EditorViewport* viewport) { ui->toolBar->addWidget(shipsLabel); _shipClassBox = new ObjectComboBox(ui->toolBar); _shipClassBox->setFixedWidth(150); - _shipClassBox->initForShips(_viewport); + _shipClassBox->setToolTip(tr("Ctrl+click in the viewport to place")); + _shipClassBox->initForShips(); ui->toolBar->addWidget(_shipClassBox); connect(_shipClassBox, &ObjectComboBox::classSelected, this, &FredView::onShipClassSelected); @@ -196,10 +197,21 @@ void FredView::setEditor(Editor* editor, EditorViewport* viewport) { ui->toolBar->addWidget(propsLabel); _propClassBox = new ObjectComboBox(ui->toolBar); _propClassBox->setFixedWidth(150); + _propClassBox->setToolTip(tr("Ctrl+Shift+click in the viewport to place")); _propClassBox->initForProps(); ui->toolBar->addWidget(_propClassBox); connect(_propClassBox, &ObjectComboBox::classSelected, this, &FredView::onPropClassSelected); + auto otherLabel = new QLabel(tr("Other: "), ui->toolBar); + otherLabel->setContentsMargins(4, 0, 0, 0); + ui->toolBar->addWidget(otherLabel); + _otherClassBox = new ObjectComboBox(ui->toolBar); + _otherClassBox->setFixedWidth(150); + _otherClassBox->setToolTip(tr("Ctrl+Alt+click in the viewport to place")); + _otherClassBox->initForOther(); + ui->toolBar->addWidget(_otherClassBox); + connect(_otherClassBox, &ObjectComboBox::classSelected, this, &FredView::onOtherKindSelected); + initializeContextToolbar(); initializeTransformBar(); @@ -228,6 +240,7 @@ void FredView::setEditor(Editor* editor, EditorViewport* viewport) { connect(this, &FredView::viewIdle, this, &FredView::onUpdateSelectionLock); connect(this, &FredView::viewIdle, this, &FredView::onUpdateShipClassBox); connect(this, &FredView::viewIdle, this, &FredView::onUpdatePropClassBox); + connect(this, &FredView::viewIdle, this, &FredView::onUpdateOtherClassBox); connect(this, &FredView::viewIdle, this, &FredView::onUpdateEditorActions); connect(this, &FredView::viewIdle, this, &FredView::onUpdateWingActionStatus); connect(this, &FredView::viewIdle, this, &FredView::onUpdateContextToolbar); @@ -1876,7 +1889,9 @@ void FredView::initializePopupMenus() { }); _createSubmenu->addMenu(_createPropSubmenu); - auto* createWaypointAction = new QAction(tr("Waypoint"), _createSubmenu); + auto* createOtherSubmenu = new QMenu(tr("Other"), _createSubmenu); + + auto* createWaypointAction = new QAction(tr("Waypoint"), createOtherSubmenu); connect(createWaypointAction, &QAction::triggered, this, [this]() { int waypoint_instance = -1; if (fred->cur_waypoint != nullptr) { @@ -1884,13 +1899,15 @@ void FredView::initializePopupMenus() { } _viewport->createWaypointAtScreenPos(_lastContextMenuLocalPos.x(), _lastContextMenuLocalPos.y(), waypoint_instance); }); - _createSubmenu->addAction(createWaypointAction); + createOtherSubmenu->addAction(createWaypointAction); - auto* createJumpNodeAction = new QAction(tr("Jump Node"), _createSubmenu); + auto* createJumpNodeAction = new QAction(tr("Jump Node"), createOtherSubmenu); connect(createJumpNodeAction, &QAction::triggered, this, [this]() { _viewport->createJumpNodeAtScreenPos(_lastContextMenuLocalPos.x(), _lastContextMenuLocalPos.y()); }); - _createSubmenu->addAction(createJumpNodeAction); + createOtherSubmenu->addAction(createJumpNodeAction); + + _createSubmenu->addMenu(createOtherSubmenu); _viewPopup->addMenu(_createSubmenu); _viewPopup->addSeparator(); @@ -2349,12 +2366,18 @@ void FredView::onUpdatePropClassBox() { } _propClassBox->selectClass(_viewport->cur_prop_index); } +void FredView::onUpdateOtherClassBox() { + _otherClassBox->selectClass(static_cast(_viewport->cur_other_kind)); +} void FredView::onShipClassSelected(int ship_class) { _viewport->cur_model_index = ship_class; } void FredView::onPropClassSelected(int prop_class) { _viewport->cur_prop_index = prop_class; } +void FredView::onOtherKindSelected(int other_kind) { + _viewport->cur_other_kind = static_cast(other_kind); +} void FredView::on_actionAsteroid_Field_triggered(bool) { auto asteroidFieldEditor = new dialogs::AsteroidEditorDialog(this, _viewport); asteroidFieldEditor->setAttribute(Qt::WA_DeleteOnClose); diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index d6b3697c5da..f313294d068 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -277,6 +277,7 @@ class FredView: public QMainWindow, public IDialogProvider { ObjectComboBox* _shipClassBox = nullptr; ObjectComboBox* _propClassBox = nullptr; + ObjectComboBox* _otherClassBox = nullptr; Editor* fred = nullptr; EditorViewport* _viewport = nullptr; @@ -297,6 +298,7 @@ class FredView: public QMainWindow, public IDialogProvider { void onUpdateSelectionLock(); void onUpdateShipClassBox(); void onUpdatePropClassBox(); + void onUpdateOtherClassBox(); void onUpdateEditorActions(); void onUpdateWingActionStatus(); @@ -341,6 +343,7 @@ class FredView: public QMainWindow, public IDialogProvider { void onShipClassSelected(int ship_class); void onPropClassSelected(int prop_class); + void onOtherKindSelected(int other_kind); void windowActivated(); void windowDeactivated(); diff --git a/qtfred/src/ui/widgets/ObjectComboBox.cpp b/qtfred/src/ui/widgets/ObjectComboBox.cpp index b7f21b607cf..7c076978efa 100644 --- a/qtfred/src/ui/widgets/ObjectComboBox.cpp +++ b/qtfred/src/ui/widgets/ObjectComboBox.cpp @@ -22,8 +22,7 @@ ObjectComboBox::ObjectComboBox(QWidget* parent) : QComboBox(parent) { &ObjectComboBox::indexChanged); } -void ObjectComboBox::initForShips(EditorViewport* viewport) { - _viewport = viewport; +void ObjectComboBox::initForShips() { fredApp->runAfterInit([this]() { buildShipsModel(); }); @@ -35,6 +34,12 @@ void ObjectComboBox::initForProps() { }); } +void ObjectComboBox::initForOther() { + fredApp->runAfterInit([this]() { + buildOtherModel(); + }); +} + void ObjectComboBox::buildShipsModel() { auto model = new QStandardItemModel(); @@ -50,16 +55,18 @@ void ObjectComboBox::buildShipsModel() { model->appendRow(item); } - auto separator = new QStandardItem(); - separator->setData("separator", Qt::AccessibleDescriptionRole); - model->appendRow(separator); + setModel(model); +} + +void ObjectComboBox::buildOtherModel() { + auto model = new QStandardItemModel(); auto waypoint = new QStandardItem("Waypoint"); - waypoint->setData(_viewport->editor->Id_select_type_waypoint, Qt::UserRole); + waypoint->setData(static_cast(OtherKind::Waypoint), Qt::UserRole); model->appendRow(waypoint); auto jumpNode = new QStandardItem("Jump Node"); - jumpNode->setData(_viewport->editor->Id_select_type_jump_node, Qt::UserRole); + jumpNode->setData(static_cast(OtherKind::JumpNode), Qt::UserRole); model->appendRow(jumpNode); setModel(model); diff --git a/qtfred/src/ui/widgets/ObjectComboBox.h b/qtfred/src/ui/widgets/ObjectComboBox.h index f1f67a8a5d5..21e286ab40c 100644 --- a/qtfred/src/ui/widgets/ObjectComboBox.h +++ b/qtfred/src/ui/widgets/ObjectComboBox.h @@ -10,13 +10,12 @@ namespace fso::fred { class ObjectComboBox : public QComboBox { Q_OBJECT - EditorViewport* _viewport = nullptr; - public: explicit ObjectComboBox(QWidget* parent = nullptr); - void initForShips(EditorViewport* viewport); + void initForShips(); void initForProps(); + void initForOther(); void selectClass(int class_index); @@ -26,6 +25,7 @@ class ObjectComboBox : public QComboBox { private: void buildShipsModel(); void buildPropsModel(); + void buildOtherModel(); void indexChanged(int index); }; diff --git a/qtfred/src/ui/widgets/renderwidget.cpp b/qtfred/src/ui/widgets/renderwidget.cpp index 481993bb91f..7cc6cc8752e 100644 --- a/qtfred/src/ui/widgets/renderwidget.cpp +++ b/qtfred/src/ui/widgets/renderwidget.cpp @@ -229,8 +229,15 @@ void RenderWidget::mousePressEvent(QMouseEvent* event) { if (event->modifiers().testFlag(Qt::ControlModifier)) { // add a new object if (_viewport->on_object == -1) { _viewport->Selection_lock = false; // force off selection lock - auto spawn_prop = event->modifiers().testFlag(Qt::ShiftModifier); - _viewport->on_object = _viewport->create_object_on_grid(event->x(), event->y(), waypoint_instance, spawn_prop); + const bool shift = event->modifiers().testFlag(Qt::ShiftModifier); + const bool alt = event->modifiers().testFlag(Qt::AltModifier); + auto kind = CreateKind::Ship; + if (alt && !shift) { + kind = CreateKind::Other; + } else if (shift && !alt) { + kind = CreateKind::Prop; + } + _viewport->on_object = _viewport->create_object_on_grid(event->x(), event->y(), waypoint_instance, kind); } else { _viewport->Dup_drag = 1; From 15e7528d410d6f7bef28ba1aa21d7da7fbcfa58e Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Sun, 17 May 2026 22:34:19 -0400 Subject: [PATCH 59/65] Expose new `Max Guard Range` to scripting (#7451) Follow-up to #7251, which added `set-guard-range` This PR exposes that value to scripting, so scripters do not have to use the more expensive `runSEXP` to use it. The value is initialized as -1 (`max_guard_radius = -1.0f;` line 7913 in ship.cpp), hence the default value for the script is -1. --- code/scripting/api/objs/ship.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/code/scripting/api/objs/ship.cpp b/code/scripting/api/objs/ship.cpp index 3af4baf9697..77d9e3234a9 100644 --- a/code/scripting/api/objs/ship.cpp +++ b/code/scripting/api/objs/ship.cpp @@ -1162,6 +1162,24 @@ ADE_VIRTVAR(Orders, l_Ship, "shiporders", "Array of ship orders", "shiporders", return ade_set_args(L, "o", l_ShipOrders.Set(object_h(objh->objp()))); } +ADE_VIRTVAR(MaxGuardRadius, l_Ship, "number", "Sets the max range in meters at which any ships guarding this ship will engage with threats. If the value is <= 0, regular dynamic guard range behavior will resume.", "number", "Max range in meters, or 0 if handle is invalid") +{ + object_h *objh; + float new_max_guard_radius = -1; + if (!ade_get_args(L, "o|f", l_Ship.GetPtr(&objh), &new_max_guard_radius)) + return ade_set_error(L, "f", 0.0f); + + if(!objh->isValid()) + return ade_set_error(L, "f", 0.0f); + + ship *shipp = &Ships[objh->objp()->instance]; + + if (ADE_SETTING_VAR) + shipp->max_guard_radius = new_max_guard_radius; + + return ade_set_args(L, "f", shipp->max_guard_radius); +} + ADE_VIRTVAR(WaypointSpeedCap, l_Ship, "number", "Waypoint speed cap", "number", "The limit on the ship's speed for traversing waypoints. -1 indicates no speed cap. 0 will be returned if handle is invalid.") { object_h* objh; From c8ea9c6035d5c3c59ad1ba93bcc942edaf1e2a8f Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Mon, 18 May 2026 06:23:41 +0200 Subject: [PATCH 60/65] Model surface particles (#7447) * Pick model surface particle by sampling random vertex * Parse and keep buffers * Add local position scaling * Fix warning * Fix warning 2 * Fix warning 3 * Fix warning 4 * Add scale parameter * Incorporate feedback --- code/model/model_flags.h | 1 + code/model/modelread.cpp | 8 ++- code/model/modelrender.h | 2 + code/particle/EffectHost.h | 1 + code/particle/ParticleEffect.cpp | 29 ++++++++- code/particle/ParticleEffect.h | 2 + code/particle/ParticleParse.cpp | 17 +++++- code/particle/ParticleVolume.h | 3 +- code/particle/hosts/EffectHostSubmodel.cpp | 8 ++- code/particle/hosts/EffectHostSubmodel.h | 3 +- code/particle/hosts/EffectHostTurret.cpp | 4 ++ code/particle/hosts/EffectHostTurret.h | 1 + code/particle/volumes/ConeVolume.cpp | 2 +- code/particle/volumes/ConeVolume.h | 2 +- .../particle/volumes/LegacyAACuboidVolume.cpp | 2 +- code/particle/volumes/LegacyAACuboidVolume.h | 2 +- code/particle/volumes/ModelSurfaceVolume.cpp | 61 +++++++++++++++++++ code/particle/volumes/ModelSurfaceVolume.h | 31 ++++++++++ code/particle/volumes/PointVolume.cpp | 2 +- code/particle/volumes/PointVolume.h | 2 +- code/particle/volumes/RingVolume.cpp | 2 +- code/particle/volumes/RingVolume.h | 2 +- code/particle/volumes/SpheroidVolume.cpp | 2 +- code/particle/volumes/SpheroidVolume.h | 2 +- code/source_groups.cmake | 2 + 25 files changed, 173 insertions(+), 20 deletions(-) create mode 100644 code/particle/volumes/ModelSurfaceVolume.cpp create mode 100644 code/particle/volumes/ModelSurfaceVolume.h diff --git a/code/model/model_flags.h b/code/model/model_flags.h index aba8da8a1d5..268f0a8e437 100644 --- a/code/model/model_flags.h +++ b/code/model/model_flags.h @@ -10,6 +10,7 @@ namespace Model { Is_live_debris, // whether current submodel is a live debris model Is_thruster, // is an engine thruster submodel Is_damaged, // is a submodel that represents a damaged submodel (e.g. a -destroyed version of some other submodel) + Is_lod, // is a submodel that is a lower LOD of a different submodel Do_not_scale_detail_distances, // if set should not scale boxes or spheres based on 'model detail' settings Gun_rotation, // for animated weapon models Instant_rotate_accel, // rotating submodels instantly reach their desired velocity diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index 95de185af92..facc019f05f 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -72,6 +72,10 @@ SCP_vector Bsp_collision_tree_list; const ubyte* Macro_ubyte_bounds = nullptr; +//If true, CPU-side vertex buffers are deleted once the model is on-GPU. +//This is typically desired for memory reasons, but will prevent certain type of particles. +bool Model_load_clear_CPU_buffers = true; + static int model_initted = 0; #ifndef NDEBUG @@ -1138,7 +1142,8 @@ void create_vertex_buffer(polymodel *pm, const model_read_deferred_tasks& deferr interp_pack_vertex_buffers(pm, i); // release temporary memory - pm->submodel[i].buffer.release(); + if (Model_load_clear_CPU_buffers) + pm->submodel[i].buffer.release(); pm->submodel[i].trans_buffer.release(); } @@ -3447,6 +3452,7 @@ int model_load(const char* filename, ship_info* sip, ErrorType error_type, bool if (dl2 >= sm1->num_details ) sm1->num_details = dl2+1; sm1->details[dl2] = j; mprintf(( "Submodel '%s' is detail level %d of '%s'\n", sm2->name, dl2 + 1, sm1->name )); + sm2->flags.set(Model::Submodel_flags::Is_lod); lower_to_higher_detail_submodels.emplace(sm2->name, sm1->name); } } diff --git a/code/model/modelrender.h b/code/model/modelrender.h index 5064b3a35fa..c780226d347 100644 --- a/code/model/modelrender.h +++ b/code/model/modelrender.h @@ -29,6 +29,8 @@ extern color Wireframe_color; extern int Lab_object_detail_level; +extern bool Model_load_clear_CPU_buffers; + typedef enum { TECH_SHIP, TECH_WEAPON, diff --git a/code/particle/EffectHost.h b/code/particle/EffectHost.h index fa25ffb155a..9bdea7296d8 100644 --- a/code/particle/EffectHost.h +++ b/code/particle/EffectHost.h @@ -27,6 +27,7 @@ class EffectHost { } virtual std::pair getParentObjAndSig() const { return {-1, -1}; } + virtual int getParentSubmodel() const { return -1; } virtual float getLifetime() const { return -1.f; } diff --git a/code/particle/ParticleEffect.cpp b/code/particle/ParticleEffect.cpp index 36252794c59..7a69bd42e59 100644 --- a/code/particle/ParticleEffect.cpp +++ b/code/particle/ParticleEffect.cpp @@ -185,6 +185,28 @@ void ParticleEffect::sampleNoise(vec3d& noiseTarget, const matrix* orientation, vm_vec_unrotate(&noiseTarget, &noiseSampleLocal, orientation); } +vec3d ParticleEffect::adaptPosition(const vec3d& pos, int parent) const { + if (parent < 0 || !m_local_position_scaling.has_value()) { + return pos; + } + + vec3d pos_local = pos; + + if (!m_parent_local) { + pos_local -= Objects[parent].pos; + vm_vec_rotate(&pos_local, &pos_local, &Objects[parent].orient); + } + + pos_local *= m_local_position_scaling->next(); + + if (!m_parent_local) { + vm_vec_unrotate(&pos_local, &pos_local, &Objects[parent].orient); + vm_vec_add2(&pos_local, &Objects[parent].pos); + } + + return pos_local; +} + /* * In persistent mode (should only ever be used by scripting, really), this function returns pointers to the persistent particles * In non-persistent mode, this function returns the multiplier for the next spawn time. This is because the source cannot know about the curve evaluation that is required to get this factor @@ -213,7 +235,8 @@ auto ParticleEffect::processSourceInternal(float interp, const ParticleSource& s } } - const auto& [pos, hostOrientation] = source.m_host->getPositionAndOrientation(m_parent_local, interp, m_manual_offset); + const auto& [pos_hit, hostOrientation] = source.m_host->getPositionAndOrientation(m_parent_local, interp, m_manual_offset); + const vec3d& pos = adaptPosition(pos_hit, parent); vec3d posGlobal = pos; if (m_parent_local && parent >= 0) { @@ -290,11 +313,11 @@ auto ParticleEffect::processSourceInternal(float interp, const ParticleSource& s vec3d localPos = posNoise; if (m_spawnVolume != nullptr) { - localPos += m_spawnVolume->sampleRandomPoint(orientation, modularCurvesInput, particleFraction); + localPos += m_spawnVolume->sampleRandomPoint(orientation, modularCurvesInput, particleFraction, *source.m_host); } if (m_velocityVolume != nullptr) { - localVelocity += m_velocityVolume->sampleRandomPoint(orientation, modularCurvesInput, particleFraction) * (m_velocity_scaling.next() * velocityVolumeMultiplier); + localVelocity += m_velocityVolume->sampleRandomPoint(orientation, modularCurvesInput, particleFraction, *source.m_host) * (m_velocity_scaling.next() * velocityVolumeMultiplier); } if (m_manual_velocity_offset.has_value()) { diff --git a/code/particle/ParticleEffect.h b/code/particle/ParticleEffect.h index f44e971baf3..35708d813fd 100644 --- a/code/particle/ParticleEffect.h +++ b/code/particle/ParticleEffect.h @@ -168,6 +168,7 @@ class ParticleEffect { std::optional<::util::ParsedRandomFloatRange> m_vel_inherit_from_orientation; std::optional<::util::ParsedRandomFloatRange> m_vel_inherit_from_position; + std::optional<::util::ParsedRandomFloatRange> m_local_position_scaling; std::shared_ptr<::particle::ParticleVolume> m_velocityVolume; std::shared_ptr<::particle::ParticleVolume> m_spawnVolume; @@ -186,6 +187,7 @@ class ParticleEffect { float m_distanceCulled; //Kinda deprecated. Only used by the oldest of legacy effects. matrix getNewDirection(const matrix& hostOrientation, const std::optional& normal) const; + vec3d adaptPosition(const vec3d& pos, int parent) const; template auto processSourceInternal(float interp, const ParticleSource& source, size_t effectNumber, const vec3d& velParent, int parent, int parent_sig, float parentLifetime, float parentRadius, float particle_percent) const; diff --git a/code/particle/ParticleParse.cpp b/code/particle/ParticleParse.cpp index 243e90fb0c4..3a619e30451 100644 --- a/code/particle/ParticleParse.cpp +++ b/code/particle/ParticleParse.cpp @@ -7,6 +7,8 @@ #include +#include "volumes/ModelSurfaceVolume.h" + namespace particle { // @@ -92,6 +94,12 @@ namespace particle { } } + static void parseLocalPositionScaling(ParticleEffect& effect) { + if (optional_string("+Local position scaling:")) { + effect.m_local_position_scaling = ::util::ParsedRandomFloatRange::parseRandomRange(); + } + } + static void parseParentLocal(ParticleEffect& effect) { if (optional_string("+Remain local to parent:")) { stuff_boolean(&effect.m_parent_local); @@ -125,7 +133,7 @@ namespace particle { static std::shared_ptr parseVolume() { - int type = required_string_one_of(4, "Spheroid", "Cone", "Ring", "Point"); //... and future volumes + int type = required_string_one_of(5, "Spheroid", "Cone", "Ring", "Point", "ModelSurface"); //... and future volumes std::shared_ptr volume; switch (type) { @@ -145,6 +153,10 @@ namespace particle { required_string("Point"); volume = std::make_shared(); break; + case 4: + required_string("ModelSurface"); + volume = std::make_shared(); + break; default: UNREACHABLE("Invalid volume type specified!"); } @@ -372,6 +384,7 @@ namespace particle { parseRadius(effect); parseLength(effect); parseLifetime(effect); + parseLocalPositionScaling(effect); parseParentLocal(effect); parseLightEmissionSettings(effect); @@ -723,4 +736,4 @@ namespace particle { "Sphere", "Volume" }; -} \ No newline at end of file +} diff --git a/code/particle/ParticleVolume.h b/code/particle/ParticleVolume.h index c03cfaf55b2..8012239bcdf 100644 --- a/code/particle/ParticleVolume.h +++ b/code/particle/ParticleVolume.h @@ -2,6 +2,7 @@ #include "globalincs/pstypes.h" #include "parse/parselo.h" +#include "particle/EffectHost.h" #include @@ -9,7 +10,7 @@ namespace particle { class ParticleSource; class ParticleVolume { public: - virtual vec3d sampleRandomPoint(const matrix &orientation, const std::tuple& source, float particlesFraction) = 0; + virtual vec3d sampleRandomPoint(const matrix &orientation, const std::tuple& source, float particlesFraction, const EffectHost& host) = 0; virtual void parse() = 0; diff --git a/code/particle/hosts/EffectHostSubmodel.cpp b/code/particle/hosts/EffectHostSubmodel.cpp index 98eec5a828c..39e332f6b3c 100644 --- a/code/particle/hosts/EffectHostSubmodel.cpp +++ b/code/particle/hosts/EffectHostSubmodel.cpp @@ -5,7 +5,7 @@ #include "ship/ship.h" EffectHostSubmodel::EffectHostSubmodel(const object* objp, int submodel, vec3d offset, matrix orientationOverride, bool orientationOverrideRelative) : - EffectHost(orientationOverride, orientationOverrideRelative), m_offset(offset), m_objnum(OBJ_INDEX(objp)), m_objsig(objp->signature), m_submodel(submodel) {} + EffectHost(orientationOverride, orientationOverrideRelative), m_offset(offset), m_objnum(OBJ_INDEX(objp)), m_objsig(objp->signature), m_submodel(submodel), m_modelnum(object_get_model(&Objects[m_objnum])->id) {} std::pair EffectHostSubmodel::getPositionAndOrientation(bool relativeToParent, float interp, const std::optional& tabled_offset) const { vec3d pos = m_offset; @@ -60,10 +60,14 @@ std::pair EffectHostSubmodel::getParentObjAndSig() const { return { m_objnum, m_objsig }; } +int EffectHostSubmodel::getParentSubmodel() const { + return m_submodel; +} + float EffectHostSubmodel::getHostRadius() const { return Objects[m_objnum].radius; } bool EffectHostSubmodel::isValid() const { - return m_objnum >= 0 && m_submodel >= 0 && Objects[m_objnum].signature == m_objsig && object_get_model_num(&Objects[m_objnum]) >= 0 && object_get_model_instance_num(&Objects[m_objnum]) >= 0; + return m_objnum >= 0 && m_submodel >= 0 && Objects[m_objnum].signature == m_objsig && object_get_model_num(&Objects[m_objnum]) >= 0 && object_get_model_instance_num(&Objects[m_objnum]) >= 0 && object_get_model(&Objects[m_objnum])->id == m_modelnum; } \ No newline at end of file diff --git a/code/particle/hosts/EffectHostSubmodel.h b/code/particle/hosts/EffectHostSubmodel.h index 860774976d4..ec7c1be67ef 100644 --- a/code/particle/hosts/EffectHostSubmodel.h +++ b/code/particle/hosts/EffectHostSubmodel.h @@ -7,7 +7,7 @@ class EffectHostSubmodel : public EffectHost { vec3d m_offset; - int m_objnum, m_objsig, m_submodel; + int m_objnum, m_objsig, m_submodel, m_modelnum; public: EffectHostSubmodel(const object* objp, int submodel, vec3d offset, matrix orientationOverride = vmd_identity_matrix, bool orientationOverrideRelative = true); @@ -16,6 +16,7 @@ class EffectHostSubmodel : public EffectHost { vec3d getVelocity() const override; std::pair getParentObjAndSig() const override; + int getParentSubmodel() const override; float getHostRadius() const override; diff --git a/code/particle/hosts/EffectHostTurret.cpp b/code/particle/hosts/EffectHostTurret.cpp index 4613fd564d6..747e4dbf519 100644 --- a/code/particle/hosts/EffectHostTurret.cpp +++ b/code/particle/hosts/EffectHostTurret.cpp @@ -74,6 +74,10 @@ std::pair EffectHostTurret::getParentObjAndSig() const { return { m_objnum, m_objsig }; } +int EffectHostTurret::getParentSubmodel() const { + return m_submodel; +} + float EffectHostTurret::getHostRadius() const { return Objects[m_objnum].radius; } diff --git a/code/particle/hosts/EffectHostTurret.h b/code/particle/hosts/EffectHostTurret.h index 2f4c020972d..112486199db 100644 --- a/code/particle/hosts/EffectHostTurret.h +++ b/code/particle/hosts/EffectHostTurret.h @@ -15,6 +15,7 @@ class EffectHostTurret : public EffectHost { vec3d getVelocity() const override; std::pair getParentObjAndSig() const override; + int getParentSubmodel() const override; float getHostRadius() const override; diff --git a/code/particle/volumes/ConeVolume.cpp b/code/particle/volumes/ConeVolume.cpp index d9468353b6d..fb9f34f0152 100644 --- a/code/particle/volumes/ConeVolume.cpp +++ b/code/particle/volumes/ConeVolume.cpp @@ -6,7 +6,7 @@ namespace particle { ConeVolume::ConeVolume(::util::ParsedRandomFloatRange deviation, ::util::ParsedRandomFloatRange length) : m_deviation(deviation), m_length(length), m_modular_curve_instance(m_modular_curves.create_instance()) { } - vec3d ConeVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) { + vec3d ConeVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& /*host*/) { auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); //It is surely possible to do this more efficiently. diff --git a/code/particle/volumes/ConeVolume.h b/code/particle/volumes/ConeVolume.h index 5fc47de3c37..ff583bc73ac 100644 --- a/code/particle/volumes/ConeVolume.h +++ b/code/particle/volumes/ConeVolume.h @@ -27,7 +27,7 @@ namespace particle { explicit ConeVolume(::util::ParsedRandomFloatRange deviation, float length); explicit ConeVolume(::util::ParsedRandomFloatRange deviation, ::util::ParsedRandomFloatRange length); - vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) override; + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& host) override; void parse() override; }; } \ No newline at end of file diff --git a/code/particle/volumes/LegacyAACuboidVolume.cpp b/code/particle/volumes/LegacyAACuboidVolume.cpp index e48882357fd..ecacd0345ab 100644 --- a/code/particle/volumes/LegacyAACuboidVolume.cpp +++ b/code/particle/volumes/LegacyAACuboidVolume.cpp @@ -5,7 +5,7 @@ namespace particle { LegacyAACuboidVolume::LegacyAACuboidVolume(float normalVariance, float size, bool normalize) : m_normalVariance(normalVariance), m_size(size), m_normalize(normalize), m_modular_curve_instance(m_modular_curves.create_instance()) { } - vec3d LegacyAACuboidVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) { + vec3d LegacyAACuboidVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& /*host*/) { auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); float variance = m_normalVariance * m_modular_curves.get_output(VolumeModularCurveOutput::VARIANCE, curveSource, &m_modular_curve_instance); diff --git a/code/particle/volumes/LegacyAACuboidVolume.h b/code/particle/volumes/LegacyAACuboidVolume.h index ea98b99c1d0..4678630a4d3 100644 --- a/code/particle/volumes/LegacyAACuboidVolume.h +++ b/code/particle/volumes/LegacyAACuboidVolume.h @@ -28,7 +28,7 @@ namespace particle { public: explicit LegacyAACuboidVolume(float normalVariance, float size, bool normalize); - vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) override; + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& host) override; void parse() override { UNREACHABLE("Cannot parse Legacy Particle Volume!"); }; diff --git a/code/particle/volumes/ModelSurfaceVolume.cpp b/code/particle/volumes/ModelSurfaceVolume.cpp new file mode 100644 index 00000000000..55fcbc35ea2 --- /dev/null +++ b/code/particle/volumes/ModelSurfaceVolume.cpp @@ -0,0 +1,61 @@ +#include "ModelSurfaceVolume.h" + +#include "math/vecmat.h" + +namespace particle { +ModelSurfaceVolume::ModelSurfaceVolume() : m_modelScale(::util::UniformFloatRange(1.f)), m_modular_curve_instance(m_modular_curves.create_instance()) { + Model_load_clear_CPU_buffers = false; +}; + +vec3d ModelSurfaceVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& host) { + int obj_num = host.getParentObjAndSig().first; + int submodel = host.getParentSubmodel(); + + vec3d point = ZERO_VECTOR; + + auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); + + if (obj_num >= 0) { + const polymodel* pm = object_get_model(&Objects[obj_num]); + if (pm != nullptr) { + if (submodel < 0) { + SCP_vector eligible_submodels; + for (int i = 0; i < pm->n_models; ++i) { + if (!pm->submodel[i].flags[Model::Submodel_flags::Is_lod, Model::Submodel_flags::Is_damaged, Model::Submodel_flags::Is_live_debris]) + eligible_submodels.emplace_back(i); + } + + if (!eligible_submodels.empty()) + submodel = eligible_submodels[::util::UniformUIntRange(0U, static_cast(eligible_submodels.size()) - 1).next()]; + } + + if (submodel >= 0) { + const bsp_info* submodel_data = &pm->submodel[submodel]; + const auto* geometry_data = submodel_data->buffer.model_list; + Assertion(geometry_data != nullptr, "ModelSurfaceVolume particles were spawned on a model with no data available!"); + + size_t target_vertex = ::util::UniformUIntRange(0U, static_cast(geometry_data->n_verts) - 1).next(); + + //This point is, despite its name, not in world space, but in model local space (NOT submodel local though!) + point = geometry_data->vert[target_vertex].world; + + point *= m_modelScale.next() * m_modular_curves.get_output(VolumeModularCurveOutput::SCALE_MULT, curveSource, &m_modular_curve_instance); + } + } + } + + return pointCompensateForOffsetAndRotOffset(point, orientation, + m_modular_curves.get_output(VolumeModularCurveOutput::OFFSET_ROT, curveSource, &m_modular_curve_instance), + m_modular_curves.get_output(VolumeModularCurveOutput::POINT_TO_ROT, curveSource, &m_modular_curve_instance)); +} + +void ModelSurfaceVolume::parse() { + if (optional_string("+Scale:")) { + m_modelScale = ::util::ParsedRandomFloatRange::parseRandomRange(); + } + + ParticleVolume::parseCommon(); + + m_modular_curves.parse("$Volume Curve:"); +} +} diff --git a/code/particle/volumes/ModelSurfaceVolume.h b/code/particle/volumes/ModelSurfaceVolume.h new file mode 100644 index 00000000000..e780f36753c --- /dev/null +++ b/code/particle/volumes/ModelSurfaceVolume.h @@ -0,0 +1,31 @@ +#pragma once + +#include "particle/ParticleVolume.h" +#include "particle/ParticleEffect.h" + +namespace particle { +class ModelSurfaceVolume : public ParticleVolume { + ::util::ParsedRandomFloatRange m_modelScale; + enum class VolumeModularCurveOutput : uint8_t {SCALE_MULT, OFFSET_ROT, POINT_TO_ROT, NUM_VALUES}; + + constexpr static auto modular_curve_definition = ParticleEffect::modular_curves_definition.derive_modular_curves_subset( + std::array { + std::pair { "Scale Mult", VolumeModularCurveOutput::SCALE_MULT }, + std::pair { "Offset Rotate Around Fvec", VolumeModularCurveOutput::OFFSET_ROT }, + std::pair { "Point To Rotate Around Fvec", VolumeModularCurveOutput::POINT_TO_ROT } + }, + std::pair { "Fraction Particles Spawned", modular_curves_self_input{}}); + +public: + MODULAR_CURVE_SET(m_modular_curves, modular_curve_definition); + +private: + modular_curves_entry_instance m_modular_curve_instance; + +public: + explicit ModelSurfaceVolume(); + + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& host) override; + void parse() override; +}; +} \ No newline at end of file diff --git a/code/particle/volumes/PointVolume.cpp b/code/particle/volumes/PointVolume.cpp index 05c9b51dcf5..9d55062ca90 100644 --- a/code/particle/volumes/PointVolume.cpp +++ b/code/particle/volumes/PointVolume.cpp @@ -5,7 +5,7 @@ namespace particle { PointVolume::PointVolume() : m_modular_curve_instance(m_modular_curves.create_instance()) { }; - vec3d PointVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) { + vec3d PointVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& /*host*/) { auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); return pointCompensateForOffsetAndRotOffset(ZERO_VECTOR, orientation, diff --git a/code/particle/volumes/PointVolume.h b/code/particle/volumes/PointVolume.h index ccfbd52a7fa..6ea532496e8 100644 --- a/code/particle/volumes/PointVolume.h +++ b/code/particle/volumes/PointVolume.h @@ -25,7 +25,7 @@ namespace particle { public: explicit PointVolume(); - vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) override; + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& host) override; void parse() override; }; } \ No newline at end of file diff --git a/code/particle/volumes/RingVolume.cpp b/code/particle/volumes/RingVolume.cpp index b38f7119b82..63d69801e7b 100644 --- a/code/particle/volumes/RingVolume.cpp +++ b/code/particle/volumes/RingVolume.cpp @@ -6,7 +6,7 @@ namespace particle { RingVolume::RingVolume() : m_radius(1.f), m_onEdge(false), m_modular_curve_instance(m_modular_curves.create_instance()) { }; RingVolume::RingVolume(float radius, bool onEdge) : m_radius(radius), m_onEdge(onEdge), m_modular_curve_instance(m_modular_curves.create_instance()) { }; - vec3d RingVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) { + vec3d RingVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& /*host*/) { auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); vec3d pos; // get an unbiased random point in the sphere diff --git a/code/particle/volumes/RingVolume.h b/code/particle/volumes/RingVolume.h index bdb000ab212..e84d3746358 100644 --- a/code/particle/volumes/RingVolume.h +++ b/code/particle/volumes/RingVolume.h @@ -22,7 +22,7 @@ namespace particle { explicit RingVolume(); explicit RingVolume(float radius, bool onEdge); - vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) override; + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& host) override; void parse() override; }; } \ No newline at end of file diff --git a/code/particle/volumes/SpheroidVolume.cpp b/code/particle/volumes/SpheroidVolume.cpp index 95d64cfdce0..f89446494e3 100644 --- a/code/particle/volumes/SpheroidVolume.cpp +++ b/code/particle/volumes/SpheroidVolume.cpp @@ -6,7 +6,7 @@ namespace particle { SpheroidVolume::SpheroidVolume() : m_bias(1.f), m_stretch(1.f), m_radius(1.f), m_modular_curve_instance(m_modular_curves.create_instance()) { }; SpheroidVolume::SpheroidVolume(float bias, float stretch, float radius) : m_bias(bias), m_stretch(stretch), m_radius(radius), m_modular_curve_instance(m_modular_curves.create_instance()) { }; - vec3d SpheroidVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) { + vec3d SpheroidVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& /*host*/) { auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); vec3d pos; // get an unbiased random point in the sphere diff --git a/code/particle/volumes/SpheroidVolume.h b/code/particle/volumes/SpheroidVolume.h index 36ea461d232..c5427968ee6 100644 --- a/code/particle/volumes/SpheroidVolume.h +++ b/code/particle/volumes/SpheroidVolume.h @@ -33,7 +33,7 @@ namespace particle { explicit SpheroidVolume(); explicit SpheroidVolume(float bias, float stretch, float radius); - vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) override; + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& host) override; void parse() override; }; } \ No newline at end of file diff --git a/code/source_groups.cmake b/code/source_groups.cmake index dbba52510dc..9a5115ae5d6 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -1154,6 +1154,8 @@ add_file_folder("Particle\\\\Volumes" particle/volumes/ConeVolume.h particle/volumes/LegacyAACuboidVolume.cpp particle/volumes/LegacyAACuboidVolume.h + particle/volumes/ModelSurfaceVolume.cpp + particle/volumes/ModelSurfaceVolume.h particle/volumes/PointVolume.cpp particle/volumes/PointVolume.h particle/volumes/RingVolume.cpp From c8d4f376c4b3d29d067ae51c66bbf19b3b687008 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Mon, 18 May 2026 16:45:18 +0100 Subject: [PATCH 61/65] Enable QTFRED Nightly --- .github/workflows/build-nightly.yaml | 80 ++++++++++++++-------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/.github/workflows/build-nightly.yaml b/.github/workflows/build-nightly.yaml index 0a5731a2953..2cbece85231 100644 --- a/.github/workflows/build-nightly.yaml +++ b/.github/workflows/build-nightly.yaml @@ -31,7 +31,7 @@ jobs: env: CONFIGURATION: ${{ matrix.configuration }} COMPILER: gcc-9 - ENABLE_QTFRED: OFF + ENABLE_QTFRED: ON run: $GITHUB_WORKSPACE/ci/linux/configure_cmake.sh - name: Compile working-directory: ./build @@ -110,30 +110,30 @@ jobs: name: Checkout with: submodules: true - # - name: Cache Qt - # id: cache-qt-win - # uses: actions/cache@v1 - # with: - # path: ${{ github.workspace }}/../Qt - # key: ${{ runner.os }}-${{ matrix.arch }}-QtCache-${{ env.QT_VERSION }} - # - name: Install Qt (32 bit) - # uses: jurplel/install-qt-action@v2 - # if: ${{ matrix.arch == 'Win32' }} - # with: - # version: ${{ env.QT_VERSION }} - # dir: ${{ github.workspace }}/.. - # arch: win32_msvc2017 - # cached: ${{ steps.cache-qt-win.outputs.cache-hit }} - # aqtversion: ==0.8 - # - name: Install Qt (64 bit) - # uses: jurplel/install-qt-action@v2 - # if: ${{ matrix.arch == 'x64' }} - # with: - # version: ${{ env.QT_VERSION }} - # dir: ${{ github.workspace }}/.. - # arch: win64_msvc2017_64 - # cached: ${{ steps.cache-qt-win.outputs.cache-hit }} - # aqtversion: ==0.8 + - name: Cache Qt + id: cache-qt-win + uses: actions/cache@v1 + with: + path: ${{ github.workspace }}/../Qt + key: ${{ runner.os }}-${{ matrix.arch }}-QtCache-${{ env.QT_VERSION }} + - name: Install Qt (32 bit) + uses: jurplel/install-qt-action@v2 + if: ${{ matrix.arch == 'Win32' }} + with: + version: ${{ env.QT_VERSION }} + dir: ${{ github.workspace }}/.. + arch: win32_msvc2017 + cached: ${{ steps.cache-qt-win.outputs.cache-hit }} + aqtversion: ==0.8 + - name: Install Qt (64 bit) + uses: jurplel/install-qt-action@v2 + if: ${{ matrix.arch == 'x64' }} + with: + version: ${{ env.QT_VERSION }} + dir: ${{ github.workspace }}/.. + arch: win64_msvc2017_64 + cached: ${{ steps.cache-qt-win.outputs.cache-hit }} + aqtversion: ==0.8 - name: Prepare Vulkan SDK uses: humbletim/setup-vulkan-sdk@v1.2.1 with: @@ -234,21 +234,21 @@ jobs: name: Mac runs-on: macos-latest steps: - # - name: Cache Qt - # id: cache-qt-mac - # uses: actions/cache@v1 - # with: - # path: ${{ github.workspace }}/../Qt - # key: ${{ runner.os }}-QtCache-${{ env.QT_VERSION }} - # - name: Install Qt - # uses: jurplel/install-qt-action@v2 - # with: - # version: ${{ env.QT_VERSION }} - # dir: ${{ github.workspace }}/.. - # cached: ${{ steps.cache-qt-mac.outputs.cache-hit }} - # setup-python: 'false' - # aqtversion: ==1.1.3 - # py7zrversion: '==0.19.*' + - name: Cache Qt + id: cache-qt-mac + uses: actions/cache@v1 + with: + path: ${{ github.workspace }}/../Qt + key: ${{ runner.os }}-QtCache-${{ env.QT_VERSION }} + - name: Install Qt + uses: jurplel/install-qt-action@v2 + with: + version: ${{ env.QT_VERSION }} + dir: ${{ github.workspace }}/.. + cached: ${{ steps.cache-qt-mac.outputs.cache-hit }} + setup-python: 'false' + aqtversion: ==1.1.3 + py7zrversion: '==0.19.*' - uses: actions/checkout@v1 name: Checkout with: From 872c7bc9ccbcaebe7c67e80eb4557c858c97cd71 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Mon, 18 May 2026 16:48:15 +0100 Subject: [PATCH 62/65] Fix Indent --- .github/workflows/build-nightly.yaml | 78 ++++++++++++++-------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/.github/workflows/build-nightly.yaml b/.github/workflows/build-nightly.yaml index 2cbece85231..716004e3588 100644 --- a/.github/workflows/build-nightly.yaml +++ b/.github/workflows/build-nightly.yaml @@ -110,30 +110,30 @@ jobs: name: Checkout with: submodules: true - - name: Cache Qt - id: cache-qt-win - uses: actions/cache@v1 - with: - path: ${{ github.workspace }}/../Qt - key: ${{ runner.os }}-${{ matrix.arch }}-QtCache-${{ env.QT_VERSION }} - - name: Install Qt (32 bit) - uses: jurplel/install-qt-action@v2 - if: ${{ matrix.arch == 'Win32' }} - with: - version: ${{ env.QT_VERSION }} - dir: ${{ github.workspace }}/.. - arch: win32_msvc2017 - cached: ${{ steps.cache-qt-win.outputs.cache-hit }} - aqtversion: ==0.8 - - name: Install Qt (64 bit) - uses: jurplel/install-qt-action@v2 - if: ${{ matrix.arch == 'x64' }} - with: - version: ${{ env.QT_VERSION }} - dir: ${{ github.workspace }}/.. - arch: win64_msvc2017_64 - cached: ${{ steps.cache-qt-win.outputs.cache-hit }} - aqtversion: ==0.8 + - name: Cache Qt + id: cache-qt-win + uses: actions/cache@v1 + with: + path: ${{ github.workspace }}/../Qt + key: ${{ runner.os }}-${{ matrix.arch }}-QtCache-${{ env.QT_VERSION }} + - name: Install Qt (32 bit) + uses: jurplel/install-qt-action@v2 + if: ${{ matrix.arch == 'Win32' }} + with: + version: ${{ env.QT_VERSION }} + dir: ${{ github.workspace }}/.. + arch: win32_msvc2017 + cached: ${{ steps.cache-qt-win.outputs.cache-hit }} + aqtversion: ==0.8 + - name: Install Qt (64 bit) + uses: jurplel/install-qt-action@v2 + if: ${{ matrix.arch == 'x64' }} + with: + version: ${{ env.QT_VERSION }} + dir: ${{ github.workspace }}/.. + arch: win64_msvc2017_64 + cached: ${{ steps.cache-qt-win.outputs.cache-hit }} + aqtversion: ==0.8 - name: Prepare Vulkan SDK uses: humbletim/setup-vulkan-sdk@v1.2.1 with: @@ -234,21 +234,21 @@ jobs: name: Mac runs-on: macos-latest steps: - - name: Cache Qt - id: cache-qt-mac - uses: actions/cache@v1 - with: - path: ${{ github.workspace }}/../Qt - key: ${{ runner.os }}-QtCache-${{ env.QT_VERSION }} - - name: Install Qt - uses: jurplel/install-qt-action@v2 - with: - version: ${{ env.QT_VERSION }} - dir: ${{ github.workspace }}/.. - cached: ${{ steps.cache-qt-mac.outputs.cache-hit }} - setup-python: 'false' - aqtversion: ==1.1.3 - py7zrversion: '==0.19.*' + - name: Cache Qt + id: cache-qt-mac + uses: actions/cache@v1 + with: + path: ${{ github.workspace }}/../Qt + key: ${{ runner.os }}-QtCache-${{ env.QT_VERSION }} + - name: Install Qt + uses: jurplel/install-qt-action@v2 + with: + version: ${{ env.QT_VERSION }} + dir: ${{ github.workspace }}/.. + cached: ${{ steps.cache-qt-mac.outputs.cache-hit }} + setup-python: 'false' + aqtversion: ==1.1.3 + py7zrversion: '==0.19.*' - uses: actions/checkout@v1 name: Checkout with: From 3a75eeb193bafc8d5caf075d3d315014fe31cfe6 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Mon, 18 May 2026 16:56:01 +0100 Subject: [PATCH 63/65] add test ability --- .github/workflows/build-nightly.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-nightly.yaml b/.github/workflows/build-nightly.yaml index 716004e3588..011f2e3a951 100644 --- a/.github/workflows/build-nightly.yaml +++ b/.github/workflows/build-nightly.yaml @@ -4,6 +4,7 @@ on: push: tags: - 'nightly_*' + workflow_dispatch: env: QT_VERSION: 5.12.12 From b3ee87949d7f9fe86d39c3034b234b5245660add Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Mon, 18 May 2026 17:03:03 +0100 Subject: [PATCH 64/65] Actually enable qtfred buidling --- .github/workflows/build-nightly.yaml | 7 +-- .github/workflows/build-test.yaml | 86 ++++++++++++++-------------- 2 files changed, 46 insertions(+), 47 deletions(-) diff --git a/.github/workflows/build-nightly.yaml b/.github/workflows/build-nightly.yaml index 011f2e3a951..34d6a32294c 100644 --- a/.github/workflows/build-nightly.yaml +++ b/.github/workflows/build-nightly.yaml @@ -4,7 +4,6 @@ on: push: tags: - 'nightly_*' - workflow_dispatch: env: QT_VERSION: 5.12.12 @@ -154,13 +153,13 @@ jobs: if [ "$ARCHITECTURE" = "Win32" ]; then cmake -DCMAKE_INSTALL_PREFIX="$(pwd)/install" -DFSO_USE_SPEECH="ON" \ -DFSO_USE_VOICEREC="ON" -DFORCED_SIMD_INSTRUCTIONS="$SIMD" \ - -DFSO_BUILD_QTFRED=OFF -DFSO_BUILD_TESTS=ON \ + -DFSO_BUILD_QTFRED=ON -DFSO_BUILD_TESTS=ON \ -DFSO_INSTALL_DEBUG_FILES="ON" -DFSO_BUILD_WITH_VULKAN="OFF" -A "$ARCHITECTURE" \ -G "Visual Studio 17 2022" -T "v143" -DCMAKE_BUILD_TYPE=$CONFIGURATION .. else cmake -DCMAKE_INSTALL_PREFIX="$(pwd)/install" -DFSO_USE_SPEECH="ON" \ -DFSO_USE_VOICEREC="ON" -DFORCED_SIMD_INSTRUCTIONS="$SIMD" \ - -DFSO_BUILD_QTFRED=OFF -DFSO_BUILD_TESTS=ON \ + -DFSO_BUILD_QTFRED=ON -DFSO_BUILD_TESTS=ON \ -DFSO_INSTALL_DEBUG_FILES="ON" -A "$ARCHITECTURE" \ -G "Visual Studio 17 2022" -T "v143" -DCMAKE_BUILD_TYPE=$CONFIGURATION .. fi @@ -273,7 +272,7 @@ jobs: COMPILER: ${{ matrix.compiler }} ARCHITECTURE: ${{ matrix.arch }} JOB_CMAKE_OPTIONS: ${{ matrix.cmake_options }} - ENABLE_QTFRED: OFF + ENABLE_QTFRED: ON run: $GITHUB_WORKSPACE/ci/linux/configure_cmake.sh - name: Compile working-directory: ./build diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 99f7fc1144c..ab9417eed1f 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -31,7 +31,7 @@ jobs: env: CONFIGURATION: ${{ matrix.configuration }} COMPILER: gcc-9 - ENABLE_QTFRED: OFF + ENABLE_QTFRED: ON run: $GITHUB_WORKSPACE/ci/linux/configure_cmake.sh - name: Compile working-directory: ./build @@ -108,30 +108,30 @@ jobs: [[ "${{ github.ref }}" =~ ^refs\/heads\/test\/(.*)$ ]] # Override the revision string so that the builds are named correctly echo "set(FSO_VERSION_REVISION_STR ${BASH_REMATCH[1]})" > "version_override.cmake" - # - name: Cache Qt - # id: cache-qt-win - # uses: actions/cache@v1 - # with: - # path: ${{ github.workspace }}/../Qt - # key: ${{ runner.os }}-${{ matrix.arch }}-QtCache-${{ env.QT_VERSION }} - # - name: Install Qt (32 bit) - # uses: jurplel/install-qt-action@v2 - # if: ${{ matrix.arch == 'Win32' }} - # with: - # version: ${{ env.QT_VERSION }} - # dir: ${{ github.workspace }}/.. - # arch: win32_msvc2017 - # cached: ${{ steps.cache-qt-win.outputs.cache-hit }} - # aqtversion: ==0.8 - # - name: Install Qt (64 bit) - # uses: jurplel/install-qt-action@v2 - # if: ${{ matrix.arch == 'x64' }} - # with: - # version: ${{ env.QT_VERSION }} - # dir: ${{ github.workspace }}/.. - # arch: win64_msvc2017_64 - # cached: ${{ steps.cache-qt-win.outputs.cache-hit }} - # aqtversion: ==0.8 + - name: Cache Qt + id: cache-qt-win + uses: actions/cache@v1 + with: + path: ${{ github.workspace }}/../Qt + key: ${{ runner.os }}-${{ matrix.arch }}-QtCache-${{ env.QT_VERSION }} + - name: Install Qt (32 bit) + uses: jurplel/install-qt-action@v2 + if: ${{ matrix.arch == 'Win32' }} + with: + version: ${{ env.QT_VERSION }} + dir: ${{ github.workspace }}/.. + arch: win32_msvc2017 + cached: ${{ steps.cache-qt-win.outputs.cache-hit }} + aqtversion: ==0.8 + - name: Install Qt (64 bit) + uses: jurplel/install-qt-action@v2 + if: ${{ matrix.arch == 'x64' }} + with: + version: ${{ env.QT_VERSION }} + dir: ${{ github.workspace }}/.. + arch: win64_msvc2017_64 + cached: ${{ steps.cache-qt-win.outputs.cache-hit }} + aqtversion: ==0.8 - name: Prepare Vulkan SDK uses: humbletim/setup-vulkan-sdk@v1.2.1 with: @@ -151,13 +151,13 @@ jobs: if [ "$ARCHITECTURE" = "Win32" ]; then cmake -DCMAKE_INSTALL_PREFIX="$(pwd)/install" -DFSO_USE_SPEECH="ON" \ -DFSO_USE_VOICEREC="ON" -DFORCED_SIMD_INSTRUCTIONS="$SIMD" \ - -DFSO_BUILD_QTFRED=OFF -DFSO_BUILD_TESTS=ON \ + -DFSO_BUILD_QTFRED=ON -DFSO_BUILD_TESTS=ON \ -DFSO_INSTALL_DEBUG_FILES="ON" -DFSO_BUILD_WITH_VULKAN="OFF" -A "$ARCHITECTURE" \ -G "Visual Studio 17 2022" -T "v143" -DCMAKE_BUILD_TYPE=$CONFIGURATION .. else cmake -DCMAKE_INSTALL_PREFIX="$(pwd)/install" -DFSO_USE_SPEECH="ON" \ -DFSO_USE_VOICEREC="ON" -DFORCED_SIMD_INSTRUCTIONS="$SIMD" \ - -DFSO_BUILD_QTFRED=OFF -DFSO_BUILD_TESTS=ON \ + -DFSO_BUILD_QTFRED=ON -DFSO_BUILD_TESTS=ON \ -DFSO_INSTALL_DEBUG_FILES="ON" -A "$ARCHITECTURE" \ -G "Visual Studio 17 2022" -T "v143" -DCMAKE_BUILD_TYPE=$CONFIGURATION .. fi @@ -233,21 +233,21 @@ jobs: name: Mac runs-on: macos-latest steps: - # - name: Cache Qt - # id: cache-qt-mac - # uses: actions/cache@v1 - # with: - # path: ${{ github.workspace }}/../Qt - # key: ${{ runner.os }}-QtCache-${{ env.QT_VERSION }} - # - name: Install Qt - # uses: jurplel/install-qt-action@v2 - # with: - # version: ${{ env.QT_VERSION }} - # dir: ${{ github.workspace }}/.. - # cached: ${{ steps.cache-qt-mac.outputs.cache-hit }} - # setup-python: 'false' - # aqtversion: ==1.1.3 - # py7zrversion: '==0.19.*' + - name: Cache Qt + id: cache-qt-mac + uses: actions/cache@v1 + with: + path: ${{ github.workspace }}/../Qt + key: ${{ runner.os }}-QtCache-${{ env.QT_VERSION }} + - name: Install Qt + uses: jurplel/install-qt-action@v2 + with: + version: ${{ env.QT_VERSION }} + dir: ${{ github.workspace }}/.. + cached: ${{ steps.cache-qt-mac.outputs.cache-hit }} + setup-python: 'false' + aqtversion: ==1.1.3 + py7zrversion: '==0.19.*' - uses: actions/checkout@v1 name: Checkout with: @@ -270,7 +270,7 @@ jobs: CONFIGURATION: ${{ matrix.configuration }} COMPILER: ${{ matrix.compiler }} ARCHITECTURE: ${{ matrix.arch }} - ENABLE_QTFRED: OFF + ENABLE_QTFRED: ON run: $GITHUB_WORKSPACE/ci/linux/configure_cmake.sh - name: Compile working-directory: ./build From 2fa159ce69a39a209d1ea4f073e2379685a1de02 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Mon, 18 May 2026 21:45:10 +0100 Subject: [PATCH 65/65] configure QTDIR environment variable --- .github/workflows/build-nightly.yaml | 3 ++- .github/workflows/build-test.yaml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-nightly.yaml b/.github/workflows/build-nightly.yaml index 34d6a32294c..410e5221874 100644 --- a/.github/workflows/build-nightly.yaml +++ b/.github/workflows/build-nightly.yaml @@ -38,7 +38,7 @@ jobs: env: CONFIGURATION: ${{ matrix.configuration }} run: | - LD_LIBRARY_PATH=$Qt5_DIR/lib:$LD_LIBRARY_PATH ninja -k 20 all + installRY_PATH=$Qt5_DIR/lib:$LD_LIBRARY_PATH ninja -k 20 all - name: Run Tests working-directory: ./build env: @@ -48,6 +48,7 @@ jobs: - name: Generate AppImage working-directory: ./build env: + QTDIR: Qt5_DIR CONFIGURATION: ${{ matrix.configuration }} run: $GITHUB_WORKSPACE/ci/linux/generate_appimage.sh $GITHUB_WORKSPACE/build/install - name: Upload build result diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index ab9417eed1f..ed78bfb9e2e 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -48,6 +48,7 @@ jobs: - name: Generate AppImage working-directory: ./build env: + QTDIR: Qt5_DIR CONFIGURATION: ${{ matrix.configuration }} run: $GITHUB_WORKSPACE/ci/linux/generate_appimage.sh $GITHUB_WORKSPACE/build/install - name: Upload build result