Lower Vulkan version requirement to 1.4.304, which should be the lowest
version supported without requiring code changes. This should also offer
out of the box support on current Linux distros, such as Debian 13 (trixie)
and Ubuntu 25.10 (questing) & 26.04 (resolute).
Also make sure that the shaderc headers are available and added to the
include path on Linux when using system libraries.
Silence CMake build type warnings when using a multi-config generator.
Fix macOS deployment target specification. It must be set before
the first project() command to work properly, and needs to be a cached
value so that it can be overridden by the user. This in particular is
required for Xcode 27 where the minimum deployment target is 12.0 and
using anything less is a hard fail rather than a warning.
Fix Xcode scheme only containing qtFred as a buildable target.
Fix various Xcode warnings when a static library contains files without
any symbols.
Allow FSO/qtFRED cmdline arguments and working directory to be set via their
respective options (FSO_RUN_ARGUMENTS & FSO_FREESPACE_PATH) in Xcode.
Fix product identifier mismatch and unset minimum system version warnings
in Info.plist file.
* only strip the legacy M/S suffix from the last pilot when needed
player_select_get_last_pilot_info() chopped any trailing uppercase M or S
from the LastPlayer value, a holdover from the pre-pilot-file era. Pilots
whose callsigns end in M or S (e.g. BOSS) were never found as the last
pilot. Now the suffix is only removed when the full callsign is not a
pilot but the shortened one is. This also avoids indexing before the
string when LastPlayer is empty.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* route all pilot selection through one commit function
player_select_close() and player_finish_select() each had their own copy
of the code that loads a pilot as the active Player, and the copies had
drifted apart. Both now call player_select_commit_pilot().
This fixes two bugs:
* -pilot did not work with the retail pilot select screen.
player_finish_select() loaded the requested pilot, but then
player_select_close() loaded the highlighted pilot (the most recent
one) over it. The close handler now skips the load if a pilot has
already been committed.
* player_finish_select() ignored its is_multi parameter when setting
player_was_multi, so scripted selection of a multiplayer pilot saved
it as single-player.
Scripted selection also now saves the pilot file, as the retail screen
already did. The background bitmap handle starts out as -1 so that the
close handler doesn't release bitmap 0 when the screen was never set up.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* select pilots automatically at game init, through the normal commit path
-startgame, -connect and -almission (via Cmdline_use_last_pilot) used
player_select_get_last_pilot(), which loaded the last pilot directly
without most of the setup that a normal selection does. It skipped
valid_pilot(), multi_options_init_globals() and saving the pilot file.
Because it never entered the pilot select state, it also turned off the
main hall's fallback for an unavailable campaign. Meanwhile -pilot and
-benchmark_mode were handled on the pilot select screen itself, which
scripted UIs replace entirely.
All of these now go through player_select_try_auto_select() at
GS_EVENT_GAME_INIT. It picks the -pilot callsign, else the last pilot
if it still exists, else the most recently saved pilot. It shows the
startup warning popups, validates the pilot, and commits it through
player_select_commit_pilot(). The pilot is multiplayer if -startgame or
-connect was given, and single-player otherwise. If there are no
pilots, or validation fails, the pilot select screen is shown as usual.
The main hall now allows the campaign fallback whenever a pilot has just
been committed, not only when coming from the pilot select state.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* select a pilot automatically for -start_mission
-start_mission skips the main hall, but it still waited on the pilot
select screen. It now uses the same automatic pilot selection as the
other quickstart options, so the screen only appears if there are no
pilots. The pilot is committed as single-player, consistent with the
main hall forcing single-player mode for -start_mission.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* skip the intro movie for quickstart options
When the command line asks to skip pilot selection (-start_mission,
-pilot, -startgame, -connect, -almission, -benchmark_mode), the intro
movie now doesn't play either. It could take several minutes before the
game even reached the pilot. The OnIntroAboutToPlay hook still runs, as
it does when the mod table disables the intro.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
This should be a safe minimum for us when system libraries are either
required or preferred. Most current Linux distros that have SDL3 should
support this version or better.
* take a model reference when a ship changes class
ship_model_change() only loaded the model when the class had none, so a ship
switched to a class whose model had been loaded only by a select-screen
preview adopted that preview's single reference. Closing the screen then
freed the model from under the ship, and the first simulation frame asserted
in model_get(). This is what happened on a replayed mission with a restored
loadout: ship select was skipped, the weapon-select overhead loaded the new
class once, the commit switched the player to it, and the screen close
released it.
Load unconditionally, as ship_create() already does, so the ship holds its own
reference.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
* reject stale model ids in model_unload and release select-screen handles at level close
model_unload only used the slot part of the id it was given, so a handle kept
across missions would free whatever model had since been loaded into that slot.
Validate the id against the slot's occupant and warn instead.
Two holders could present such handles: the weapon-select icon models under a
scripted UI that overrides the state hooks (the native close never runs), and
the ship-select 3D model global, which was never reset between missions.
Release the select-screen handles from game_level_close, and unload the
ship-select model on screen close and on every selection change.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
* consolidate the select-screen resource release
The weapon side released its icons, anims, and ship class data from two
functions keyed on whether the screen was open, and the level-close path added
a third copy. Give the weapon side the same shape as the ship side: one
unconditional weapon_select_common_close(), with weapon_select_close() left as
pure UI teardown. common_select_level_close() now just calls the two common
closes, and common_select_close() calls it in turn. The ship-select 3D model
is released from ship_select_common_close() so both paths cover it.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
---------
Co-authored-by: Claude Fable 5.1 <[email protected]>
During phase 1 of the FS2 ship/weapon select animation, the wireframe
is supposed to be revealed as the scanline moves down. That relied on
the clip plane set up for the grid carrying over to the wireframe
render. 90a2676f1 replaced the grid's clip plane with line trimming,
which left the phase 1 wireframe unclipped, so it popped in all at once.
Clip the wireframe in both phase 1 and phase 2.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The actively-pursues filter added to "$fix ai target recovery:" in #7736 was applied to every target search. But a ship's target is not only used for pursuit: it also drives the ship's turrets, other ships' guard reactions, and attacker counts. In retail SM1-04, the friendly Knossos (which does not pursue fighters) was made to target the Behemoth instead of a Mara, which caused the Mara guards to react to the protected Knossos endlessly instead of rushing the player.
The filter now applies only where the target is chosen for maneuvering:
* find_enemy() and get_nearest_objnum() take a required for_pursuit parameter; automatic searches pass ai_targets_for_pursuit(), which is true only in AIM_CHASE or AIM_STRAFE
* guard reactions, retaliation (dynamic chase), and the retargets in ai_big_chase_attack() and ai_big_maybe_enter_strafe_mode() skip ships the attacker won't pursue, via the new ai_declines_pursuit()
* ai_attack_wing() prefers living wing members the ship will pursue; if there are none, it holds a member as an explicit order to attack an unpursued ship does
* get_nearest_objnum() holds an unpursued member of the enemy wing rather than leaving the wing
Since refused targets are no longer picked for pursuit, ai_chase() no longer needs to drop them. Retail behavior is unchanged when the flag is disabled.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Several ai_info fields remember which order set them up (enemy_wing,
guard_objnum/signature/wingnum) or which mode a dynamic goal interrupted
(previous_mode, resume_goal_time), but nothing cleared them when a new
order took over. A ship that once guarded a wing would be yanked back
into AIM_GUARD whenever a wingmate died; a stale previous_mode of
AIM_GUARD could make a former guard abandon a counterattack; a stale
enemy_wing biased every find_enemy() toward a long-ago target; and a
resume_goal_time left over from a dynamic chase suppressed target
acquisition for up to 20 seconds into the next order.
Add ai_reset_order_state(), called when ai_process_mission_orders()
takes up a goal and when ai_do_default_behavior() runs. Each order's
own setup repopulates the fields it needs. enemy_wing is reset under
either $fix stale ai order state or $fix ai target recovery, since it
affects target selection; the rest only under the new flag.
maybe_set_dynamic_chase() wrote aip->mode into previous_submode, a
retail typo that left previous_mode untracked for dynamic chases. Under
the flag, save both fields properly, but not when a dynamic goal is
already in progress, so a nested interruption does not clobber the mode
to return to. Apply the same guard to the strafe/chase transitions in
aibig.cpp.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
The load screen background was drawn in the fixed 1024x768 layout, so an
image larger than that overshot the screen and only a corner was visible.
Set the screen scale to the background's dimensions, as the main hall and
title splash already do, and draw the loading animation in the same
layout.
The loading animation is assumed to match the default loading background
(or the default layout, if there is none). That reference is fit into
the mission's background the way menus are fit into the screen: scaled by
the limiting dimension and centered in the other. So a mission can use a
background of any resolution or aspect ratio without the animation
changing size or position relative to it. Retail assets behave as
before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a HiDPI monitor with OS scaling, every Qt widget in qtFRED scales
but the ship labels, waypoint labels and coordinate readout drawn by
the engine inside the viewport do not, because the engine draws them in
physical pixels. At 200% they come out half the size of everything
around them.
FredRenderer already receives the window's device pixel ratio as the
scale argument of render_frame(). Remember it there and multiply it into
the label font scale at every label site, so the user's Preferences
value stays "100% = normal size on screen" regardless of OS scaling.
Fixes#7727
The warning fired for any 3D shockwave model without a glow map, but it turns out that situation is fine. Drop the warning along with the cmdline include it required.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
get_ship(), get_wing(), get_object() and get_parse_object() only assigned
their out-pointer on success. When a lookup failed (no bytes left, or a net
signature that no longer resolves, e.g. a ship destroyed before the packet
was processed) the caller's uninitialized local was left as stack garbage,
so its nullptr check could pass and the next line would write through it.
multi_sexp_set_camera_target() used the object without checking at all.
Set the out-pointer to nullptr up front, so every caller's existing nullptr
check works. This also clears the C4701/C4703 "potentially uninitialized"
warnings MSVC 2026 reports for the callers in sexp.cpp when it inlines these
getters at link time.
The failure checks after parsing (bad detail levels, debris objects or
parents, and too many textures) run after the TXTR and GLOW chunks have
already loaded bitmaps, but model_load only deleted the polymodel, so
those bitmap slots were never released. Page out the textures and free
the model through model_free, as the virtual POF cache already does for
models it loads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Retail models (111 of them in sparky_fs2.vp) store -1 as their cross
section count, meaning none, which the original loader silently accepted.
Routing the count through pof_read_count made every such model warn on
load. Read it directly and floor it at zero, as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A virtual POF ignored the load status of its base POF, so a base that
failed to load (missing, bad version, or rejected after parsing) was
still built upon and reported as a successful load. Likewise, every
operation that copies from another POF used it even if that POF had
failed to load.
Return FAIL from read_virtual_model_file when the base POF fails, and
have the operations skip, with a warning, any source POF that failed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When appending submodels would take the combined POF past
MAX_MODEL_TEXTURES, the limit was only noticed partway through, after the
submodel array had been grown, subsystems copied, and n_textures
incremented past the limit, which later loops over maps[] then overran.
Work out the texture mapping first, and give up before touching the model.
Also skip the -1 texture id of untextured flat polygons, which was being
assigned a texture slot, and which replace_textures_used then used to
index Num_verts and Num_polies.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Clamping the TXTR count keeps maps[] safe, but texture indices are baked
into the BSP data, so a model declaring more than MAX_MODEL_TEXTURES
textures cannot be built correctly. Keep the clamp so that the chunk is
still read in full, then fail the load after parsing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
process_tmap and process_tmap2 used the texture index from the BSP data
to index Num_polies and Num_verts without checking it, so a corrupt
model wrote past both arrays while its vertex buffers were built. The
collision tree parser already treats such an index as a fatal error, but
it runs afterward. Raise the same error here, and skip the polygon in
case the error is continued past.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The post-parse sanity checks and the bad-version check returned FAIL
without closing the POF. Nothing reads the file after the chunk loop, so
close it there, and close it before the bad-version return too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MSVC 14.50 (v145) no longer targets Windows 7, 8, or 8.1, so building
with it would quietly drop those platforms. The VS 2026 runner images
include the v143 compiler but ship ATL and MFC only for the latest
toolset, and both are needed: ATL by SAPI speech (sphelper.h) and MFC
by FRED2. Install the 14.44 ATL/MFC components before configuring and
keep building with v143, matching the 26.0 release.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
All default Windows runners should now be moved over the Visual Studio 2026,
with the exception of windows-2022 (which may be retired before long). So
move to windows-2025 and update the CMake generator to use vs2026 as well.
Directional, point, and cone lights left local_vec, local_vec2, and some of
the type-specific fields uninitialized until something rotated or set them.
The sun copies in Static_light were never rotated at all until the previous
commit, so light_apply_rgb's sun term read uninitialized memory. Zero every
field up front, which also covers what the tube light did by hand.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
light_apply_rgb works in the model's local frame, but nothing rotated the
lights into that frame before insignias were drawn: light_rotate_all() was
only called for the briefing and tech room lights, and even then it skipped
Static_light, the separate copies of the sun lights that light_apply_rgb
reads. So the sun term used a stale, never-set direction and the point
lights were treated as if they sat at the model's origin.
Rotate the lights, including the sun copies, once the insignia's instance
is set up, and light each vertex at its actual position, which includes
the insignia's offset.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
In debug builds, subsystems found in a model but missing from ships.tbl
are meant to be dumped to data/tables/<model>.subsystems as stub table
entries. Since subsystem matching was deferred until after the model
file is read (#4590), the dump file was already closed by the time
do_new_subsystem ran, so it was always empty and deleted. Worse, ss_fp
was left pointing at a closed CFILE slot, which another file could reuse
before do_new_subsystem wrote to it, and the dump file leaked whenever a
load failed.
Open the dump file on the first unmatched subsystem instead, and close it
once the deferred subsystems have been matched. This also means no file
is created and deleted for models without unmatched subsystems. Reset
the once-per-model warning flags there as well, rather than only when
the dump file opened successfully.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The insignia normal array was declared with the vertex limit, filled in by
face index, and read by vertex index. An insignia with more than 81 faces
therefore wrote past the array, into the following insignia or, for the
sixth and final slot, past the insignia list into the rest of the
polymodel. And the renderer lit each vertex with the normal of whichever
face shared its index, or with a zero normal (ambient light only) when
there was no such face.
A normal belongs to a face, so size the array by the face limit and light
all three corners of each face with that face's normal.
Co-Authored-By: Claude Opus 5.5 <[email protected]>