* 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
* 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]>
* 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]>
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]>
* New guard-range sexp
New guard-range sexp which allows different ranges for different guarders
* cast value
* Clang
* bugfixes
* fix CI
* Address Feedback
* address feedback
* typo
TLDR: This is a long description, but it boils down to exposing 2 hardcoded AI values and updating 1 value in the optional 'better collision avoidance' behavior allows mods to substantially fix fighters ramming into capital ships they are attacking.
AI accidentally ramming into big ships while trying to evade fire from a hostile turret or a dogfight makes some sense given a chaotic combat situation. What does not seem good though is AI not being able to attack a large stationary target that is not shooting back and has no fighters around. This is especially evident in mods that use the new strafing AI fields, where AI will fly to a distant point then turn around to do an attack run, then once close enough pick another strafing run to conduct. In these situations, AI will commonly not pull up from the attack run in time, especially for mods that use different speeds or AI damp values compared to retail.
Fortunately, there exposing two values to the AI profiles allows modders to tune these values, the strafe retreat time and strafe retreat distance. By default these values are 2 seconds and 100 meters, which is commonly far too short for mods with higher speeds or larger damp values. Exposing these values allows modders to make the AI far more effective and virtually eliminate ramming into stationary ships they are attacking. For example, on current 26.0 with FotG testing we register 2-4 rams of a stationary target per minute. Using the new exposed values in this branch eliminates the ramming (after 10 minutes of firing no ramming occurs across multiple runs of this and other test missions).
Furthermore, this PR helps `better_collision_avoidance_triggered` function (enabled by using `better collision avoidance` in the `ai_profiles.tbl` fix an edge case with predicting collisions. On current master the `delta_time` value used in this function was `0`, which was only ever used eventually downstream in the `will_collide_with_big_ship` function call. This `will_collide_with_big_ship` function had an early return on line 7620:
```
int will_collide_with_big_ship(object *objp, vec3d *goal_point, object *big_objp, vec3d *collision_point, float delta_time)
{
float radius;
vec3d end_pos;
radius = big_objp->radius + delta_time * objp->phys_info.speed;
if (vm_vec_dist_quick(&big_objp->pos, &objp->pos) > radius) {
return 0;
}
```
This early return and the passing of `0` to the `delta_time` from `better_collision_avoidance_triggered` meant that the early out radius distance was only the big object's radius, and did not project any forward time. In other words, better collision avoidance only actually checked or triggered when the small ship was within the radius distance of the large ship. Thus, with ships that had geometry reach right to the edge of the object radius, it could result in the small ship not having enough time to react to avoid a collision. Notably, all other calls to `will_collide_with_big_ship` use a `delta_time` of 5-10. As such, this PR also fixes that edge case by ensuring the better collision avoidance also incorporates some lead travel time by passing a non-zero value for delta time. Given `better_collision_avoidance_triggered` aggression factor is 3.5 by default, it fit well in testing to pass this value to `delta_time`. That aggression factor is already tuneable for mods, so also using it here provides extra adaptability.
Overall these values and updates were tested and substantially remove collision accidents, especially with ships with high speeds and/or higher damps. The PR boils down to exposing two values to the AI profiles table and updating values in the optional 'better collision avoidance' flag, so it will not affect retail. Happy to discuss as well, thanks!
* Fix scripting copy-paste bug
The iterators for secondary banks for lua access had a copy-paste bug from primaries, so we should fix that. The early == -1 break masks it whenever a ship uses 3 or fewer secondary banks, which is probably why it went unnoticed.
Also add some additional low-hanging fruit optimizations.
* radar distance and other cleanup
* clang
Refactor `Ship_class_names`, `Iff_info_names`, and `Ai_class_names` to be populated once at parse time, as opposed to ad-hoc and possibly many times.
Also make `Ai_classes` and `Spawn_names` proper SCP_vectors.
Gunships and other custom ship classes may run through the primary weapon code despite having no primary banks. Add a few guards to prevent this from causing crashes.
Fixes a crash reported in Causality: The Great War.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Fix C4709 Warnings on latest VS
* more
* suppress false positive
* Switch to using any/none_of
* Address Feedback
* Fix Line endings?
* Fix commits and strip out flag_combinator
* more fixes
* nolint
* Fix AI Bug with Strafing
Bug:
When a fighter or bomber enters strafe mode attacking a large ship, that large ship may die or depart, which results in the fighter/bomber AI slowing down to 0 and then not moving until another order is issued. This is most apparent in mods that force strafe to be used more via the ai profile flags (such as FotG).
Cause:
This bug is caused by the following. A fighter/bomber whose strafe target is destroyed drops to AIM_NONE (see ai_execute_behavior) with its active_goal still set, so a standing order like attack-any is never re-processed and the ship slows to zero and remains still until another order is issued.
Fix:
If we're chasing or strafing against large ship but have lost our target, we need to clear the active goal to ensure ai_process_mission_orders() re-evaluates our orders next frame. Of course gate this behind a flag.
Tested and update fixes the issue. We are very much hoping this can be a part of a point-release since it is substantially needed for FotG missions and we want to release in the next month or so. Happy to answer any questions or discuss, too!
* target 26.2
The logic for offsetting a shot by the external weapon model's firing
points was duplicated in ship_fire_primary(), ship_fire_secondary(),
and the AI's line-of-sight check. Extract it into
ship_get_external_model_fp_offset().
This fixes two bugs that were present in all three copies:
1. The chain counter for "chain external model fps" weapons was reset
when it reached weapon_model->n_guns (the number of gun banks)
rather than the number of firing points in the bank being indexed.
For the typical model with one gun bank the counter reset after
every shot, so chaining never advanced past the first firing point;
for a model with several gun banks the counter could index past the
end of the first bank's point array.
2. The AI line-of-sight check advanced the chain counter even though it
is only a query, desyncing the counter from the shots actually
fired. It now reads the current firing point without advancing.
Co-Authored-By: Claude Fable 5 <[email protected]>
* avoid undefined behavior from UNREACHABLE on reachable code paths
UNREACHABLE expands to __assume(false) / __builtin_unreachable() in
release builds, which is undefined behavior if the code is ever actually
reached. Many UNREACHABLE sites only guard against unexpected-but-
possible runtime states (values from tables, mission/save files, network
packets, scripting, or user input), so reaching them should degrade
gracefully rather than invoke UB.
Replace UNREACHABLE with Assertion(false, ...) at all such sites. It
still pops an error in debug builds, but in release it is a harmless
no-op that simply falls through to the following code. Sites that are
genuinely unreachable by local invariants (e.g. a switch over a bounded
random number) are left as UNREACHABLE.
A few converted sites had no safe fall-through -- an uninitialized read,
null dereference, or null call would have followed -- so they also get a
minimal recovery: initialize the value, guard the dereference, return
early, or skip the offending item.
Where the converted site was a simple guard of the form
if (x) { Assertion(false, ...); return; }
hoist the check onto the invariant instead:
Assertion(!x, ...);
if (x) { return; }
This states the expectation directly and shows the offending value in
the debug popup, while keeping the same graceful runtime degradation.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* addendum
After consultation with Lafiel, use UNREACHABLE in situations that are effectively impossible even if not provably impossible. UNREACHABLE = "this shouldn't ever happen"; Assertion = "if this ever happens, it's a coding error". Also fix a few other sites and change a couple to Warnings.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Since FS1 retail, a large ship could get permanently stuck circling its
final waypoint. Waypoint completion requires the ship center to pass
within MIN_DIST_TO_WAYPOINT_GOAL + sqrt(radius) of the point (~37m for
a 1000m-radius capital ship), and the check only runs when the ship
covers 0.1m in a single frame (a framerate-dependent minimum speed).
At any speed that passes that gate, a capital ship turning at
2PI/(3*rotation_time) has a turning circle of hundreds of meters, so
once it overshoots a misaligned final leg it can never re-aim into the
completion bubble: an endless overshoot-turnaround-miss loop.
Add ai_profiles flag "$fix big ship waypoint completion:"
(Fix_big_ship_waypoint_completion), enabled by default for mods
targeting 26.2. With the flag set:
- big/huge ships complete a waypoint anywhere within their own radius
of it (matching what the docking path code has always done), while
small ships keep the retail sqrt(radius) tolerance; and
- the completion check runs every frame instead of requiring 0.1m of
travel per frame, removing the framerate-dependent minimum speed
(which at 240fps is 24 m/s, enough to affect fighters too).
With the flag unset, behavior is unchanged from retail.
Co-Authored-By: Claude Fable 5 <[email protected]>
If the carrier a fighter launched from was destroyed earlier in the
mission, its Ships[] entry retains stale bay door state while its
object and model instance are already freed (objnum/model_instance_num
== -1). When the fighter later departs, ai_manage_bay_doors would still
try to animate the dead carrier's bay doors, calling
model_get_instance(-1) and asserting/crashing.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
* framework for support ship rearm pool and flags
* scripting api
* FRED support
* QtFRED support
* do not save non-player-allowed weapons
* tvt and mission versioning
* fix variable name
* minor fixes and clarity
* oops
* add qtfred help docs
* fixes and cleanup
* clang and readability
* address feedback
* address feedback
* new mine weapon type
* some fixes and cleanup
* fix some parsing issues
* make sure mines get plotted
* make mine spawned objects can get a correct target
* some cleanup and additional features
* chasing mines
* some Lua tweaks
* generalize proximity detonations
* minor tweaks
* collapse namespace
* address feedback
Testing in FotG exposed a bug that dates back to retail. During mission parsing, arrival and departure anchors were represented by indexes into `Parse_names`. After parsing, they were converted to ship indexes -- but only in FRED. Since some functions are shared between FRED and FS2, this can produce inconsistent results, which can be hard to detect since parse indexes and ship indexes are usually the same.
This refactors arrival and departure anchors to be represented by ship registry indexes in all cases. It also adds conversion functions for the benefit of FRED and QtFRED since those dialogs were designed with ship indexes in mind.
The `$better combat collision avoidance for fightercraft:` flag is incredibly useful in preventing AI from hitting large ships while attacking, and the ability to tune the collision avoidance aggression factor further helps mods tune behavior based on their ship speeds (tuning that value is via `+combat collision avoidance aggression for fightercraft:` ). Nevertheless, the improve collision avoidance still is not performed for that actual fightercraft's actual target, so there can be situations where a fast fighter attacks a large ship with high speed, and then when that fighter break off from the attack they turn directly into the large ship's hull. Allowing the target to be incorporated into the `better_collision_avoidance_triggered` check prevents this from happening. To allow the target to be a part of the checks we just simply need to not pass an 'ignor_ship` argument to `maybe_avoid_big_ship`.
This PR adds that ability via flag. This new flag is tested and works as expected. For example, without the flag a squadron of TIEs in FotG would literally ram themselves to death attacking some of our larger capital ships because they would break off attack too late, but with this flag they no longer accidentally collide at all. In discussion with Asteroth it was decided to make this enhanced behavior behind a flag, too.
This PR also fixes a small oversight from the original #2810 where `next_check_time` was never actually used within the `else` block of `maybe_avoid_big_ship`.
* Expose value of idle circling with 'attack-any'
The attack-any order is extremely useful and utilized order, and there was one aspect that was ripe for exposure to modders: the `CHASE_CIRCLE_DIST` that was used to determine how big of a circle to fly around while the AI waited for new hostiles to arrive. For ships with high speeds, like FotG and others, the default radius of 200 m was a bit on the small side, which lead to ships flying in tight circles. This was was counterproductive to goals of tuning the AI to have a more cinematic style, such as where AI would fly around in large distances while waiting for enemies to arrive. Fortunately, this value is easy to expose and allow modders to tune with just a new ai_profiles value.
Tested and works as expected.
* improve name and use correct distance
* Back in business hopefully
* Refactor
* Resolve CI issue in aigoals.cpp
* Updating order.cpp
* Whoops
* Updated objecttypes.tbl to incorporate attack ship type
* Fix clang-tidy issue
* Forgot one other instance
---------
Co-authored-by: wookieejedi <[email protected]>
Currently, the `$better combat collision avoidance for fightercraft:` flag only checks for possible big ship collisions when in normal chase mode (`AIM_CHASE`), but strafing a big ship is a separate AI mode (`AIM_STRAFE`). Per discussion with Asteroth, strafe attacking should also check for better collision avoidance, as the modder likely would expect that to fall under this AI flag. Also took the chance to make the collision avoidance check into a function to improve maintenance and usability.
1. Allow "Stay near me" and "Stay near my target" to work even if the teams do not match
2. Allow "Stay near my target" to be issued regardless of team
3. Avoid divide-by-zero errors when running `ai_stay_near()`
* One fix and one enhancement to AI shield behavior
This PR adds one fix and one enhancement to AI shield behavior.
1) Fix: Testing, especially in FotG, revealed an edge-case bug with shield management where the AI will not direct shield energy to a quadrant if it was hit when that quadrant was down. In other words, if a shield quadrant is knocked down to 0 and continues to be shot the AI will not realize it needs to direct energy to that quadrant. This is because `quadrant` is set to -1 if a shield segment is down. This ensures that the resulting effects and damage calculations properly ignore that quadrant downstream, but it also results in `aip->last_hit_quadrant` being set to -1. This PR adds a fix to properly record which shield was actually hit, so that the AI knows to direct energy to that quadrant.
2) Enhancement: In combat situations with fire coming from many directions, it is good practice to keep shields equalized, so this PR adds a flag for the AI to always attempt to balance shields, even if it is being fired upon (instead of the current behavior to always direct energy to the hit shield quadrant).
Both flags are tested and work as expected.
* fix copy paste
* rename to avoid previous confusion
* also make as ai class flag
Two updates to the energy transfer system.
1. Previously, for fighters missing a system (e.g. shields), the energy would be treated as if all energy from that system was routed to the other systems. So, for example, a fighter's typical speed would be faster without shields than with shields. A new AI profile flag now treats the other systems as if the energy had not been transferred.
2. At some point in development, Volition intended the `$Power Output` ships.tbl field to affect ETS. A new AI profile flag now treats the power output as a factor of energy applied to energy systems.
1. Add an explicit `mn.createWaypointList` function
2. Allow `mn.createWaypoint` to be used with an empty waypoint list
3. Update documentation of `mn.createWaypoint` to clarify optional arguments
* Fix some issues preventing various secondary types from firing in the lab
* Add ability to fire turrets from the lab
* const char*, mjn please
* more cleanup
* range-based for loop
* Update comment
Although normalized vectors in FSO are supposed to stay normalized, occasionally small floating point errors can pop up. In most cases these do not cause a problem, but in certain cases these errors can rapidly accumulate and corrupt an otherwise well-behaved matrix. This can most easily be seen with daisy-chained dock sequences or docked ships with angled dockpoints.
This PR audits the usage of `vm_vector_2_matrix_norm` to re-normalize vectors in cases where floating point errors could accumulate. There are also a few new Assertions for extra vigilance.
Follow-up to #6596. Fixes#6634.
Most places in the code that use `vm_vector_2_matrix` do so on already-normalized vectors. Changing these to `vm_vector_2_matrix_norm` avoids redundant normalization.
* Unify AI Usage of Shield Manage Delay
`$AI Shield Manage Delay:` can be set in the AI profiles, but also set for AI specific classes. Currently, The values set for the specific AI class are only used `if (iff_x_attacks_y(Player_ship->team, Ships[objp->instance].team))`, otherwise the values in the AI profile are used.
Being able to have the shield delay work in a unified way according to the AI class values is very useful, especially for friendly wingmen that you want have different shield manage delays.
This PR adds a AI profiles flag to unify Shield Manage Delay behavior, so that it always works in the same way regardless of AI team conditions.
* cleanup unifying behavior along with CM firing chance
* restore what sushi had for now