Cockpit_active was latched inside ship_render_player_ship(), which only
runs when Viewer_obj is non-null. External, chase, warp-chase, topdown,
free-camera and dead views all null Viewer_obj, so the flag froze at its
last cockpit-view value and cockpitinactive HUD gauges vanished. It was
also read before being written each frame (by the show-ship check and the
shadow helpers), so those decisions lagged a frame on any transition.
Replace it with three functions computed on demand:
- ship_cockpit_enabled(sip): the ship has a cockpit model and cockpits
aren't disabled; used by the Show_ship_only_if_cockpits_enabled checks
- ship_render_player_cockpit(sip): the above, outside topdown view;
replaces three duplicated copies of that expression
- ship_render_player_cockpit_active(): whether the viewer's cockpit is
being rendered this frame; used by the HUD cockpit-view-choice checks
With the predicate correct, the VM_CHASE special cases added for #4804
are no longer needed and are removed. In chase view, cockpitinactive
gauges now also respect pop-up state, No_ets, and setRenderOverride,
which the early return in HudGauge::canRender() used to bypass.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Views that don't render the cockpit (external, chase, topdown, etc.) left
Cockpit_depth_texture holding the last cockpit-view frame, so the lightshaft
pass painted that silhouette. Fixes#7568.
Co-authored-by: Claude Opus 5 <[email protected]>
* Add command-line option overrides to options system
- Extend `OptionsManager` to support storing and displaying reasons for overrides.
- Update related code paths to prioritize command-line overrides, ensure correct option resolution, and display override details in the UI.
- Add unit tests for override behavior and round-trip validation.
- Remove unused legacy functions (`removeVSyncOption`, `removeWindowModeOption`) in favor of the new system.
* Add unit tests for `Graphics.Anisotropy` option validation with and without OpenGL
* Add support for associating command-line option names to `OptionsManager` overrides
- Update HDR, shadow rendering, and graphics API option overrides to include their respective command-line flag names.
- Refactor `Graphics.RenderAPI` to use the `GraphicsAPI` enum instead of raw integers for better type safety and clarity.
- Ensure Vulkan support logic and fallback behavior function correctly with the updated options system.
- Update unit tests to validate override functionality and enum-based API configuration.
* Remove unused `removeVSyncOption` function
* Apply the PR review feedback for the command-line option overrides
Delete the unit-test run artifacts that this branch committed by mistake, and
add .gitignore rules for the files that the unittests binary writes.
Give an override priority over an in-session edit again. The options UI now
shows a disabled control for an overridden option, and setConfigValue()
refuses an edit for an overridden key. Before this change, a save wrote the
edit to the config file and applied it to the bound global, but getValue()
kept returning the override. The game and the options menu then showed
different values for the rest of the session.
Add a setOverride() overload that takes a JSON value. Every command-line flag
now builds its override with json_integer, json_real, json_true, json_false
or json_pack, in place of a hand-formatted string.
Make Graphics.CockpitFOV and Graphics.CockpitFOVToggle independent of the
order in which the options manager loads them, and remove the forced override
on the player's toggle preference. The toggle listener also replaces an
identical bind_to() call that made the listener dead code.
Add 16 to the values of Graphics.MSAASamples. The -msaa flag accepts 16, so an
override with that value made printValues() report an invalid value.
Keep the render API that the caller gives to gr_init(). Only a Default value
now comes from the Graphics.RenderAPI option, so FRED and the unit tests keep
the backend that they ask for.
Make the default_func of Graphics.Shadows and Graphics.ShadowRenderMethod
return their global. A constant discarded the value that -enable_shadows and
the mod table set. Set Max_rt_shadow_local_lights to 4, because 3 is not one
of the values that the option offers.
Also: resolve -lang one time, share the render API name with version.cpp,
correct the stale comments, and add unit tests with a fixture that clears the
overrides after each test.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UGh4ZM2u4oUGSbrP122Uyr
* Fix the order-dependent crash in the MSAA option test
The test `MsaaOverrideAcceptsEverySampleCountTheFlagAllows` calls
`getValidValues()`. `Option<T>` makes a display string for each value, and the
display function of `Graphics.MSAASamples` calls `XSTR()`. `XSTR()` runs
`Int3()` if `lcl_xstr_init()` did not run before. Only `FSTestFixture` runs it,
and `Xstr_inited` stays set for the remainder of the process. Thus the shuffled
test order decided if the test passed or if it stopped the test binary with
SIGILL.
Move that one test to a new fixture that derives from `FSTestFixture`. The
fixture initializes localization, and it also clears the overrides after each
test. Refresh the comment that told that the test binary never runs
`lcl_init()`.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UGh4ZM2u4oUGSbrP122Uyr
---------
Co-authored-by: Claude Opus 5 <[email protected]>
FRED and qtFRED no longer overwrite a hand-edited arrival cue on the wing
containing the player start. The cue is editable when the wing has multiple
waves (where it governs the arrival of subsequent waves) and read-only
otherwise. Waves and threshold are now editable for the single-player
player wing, while the arrival delay remains locked; the arrival location
is editable in both editors.
Both error checkers flag a single-wave player wing with a custom cue as a
potential issue, and the engine now warns and aborts the mission load,
rather than tripping an Assert, if the player start ship was not created
at mission start.
Also fix an unguarded read of Ships[Player_start_shipnum] in both editors.
Co-authored-by: Claude Fable 5 <[email protected]>
* Safeguard divide by 0 for extra collision avoidance
Extra collision avoidance could have divided by zero, so just safeguard that possibility.
* one more div 0 failsafe
With every fixed-size weapon-class-indexed structure now converted to dynamic containers, we can finally remove the `MAX_WEAPON_TYPES` limit!
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* Automatically generate the isValid() Lua API function for handle types
If an ADE object's stored type has an isValid() member, ade_obj now registers an isValid function for it automatically. An explicit ADE_FUNC(isValid, ...) declared after the ADE_OBJ replaces the generated one, so types with special semantics (e.g. sound) keep their own. The 67 hand-written functions that merely delegated to the member are removed, and 26 types that previously exposed no isValid() gain one.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Add ADE_OBJ_VALIDATOR for index-based Lua handles
Int-index handles (shipclass, weaponclass, team, etc.) can now declare their validity predicate with a single ADE_OBJ_VALIDATOR or ADE_OBJ_VALIDATOR_RANGE line after the ADE_OBJ. The registered validator drives the generated isValid() function and is also honored by ade_set_args when the mod table asks for nil instead of invalid objects, which previously never applied to these handles. The 19 hand-written range-check functions are replaced, and loadout_ship, default_primary, default_secondary, and support_rearm_pool_team gain isValid(). Add a validator to HudGauge while we're at it.
Also fully qualify the scripting namespace in LuaConvert.h so its templates can be instantiated from the unit tests.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
---------
Co-authored-by: Claude Fable 5.1 <[email protected]>
`support_ship_info::rearm_weapon_pool` was a dense per-weapon-class array where -1 = unlimited (the default), 0 = not rearmable, and >0 = remaining stockpile. Convert it to a sparse per-team map:
`std::array<SCP_map<int, int>, MAX_TVT_TEAMS> rearm_weapon_pool;`
`int rearm_pool_default() const; // -1, or 0 under rearm_pool_from_loadout`
Mission-file format and save output are unchanged. Also tightens the two weapon-class bounds checks in ship.cpp. This removes another `MAX_WEAPON_TYPES` sized structure on the way to lifting the weapon class limit.
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Chase view deltas are not reset on mission init, so ensure they are reset to prevent the viewing jumps that can be seen at the start of mission in chase view. Also ensure perspective and slew lock are properly resotred.
Every copied bank was written to the same array slot, so all but the last
were lost, and the slots left behind kept their zero-initialized contents.
A zeroed bitmap handle is slot 0 rather than -1, so the paging code would
then page out an unrelated bitmap.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Follow-up to #7635.
Pass the firing bank explicitly through weapon_create() and beam_fire()
instead of searching the ship's banks by weapon class, which could
attribute substitution patterns to the wrong bank. Factor the
substitution logic into get_weapon_substitution_tuple() and only apply
linked/dual-fire pairing to ship-level fire, not turret fire. Skip
substitution on multiplayer clients, since the server already resolved it.
Derive turret bank indices from the weapon slot being fired rather than
the turret's bank cursor, so ammo and substitution state are tracked on
the bank that actually fired. Previously a single-selection turret could
consume ammo from a bank it never fired, and non-ammo turrets (whose
cursors stay at -1) would have skipped substitution entirely.
Use the Player_ship object flag rather than Player_obj in
ship_secondary_bank_can_dual_fire() so the server and clients agree on
dual fire for remote players, and guard ship_secondary_has_ammo() against
a negative bank index.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Multi-line and tabbed strings drawn with a scaleMultiplier != 1 overlapped
or misaligned because the font code advanced newlines and tabs by the
unscaled font height/tab width -- most visible in FRED's viewport labels
now that they honor an adjustable font scale.
Scale the newline and tab advance in both the rendering and measurement
paths of each backend: the NVG path (gr_string and NVGFont::getStringSize)
and the VFNT path (gr_string_old and VFNTFont::getStringSize), which also
gains tab handling. The tab width is hoisted onto the shared FSFont base
so both backends share it.
While here, pass the FSFont wrapper to gr_string_old instead of a bag of
scalars, make its font pointer const, and consolidate the
get_char_width_old externs into a single declaration in font_internal.h.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
With every fixed-size ship-class-indexed structure now converted to dynamic containers, we can finally remove the `MAX_SHIP_CLASSES` limit!
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Fix bitmap and model handle leakage in the ship/weapon select screens, and make release symmetric with load:
* Initialize each team only once in `ship_select_common_init`
* Unload every team's map in `ss_unload_team_icons`, not just the active one.
* Add a per-team unload helper and use it in the load-all functions
Also drop the stray per-icon Cur_Anim unload, and call `wl_free_ship_class_data` before loading in `wl_init_ship_class_data`.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Convert the mission ship/weapon select screens' per-class UI state from dense MAX_SHIP_CLASSES / MAX_WEAPON_TYPES arrays to sparse containers keyed by loadout-pool membership, matching the Ss_pool / Wl_pool maps they shadow:
ss_icon_info Ss_icons_teams[][MAX_SHIP_CLASSES] -> SCP_map<int, ss_icon_info>[]
wl_icon_info Wl_icons_teams[][MAX_WEAPON_TYPES] -> SCP_map<int, wl_icon_info>[]
wl_ship_class_info Wl_ships[MAX_SHIP_CLASSES] -> SCP_map<int, wl_ship_class_info>
int Plist[] / Slist[] + manual size counters -> SCP_vector<int>
The ship-select active list is replaced outright by a vector of ship classes rebuilt on each interface sync, plus the existing scroll offset. active_list_remove() was dead code and is deleted.
Behavior fixes required by the conversion:
* wl_set_disabled_weapons now computes usability flags for the pool
weapons plus every weapon currently in a slot's banks, instead of all
weapon classes. (Bank weapons are not guaranteed pool members.)
* The two mouse-region handlers that read Plist[Plist_start+index] for
raw indices 0..3 now bounds-check against the list size (as does
ss_get_ship_class_from_list).
* wl_unload_icons and the ss unloaders early-return on a null team
pointer instead of asserting and dereferencing.
The ship side loads icons for every pool class including exhausted 0-count entries while the weapon side loads only positive counts, preserving the existing asymmetry.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Mission flags are saved to the mission file as the full 64-bit flagset value, but three issues prevented anything larger than 32 bits from being saved as intended:
* `stuff_flagset` parsed the value with `stuff_long`, where `atol` clamps anything above `LONG_MAX`
* `flagset::from_u64` cast the incoming value to `unsigned long`, truncating bits 32-63
* `flagset::to_u64` used `bitset::to_ulong` where it should use `bitset::to_ullong`
Fix these issues and add `static_assert` in the flagset functions to defend against future issues. Also use the proper UINT64_T_ARG where it should be used.
This was first triggered by `Limited_support_rearm_pool`, the first mission flag to occupy bit 31. Only mission flags are affected; the other flagset serialization paths were audited and are safe.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The giveOrder scripting function now accepts optional DockerPoint and
DockeePoint arguments for ORDER_DOCK, each given as a dockpoint name or
one-based index (matching the setDocked convention). DockerPoint refers
to a dockpoint on the ordering ship and DockeePoint to one on the target.
If only one point is specified, a compatible point is chosen automatically
on the other ship using the same dock-type precedence as the existing
automatic dockpoint negotiation. When neither is specified, behavior is
unchanged.
Fixes#7119
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Fix estimate_ship_speed_upper_limit to handle non-ship objects gracefully
* Add collision handling for OBJ_PROP objects
Implement OBJ_PROP-specific collision messaging and logic: update player death messages, fix incorrect argument usage in collision checks, and set killer attributes for PROP collisions.
* consolidate per-bank external weapon model state into one struct
The state for a bank's external weapon model was scattered across two
structs and three naming schemes: the model instance and its weapon in
ship_weapon, the firing-point chain counter in a combined array indexed
with the bank + MAX_SHIP_PRIMARY_BANKS packing convention, and the
gatling spin rate/angle on ship itself.
Gather them into external_weapon_state, with one array for primary
banks and one for secondary banks in ship_weapon. This removes the
index-packing convention (ship_get_external_model_fp_offset now takes
the bank's state directly), and ship_delete now also frees secondary
instances should a future weapon ever create one.
No behavior change.
Co-Authored-By: Claude Fable 5 <[email protected]>
* update external weapon model spin in per-frame processing
The gatling spin state was integrated inside ship_fire_primary (spin-up)
and ship_stop_fire_primary_bank (spin-down), which entangled a per-frame
visual/state update with the firing event paths and had two quirks:
- Spin-down only ran when something called the stop-fire path, so a bank
could freeze mid-spin (e.g. AI ships that stop calling
ship_fire_primary without going through ship_stop_fire_primary).
- The spin angle only advanced when the ship class set
$Show Primary Models:, so identical weapon state evolved differently
per ship class even though the rate (which gates firing) always ran.
Banks that try to fire now just request spin-up, and
update_external_weapon_spin() -- called from ship_process_post next to
update_reload_percent(), which was moved out of the physics code for
the same reason -- integrates the rate and angle every frame. The
firing gate (no shots until the barrels reach full speed) is unchanged.
Co-Authored-By: Claude Fable 5 <[email protected]>
* make external_model_num mean what it says; gate firing offsets on drawn models
Since the original 2005 implementation, external_model_num was bashed
to model_num at parse time when no $External Model File: was given, so
every POF-rendered weapon appeared to have an external model. All the
checks downstream therefore tested 'has any model' while reading as
'has an external model', and two paths leaked the fallback into
gameplay:
- ship_fire_secondary applied the external-model firing point offset
without checking $Show Secondary Models: (unlike the primary path),
so any missile whose own in-flight POF contained gun points would
have its launch position offset on every ship in the game.
- The AI line-of-sight check, copied from ship_fire_secondary,
applied the offset with no draw check for either bank type, so for
primaries it could disagree with the actual firing path.
external_model_num now stays -1 unless $External Model File: was
given. Rendering code (ship_render_weapon_models, the HUD hardpoints
gauge, and the gatling instance helper) falls back to the weapon's own
model at the point of use, preserving the existing display of missiles
on hardpoints. Firing point offsets now require a dedicated external
model *and* the bank's models to actually be drawn, in all three
paths. In practice this does not change firing behavior, because GPNT
chunks are never added to weapon models except when explicitly built
as external models.
Co-Authored-By: Claude Fable 5 <[email protected]>
* warn on external weapon model conventions that are silently ignored
Two load-bearing conventions of the external weapon model feature had
no diagnostics:
- Only the first gun bank of an external model supplies firing points;
additional banks are silently ignored. Warn when an external model
has more than one gun bank.
- $Show Primary Models: / $Show Secondary Models: must list exactly
one bool per weapon bank; missing entries silently default to not
drawing. Warn when the list length doesn't match the bank count.
Co-Authored-By: Claude Fable 5 <[email protected]>
* orient external weapon models along their slot's firing normal
External weapon models were always rendered pointing ship-forward, and
reloading missiles slid backward along fixed -Z, regardless of the gun
or missile point's normal -- so a bank angled 45 degrees outward fired
along its normal while its mounted model pointed dead ahead. The
per-slot angle offset was also applied as a roll about ship-forward
rather than about the firing axis.
ship_get_weapon_model_slot_transform now builds the slot orientation
from the firing normal (identity for the usual dead-ahead normal, so
existing forward-facing mounts render exactly as before) with the
angle-offset roll applied about that axis, and slides reloading
missiles along the normal.
Correspondingly, ship_get_external_model_fp_offset now rotates the
external model's firing-point offset by the same slot transform, so
shots continue to emerge from the drawn barrels on angled mounts, and
now also respect the angle-offset roll (previously the offset was
added unrotated, mismatching rolled models).
Co-Authored-By: Claude Fable 5 <[email protected]>
* address review: same-frame spool-up gating; optional slot transform outputs
Move the update_external_weapon_spin() call from ship_process_post() to the top of ship_process_pre(). The update now happens before any firing attempt in the frame, so the spool-up firing gate sees the current frame's spin rate, as it did on master where the update happened inside the firing code. This also makes the ordering consistent for the AI regardless of Framerate_independent_turning.
Also make both outputs of ship_get_weapon_model_slot_transform() optional.
Co-Authored-By: Claude Fable 5 <[email protected]>
---------
Co-authored-by: Claude Fable 5 <[email protected]>
- Clean up `check_sexp_syntax` to use `eval_ship` (and incidentally `eval_wing` and `eval_prop`) rather than the old lookups.
- Disable SEXP node caching while FRED is running. Also use smart pointers and fix a cache memory leak.
* Fix path that could lead to unnormalized vector
TLDR:
Within `ai_chase` the value for `predicted_enemy_pos` could remain unitialized because not every else branch was accounted for. This could lead to downstream effects and trigger unnormalized vector asserts.
More Detail:
The trigger
`predicted_enemy_pos` is declared with no initializer — `aicode.cpp:8991`
The lead-prediction gate at `9110` falls through: needs `real_dot_to_enemy >= 0.25f and target_time >= 1.0f`, and not seek-lock-with-aspect.
`ai_get_weapon` returns `nullptr` — 9125 → 7113, which bails on `current_primary_bank < 0`
Every assignment lived inside `if (wip)`, and there was no else — so the variable stayed unwritten
First read of uninitialized stack memory at 9167
Passed to `ai_chase_attack` at 9198, becomes the goal point
Target beyond 500m selects the `rvec` branch, which routes into `vm_vector_2_matrix_norm` and the `fvec` assert
Commonly this uninitialized value would result in a plausible float. In that case nothing asserts and the ship just flies at a garbage point — so this was likely causing erratic no-primary AI behavior far more often than it was crashing. The assert only fired on the subset where the residue happened to be NaN, Inf, or astronomically large.
This PR adds that final else branch, though given it's an AI change it is via flag. The flag is auto-enabled for 26.2 and onward though.
Happy to discuss!
* fix typo
* no need for flag
Two unrelated weapon-indexed buffers dropped their MAX_WEAPON_TYPES dependency, by different means since they have different natural bounds:
* ai_select_secondary_weapon and update_aspect_lock_information declared weapon_id_list / weapon_bank_list as [MAX_WEAPON_TYPES], but these are filled only by get_available_secondary_weapons, which appends at most one entry per secondary bank -- so they never hold more than MAX_SHIP_SECONDARY_BANKS!
* parse_allowed_weapons' allowed_weapons[MAX_WEAPON_TYPES] is genuinely weapon-class-bounded, so it becomes an SCP_vector<int> filled by the vector overload of stuff_int_list
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
FRED2, qtFRED, and the shared mission save code each carried their own copy of the loadout usage builders. The weaponry pair already had shared versions in missioneditor/common; this converts those to the SCP_map<int,int> signature the editors now use, adds the missing ship builder alongside them, and deletes both editors' private copies.
FRED2's playerstarteditor/freddoc and qtFRED's Editor/ErrorChecker all call the shared functions now, following the update_custom_wing_indexes consolidation precedent. missionsave's used_pool becomes a map as well, which removes its MAX_WEAPON_TYPES stack array.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
team_data was the last persistence-adjacent structure sized by MAX_SHIP_CLASSES: four parallel ship arrays plus four parallel weapon arrays (one slot per mission-file loadout line, holding a class or a sexp variable name, and a count or a sexp variable name) with manual num_*_choices counters, plus a dense per-class weapon_required flag array. Convert all of it to the sparse-container family:
ship_list / ship_list_variables / ship_count / ship_count_variables
-> SCP_vector<loadout_entry> ship_choices
weaponry_pool / weaponry_count / weaponry_pool_variable /
weaponry_amount_variable
-> SCP_vector<loadout_entry> weapon_choices
bool weapon_required[MAX_WEAPON_TYPES]
-> SCP_set<int> required_weapons
where loadout_entry is {class_index, count, class_variable, count_variable}, with empty variable strings meaning the value was given literally. num_ship_choices / num_weapon_choices become .size().
Intentional behavior fixes, all previously latent memory errors or
data bugs:
* is_ship_assignable stored an alt ship CLASS in a variable used as a
loadout list INDEX when default_to_this_class was false, then
swapped to ship_list[class] - garbage for any mission where the two
domains diverge. It now swaps to the alt class itself, per the
evident intent.
* The default-ship fallback read ship_list[0] even when a team had
zero ship choices; it now leaves default_ship at -1, and
ss_fixup_team_data guards its append of the default ship accordingly.
* FRED2's dumpstats indexed Ships[] with a ship class when printing
loadout entries; it now prints the entry's class or variable name
directly.
* Team parse now clears the previous mission's entries explicitly
rather than relying on overwrite-in-place.
This clears the largest remaining cluster of MAX_SHIP_CLASSES references on the way to lifting the ship class limit.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Generalize the unreleased "$fix target updating with strafe:" ai_profiles flag
into "$fix ai target recovery:", which now also covers two more ways a ship
holding a standing chase order could park in AIM_NONE forever:
* a ship already in AIM_NONE with a standing chase order (attack-any etc.) now
clears its active goal so the order is re-processed, closing the window where
retargeting finds a new enemy before the strafe clause can fire
* target selection (find_enemy/get_nearest_objnum) now skips ships outside the
attacker's actively-pursues list, and ai_chase() drops self-chosen targets it
refuses, instead of holding a target it will never attack
Retail behavior is unchanged when the flag is disabled; explicit orders on
non-pursued targets are still declined as before.
Co-authored-by: Claude Fable 5 <[email protected]>
`sexp_percent_ships_arrive_depart_destroy_disarm_disable_scan` could have an edge case where the total value (ie the denominator) is 0 and thus crashes FSO. This could happen if `Cancel_future_waves_of_any_wing_launched_from_an_exited_ship` is used and the death or departure of a mothership triggers the total of a wing to change that is later checked in a sexp such a 'percent-destroyed` or other similar sexps.
Fix is just to add an early out if the total value is 0.
Root cause
Commit 87ea04fa5 "change atan2_safe to atan2" (July 2024) replaced FSO's custom atan2_safe with the standard atan2. The old function returned a different range:
range returned
old atan2_safe (-PI/2, 3PI/2)
standard atan2 (-PI, PI]
do_view_track_target() in playercontrol.cpp:417 computes the padlock heading as a raw subtraction of two extracted headings:
chase_slew_angles.h = forward_angles.h - view_angles.h;
The forward vector rotated into the player's own frame is always (0,0,1), so forward_angles.h is always PI/2. With the old range that subtraction landed in (-PI, PI) — symmetric, and the ±120° neck clamp below applied evenly. With standard atan2 it lands in [-PI/2, 3PI/2) instead: -90° to the left, but +270° to the right.
Leftward travel dead-ends at 90° (straight abeam) instead of 120°, and the moment a target crosses just past the player's left shoulder the value wraps to a large positive number and gets clamped to +120° — the camera snaps hard right.
The fix
Rather than reverting atan2_safe (standard atan2 is correct, and other callers depend on the new range), this PR wraps the heading difference back into (-PI, PI] before the neck clamp, using the same "over-the-top correction" idiom already used by do_view_external() a few hundred lines down:
// Do over-the-top correction.
// Headings are extracted with atan2, so each one lies in (-PI, PI] and their difference can be
// nearly a full circle in either direction. Without wrapping it back into (-PI, PI], a target just
// past the left shoulder reads as being almost all the way around to the right instead.
if (chase_slew_angles.h > PI)
chase_slew_angles.h -= PI2;
else if (chase_slew_angles.h < -PI)
chase_slew_angles.h += PI2;
The sexp tree refactor (#7301) moved the OPF listing logic into a shared
model that asks the editor for campaign-aware lists via virtual methods
on SexpTreeEditorInterface. QtFRED's campaign editor got the necessary
overrides, but FRED2's never did, so its OPF_MISSION_NAME /
OPF_GOAL_NAME / OPF_EVENT_NAME dropdowns fell back to the currently
loaded mission's data instead of the campaign's.
Backport the overrides to FRED2, restoring its pre-refactor behavior
(including listing only missions at earlier campaign levels), and hoist
QtFRED's loadAndFindCampaignMission helper into missioneditor/common so
both editors share one copy.
Co-Authored-By: Claude Fable 5 <[email protected]>
- A parse flag to indicate when the mission is being parsed, and a new `ship_class_index()` accessor in the ship registry, both currently unused
- Make the ship registry more robust, especially in FRED: properly update it when ships are renamed or deleted
Following PR #7429, ships belonging to wings were hashed the same way as every other ship, instead of the original hackish format where the hash was juggled around the number. This was a mostly transparent upgrade, and ship names were automatically converted from the legacy format to the new format. However, missions that referenced ships *in subsequent waves* broke; the converter only handled ships in the first wave.
This PR adds compatibility checks to `eval_ship` and `ship_registry_get` to accommodate legacy names.