Compare commits

...
Author SHA1 Message Date
SirKnightly 9d431ff824 Automated build commit 2026-07-21 00:25:23 +00:00
Goober5000 28df181995 patch mission format compatibility for new support features (#7624)
Although #7249 added the new support features to `check_for_25_1_data()`, the new fields were still written to the mission file in all formats except retail.  Use a version check when writing the fields too.
2026-07-20 16:52:40 -04:00
Goober5000 6dbe720fe0 Off is 1286 2026-07-19 23:49:28 -04:00
Goober5000 7ad97097d5 when disbanding a wing, delete its reinforcement entry
Also rename `remove_wing` to `disband_wing` for clarity.
2026-07-19 23:49:05 -04:00
Goober5000 2d1f4e2411 fix an Assertion and a latent campaign option 2026-07-19 23:48:42 -04:00
Goober5000andClaude Fable 5 037c53d853 add briefing icon ID collision validation to qtFRED
FRED2 rejects an icon ID edit that would collide with another icon,
warning and reverting the field, but qtFRED's setIconId() applied the
value unconditionally. Two icons could be given the same ID in a stage
(making find_icon and forward propagation ambiguous), or a global rename
could merge two distinct icon chains by reusing a downstream ID.

setIconId() now mirrors FRED2:

- An ID is a single-icon concept, so it operates on the current icon
  rather than the whole selection (applying one ID to several selected
  icons would itself create a same-stage collision).

- Rejects an ID already used by another icon in the current stage.

- When propagating forward (Change Locally off), rejects an ID already
  used in a later stage, since the rename would merge two icons.

- Otherwise applies the change and renames the icon's forward copies by
  matching the old ID, respecting Change Locally.

It returns false when a change is rejected; the dialog resets the spin
box to the model's value. The spin box is switched to non-keyboard-
tracking so validation runs when the edit is committed (focus-out/Enter)
rather than on every keystroke, avoiding a modal warning on a transient
value mid-typing.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 23:43:36 -04:00
Goober5000andClaude Fable 5 17ad536beb fix minor briefing icon editor issues in FRED
- OnMakeIcon() left closeup_label uninitialized. The icon arrays are
  only cleared at mission reset, and delete_icon shifts structs down
  leaving stale copies behind, so a newly created icon could inherit a
  deleted icon's closeup label and then propagate/save it. Clear it,
  matching qtFRED's makeIcon.

- Case-only label edits (e.g. "alpha" to "Alpha") did not propagate
  forward: the change detection used stricmp, so a case-only change was
  treated as no change and applied only locally. Use strcmp for both
  label and closeup_label. qtFRED already compares case-sensitively.

- OnPropagateIcons() propagated a snapshot that could predate the text
  typed into the icon fields, since it never committed pending dialog
  edits first. Call update_data(1) before propagating.

- copy_stage() copied every stage field except draw_grid, so inserting,
  deleting, or reordering stages reset the grid-disabled flag. Copy it
  through (and default it to true for freshly created stages). qtFRED's
  copyStageData already preserves it.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 23:43:24 -04:00
Goober5000andClaude Fable 5 452814e9de prevent invalid briefing icon IDs in FRED and qtFRED
- Negative icon IDs are now rejected. FRED2 assigned the ID edit box
  value unconditionally even though it only validated collisions for
  non-negative values, so a negative ID could slip through and silently
  break find_icon()/propagation. It now reverts to the previous ID (with
  a warning, but only when the value was actually changed, to avoid a
  message-box loop on refresh). qtFRED gets a matching defensive guard in
  setIconId().

- OnMakeIcon() now skips IDs that are already in use. It previously used
  Cur_brief_id++, but Cur_brief_id is only advanced at mission parse, so
  a manual ID edit that raised an icon ID above it would cause the next
  new icon to silently collide. The new get_unused_briefing_icon_id()
  helper advances Cur_brief_id past any value already taken by an icon in
  the current briefing. qtFRED's makeIcon() already computes maxId + 1,
  which inherently skips existing IDs.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 23:43:06 -04:00
Goober5000andClaude Opus 4.8 9c78d15dd9 fix briefing icon propagation bugs in FRED
- get_next_icon() used `stage_loop > num_stages` as its terminating
  check, allowing it to read one stage past the live range. On a full
  MAX_BRIEF_STAGES briefing this is a genuine out-of-bounds array
  access; below capacity it reads a non-live stage. Changed to `>=`.

- Icon scale_factor and the editable flag bits (highlight, mirror,
  wing, cargo) were always written locally, ignoring the "Change
  Locally" checkbox and never propagating forward like the other
  attributes. Route them through the same reset_icon_loop/get_next_icon
  propagation so they match id/label/type/team/ship_class (and qtFRED).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-19 23:42:54 -04:00
wookieejedi 4ea8885df1 Fix AI Bug with Strafing (#7619)
* 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
2026-07-19 23:36:31 -04:00
wookieejedi 22b6eff659 Fix fighter/bomber AI remaining stationary if engines repaired (#7609)
* draft both options

* cleanup

* authors

* whitespace
2026-07-19 23:35:02 -04:00
Goober5000andClaude Fable 5 8e308d4e34 Fix heap overrun in texture readback for the model transparency scan (#7616)
* Fix heap overrun in texture readback for the model transparency scan

gr_opengl_get_bitmap_from_texture() sized its readback buffer from the
texture cache slot dimensions and the bmpman frame count, but
glGetTexImage() returns whatever the bound GL texture actually contains.
When texture detail below maximum culls textures on upload, the two can
disagree, and the readback overruns the buffer (observed as heap
corruption when loading Zeus.pof with Texture Detail: Minimum).  Size
the readback from the dimensions and layer count OpenGL itself reports
for the texture, and pad the staging buffer to 4x4 block granularity in
case a driver writes tiny compressed mips back in whole blocks.

The readback now also allocates the returned buffer and reports the
dimensions of the data it returns, because the second half of the bug
was on the consumer side: bitmap_lookup indexed the readback data by
the bitmap's own dimensions, so with any culling in effect the
transparency scan read mostly uninitialized memory.  bitmap_lookup (and
its cache) now carries the actual readback dimensions, and it skips the
texture cleanly if the readback fails.

Also pairs the cache's vm_malloc allocations with vm_free instead of
free, and no longer ignores a failed tcache_set before reading back.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Harden the transparency-scan texture readback

Return fully opaque from bitmap_lookup::get_channel_alpha() when the
bitmap has no alpha channel, since indexing channel 3 of a 3-channel
buffer reads out of bounds (currently unreachable because the only
caller checks bm_has_alpha_channel() first, but no longer a trap).

Per review, also assert at maximum texture detail, where no culling
occurs on upload, that the readback dimensions OpenGL reports match the
texture cache slot, so any future slot mismatch is caught immediately
in debug builds instead of surfacing as corruption.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Fix inverted dimension check in opengl_export_render_target

The validation rejected the export only if both the width and the
height mismatched the texture cache slot, so a texture wrong in one
dimension passed through into opengl_get_texture, which trusts the
caller-supplied dimensions for its mipmap loop.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-19 23:32:58 -04:00
Goober5000andClaude Fable 5 bb4b8cf792 fix Lua weapon bank setter writing primary fields for secondary banks
The SWH_SECONDARY case of the ship_bank setter was copy-pasted from the
primary case and still assigned primary_bank_* fields; it now assigns
the secondary_bank_* fields.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 23:29:47 -04:00
Goober5000andClaude Sonnet 4.6 64f7b603e5 fix prop selection using model geometry instead of bounding sphere in FRED
In both FRED2 and QtFRED, object_check_collision() only dispatched to
model_collide() for OBJ_SHIP and OBJ_START, leaving OBJ_PROP to fall
through to fvi_ray_sphere().  For flat, wide props the bounding sphere is
much larger than the visible geometry, making it impossible to
click-deselect without clicking far outside the model bounds.

Add an OBJ_PROP branch that calls model_collide() with the prop's model
and instance numbers, consistent with how OBJ_SHIP and OBJ_START are
handled, and move the mc.model_instance_num default initialisation above
the dispatch so the prop branch can override it with the real instance
number.  In FRED2, also add an early-out guard for unoccupied prop slots
(QtFRED already gates on Show_props).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-07-19 23:27:22 -04:00
Goober5000andClaude Opus 4.8 f8f337a195 fix a bug with player ships being demoted to common ships
If a player ship is changed to a regular ship, be sure that
Player_start_shipnum does not still point to the old ship.

Consolidate the several near-duplicate copies of the "repoint
Player_start_shipnum to a valid player start" logic, in both FRED and
qtFRED, into a single shared helper, ensure_valid_player_start_shipnum()
in missioneditor/common.cpp.

Also add some guards in situations where Player_start_shipnum might
be -1.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-19 23:23:56 -04:00
wookieejedi 3d706aa2d3 Skip particle parsing to speed up FRED loading (#7602)
FRED takes a long time opening mods that have a lot of particle entries in the particles table such as FotG (can take over a minute or more sometimes), so given FRED never renders or pages in particles, just skip the parsing.
2026-07-19 23:16:26 -04:00
wookieejedi 75a5a7e086 Fix shockwave damage bug (#7601)
* Fix two shockwave bugs

Bug:
Ship and SEXP created shockwaves with 0 speed last forever. A shockwave speed <= 0 is an "instant" shockwave, but the speed is used as a denominator for total_time. Dividing by 0 that creates in infinite shockwave (frozen at its initial radius of 1.0).

Fix:
Thus, add a safeguard to protect against the infinite duration. Note, ship-death and sexp shockwave run shockwave straight to shockwave_create(), whereas weapon shockwaves run weapon_do_area_effect() if <= 0 speed.

Bug:
It is possible the shockwave may have lifetime shorter than one frame (e.g. high speed, small radius), which by default results in shockwave applying no damage.

Fix:
Provide optional fix to ensure shockwave is not killed until after this frame's damage pass has run.

Tested and work as expected.

* just the 1expire fix

* comment cleanup
2026-07-19 23:07:45 -04:00
Goober5000andClaude Fable 5 0342fc305e fix big ship waypoint completion (the "spinning Orion" bug)
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]>
2026-07-19 22:48:12 -04:00
Goober5000andClaude Opus 4.8 44b14d363e Keep event annotations attached to their node across tree mutations (#7538)
Event annotations cache the tree item they point to as a handle (HTREEITEM in
FRED, QTreeWidgetItem* in qtFRED). The handle was silently invalidated whenever
the tree control deleted-and-recreated an item, so annotations got decoupled.

Fix: notify when a node's handle changes. move_branch reports old->new, and
free_node2 / the root-delete path report old->null (deleted). The event tree
remaps the annotation's handle to follow a move, or clears it on delete (the path
is left intact, so a cleared annotation is dropped at save but survives a Cancel).

Co-authored-by: Claude Opus 4.8 <[email protected]>
2026-07-18 11:25:38 -04:00
BMagnu f671153a86 Ensure props survive mission restarts (#7571) 2026-07-12 17:40:37 -04:00
BMagnu 46b77b69fa Move inline static to global static (#7572) 2026-07-12 17:40:37 -04:00
Goober5000 ed581be4a3 fix translation/rotation bugs (#7544)
For standard translation offsets, the wrong axis was being used for translation.  Fix that.  Also fix a string offset when parsing a custom axis.

Follow-up to #4582 and #3056.
2026-07-12 17:40:37 -04:00
Goober5000 303478fb58 clean up ship status after mission (#7523)
Create a `ship_level_close()` function and call it from `game_level_close()`.  Among other tasks, this clears out any PRESENT or DEATH_ROLL statuses to prevent SEXPs or scripts from obtaining references to ships that have been deleted.

Follow-up to #7522.
2026-07-12 17:40:37 -04:00
the-eandClaude Sonnet 5 fe922d2ec9 Fix crash in ai_manage_bay_doors from stale destroyed parent ship
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]>
2026-07-12 17:40:36 -04:00
Asteroth 8544acc951 change version (#7570) 2026-07-12 17:40:36 -04:00
Goober5000 76e9522e5a fix FRED indexing of Props (#7546)
Get the Prop class from the list, in the same way that a Ship class is gotten, which accounts for the no_fred flag.  Fixes #7545.
2026-07-12 17:40:36 -04:00
Asteroth cfc2156a12 no collision avoidance for kamikaze 2026-07-12 17:40:36 -04:00
wookieejedi 4a8bcc45d7 Fix CockpitFOV and CockpitFOVToggle missing in default table (#7547)
* Update camera.cpp

* order cleanup
2026-07-12 17:40:36 -04:00
Goober5000andClaude Opus 4.8 570d6bcec9 Fix FRED resetting the grid position after saving a mission (#7552)
When FRED2 saves a mission, OnSaveDocument() reloads it via
load_mission(MPF_FAST_RELOAD).  That reload calls clear_mission() ->
fred_render_init() -> create_default_grid(), which rebuilds The_grid as
the default origin-centered XZ plane, discarding any adjustments made in
the Adjust Grid dialog (e.g. the X-Z Plane Y Level).  The grid is editor
display state and is not stored in the mission file, so the reload had
nothing to restore it from.

The camera (view_pos/view_orient) is already preserved across this reload;
apply the same preserve-and-restore pattern to the grid by capturing its
orientation, center, dimensions, and square size before clear_mission()
and rebuilding it with create_grid() afterward.

qtfred is unaffected because its save path does not reload the mission.

Fixes #7548

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-12 17:40:36 -04:00
Goober5000andClaude Fable 5 5bb7eabbc7 fix briefing lines when deleting an icon (#7556)
When an icon is deleted, remove any lines that reference it, and fix the
indexes of lines that reference the icons shifted down to fill the gap.
Previously stale lines silently reattached to the wrong icons.  This
matches the behavior of qtFRED's deleteCurrentIcon.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-12 17:40:36 -04:00
Goober5000 9a952cc892 Merge pull request #7557 from Goober5000/fix/brief_stage_move
make brief_stage non-copyable and non-movable; provide swap() and reset()
2026-07-12 17:40:36 -04:00
Goober5000andClaude Fable 5 cff5b7327b two loadout fixes (#7558)
* fix stale color_to_draw causing draw_model_icon crash with model_id of -1

In draw_wing_block(), color_to_draw was initialized once outside the
slot loop while bitmap_to_draw was reset every iteration.  Once any
slot in the wing took the model-icon path, color_to_draw stayed
non-NULL for all remaining slots.  A later slot whose icon had no
model (model_index == -1) and no bitmap frame to draw (e.g. a carried
icon that has not moved yet, or an icon ani with missing frames) then
fell into the color branch and called draw_model_icon() with a model
index of -1, crashing in model_clear_instance().

Reset color_to_draw each iteration, and give draw_model_icon() the
same model_id guard that draw_model_rotating() already has.

Co-Authored-By: Claude Fable 5 <[email protected]>

* guard against NULL color_to_draw when an icon ani is missing frames

In draw_ship_icon_with_number() and wl_render_icon(), a bitmap-based
icon takes the bitmap branch initially, so color_to_draw is never
assigned.  If a hot/selected/disabled state then swaps in an icon ani
frame that does not exist (e.g. a single-frame or truncated icon ani),
the frame index is -1, the final dispatch falls into the color/model
branch, and gr_set_color_fast() dereferences a NULL color.

Fall back to the normal icon color in that case.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-12 17:40:36 -04:00
Goober5000andClaude Fable 5 631ef0ec82 call object_moved() in the FRED and qtFRED wing Align handlers (#7559)
The Align button repositions wing members by assigning objp->pos and objp->orient directly, skipping the post-move bookkeeping that the viewport drag and orient editor paths perform.  A wing member with a docked partner (e.g. an attached support ship) would be moved to its formation slot while the partner stayed behind with broken dock geometry.

Fix by calling object_moved() for each repositioned member, which resets the Docked_already_handled flags and drags docked partners along.  No-op for undocked ships.

Verified in FRED by docking a ship to a wing member and pressing Align.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-12 17:40:36 -04:00
Goober5000 fbd0b6704c fix wing display names in a single wave wing (#7565)
As noticed by @Achille44, wing display names were handled properly for multi-wave wings, but were inadvertently skipped for single-wave wings.
2026-07-12 17:40:36 -04:00
John Fernandez 9fb3b31a10 Mark Builds as 26.0.1
Public builds will have RC appended, and anyone testing their own build can easily verify that it's definitely from the point release branch.
2026-07-12 17:40:36 -04:00
65 changed files with 790 additions and 276 deletions

No files matched your search

+3 -3
View File
@@ -6,9 +6,9 @@ if (EXISTS "${PROJECT_SOURCE_DIR}/version_override.cmake")
include("${PROJECT_SOURCE_DIR}/version_override.cmake")
endif()
set_if_not_defined(FSO_VERSION_MAJOR 25)
set_if_not_defined(FSO_VERSION_MINOR 1)
set_if_not_defined(FSO_VERSION_BUILD 0)
set_if_not_defined(FSO_VERSION_MAJOR 26)
set_if_not_defined(FSO_VERSION_MINOR 0)
set_if_not_defined(FSO_VERSION_BUILD 1)
# A numeric revision e.g. the CI build number
set_if_not_defined(FSO_VERSION_REVISION 0)
+6
View File
@@ -176,6 +176,7 @@ namespace AI {
Dont_limit_change_in_speed_due_to_physics_whack,
Guards_ignore_protected_attackers,
Fix_standard_strafe,
Fix_target_updating_with_strafe,
Standard_strafe_used_more,
Unify_usage_ai_shield_manage_delay,
Fix_AI_shield_management_bug,
@@ -191,6 +192,11 @@ namespace AI {
Purge_player_issued_form_on_wing_after_subsequent_order,
Cancel_future_waves_of_any_wing_launched_from_an_exited_ship,
Fix_ignore_if_dead_flag,
Kamikaze_no_collision_avoidance,
Fix_big_ship_waypoint_completion, // a) big ships complete a waypoint within their radius rather than sqrt(radius);
// b) completion no longer requires moving 0.1m in a single frame (framerate-dependent)
Fix_shockwave_expire_before_do_damage, // shockwaves whose lifetime is shorter than one frame apply their area damage at least once before expiring
Fix_small_ai_recover_after_engines_repaired, // ensure small ship AI can switch back to useful AI modes if engines get repaired
NUM_VALUES
};
+17
View File
@@ -677,6 +677,8 @@ void parse_ai_profiles_tbl(const char *filename)
set_flag(profile, "$fix standard strafe:", AI::Profile_Flags::Fix_standard_strafe);
set_flag(profile, "$fix target updating with strafe:", AI::Profile_Flags::Fix_target_updating_with_strafe);
set_flag(profile, "$standard strafe used more:", AI::Profile_Flags::Standard_strafe_used_more);
if (optional_string("$standard strafe triggers under this speed:")) {
@@ -749,6 +751,13 @@ void parse_ai_profiles_tbl(const char *filename)
set_flag(profile, "$fix 'ignore if dead' flag:", AI::Profile_Flags::Fix_ignore_if_dead_flag);
set_flag(profile, "$no collision avoidance for kamikaze fighters:", AI::Profile_Flags::Kamikaze_no_collision_avoidance);
set_flag(profile, "$fix big ship waypoint completion:", AI::Profile_Flags::Fix_big_ship_waypoint_completion);
set_flag(profile, "$fix shockwave expiring before dealing damage:", AI::Profile_Flags::Fix_shockwave_expire_before_do_damage);
set_flag(profile, "$fix fighter/bomber AI recovers after engines repaired:", AI::Profile_Flags::Fix_small_ai_recover_after_engines_repaired);
// end of options ----------------------------------------
@@ -975,4 +984,12 @@ void ai_profile_t::reset()
if (mod_supports_version(26, 0, 0)) {
flags.set(AI::Profile_Flags::Fix_ignore_if_dead_flag);
}
if (mod_supports_version(26, 2, 0)) {
flags.set(AI::Profile_Flags::Kamikaze_no_collision_avoidance);
flags.set(AI::Profile_Flags::Fix_big_ship_waypoint_completion);
flags.set(AI::Profile_Flags::Fix_shockwave_expire_before_do_damage);
flags.set(AI::Profile_Flags::Fix_small_ai_recover_after_engines_repaired);
flags.set(AI::Profile_Flags::Fix_standard_strafe);
flags.set(AI::Profile_Flags::Fix_target_updating_with_strafe);
}
}
+63 -55
View File
@@ -1198,70 +1198,78 @@ void ai_big_chase()
}
// If a collision is expected, pull out!
float dist_normal_to_enemy;
if (!(The_mission.ai_profile->flags[AI::Profile_Flags::Kamikaze_no_collision_avoidance] && aip->ai_flags[AI::AI_Flags::Kamikaze])) {
if (vm_vec_mag_squared(&aip->big_attack_surface_normal) > 0.9) {
if (aip->ai_profile_flags[AI::Profile_Flags::Improved_subsystem_attack_pathing])
dist_normal_to_enemy = fl_abs((dist_to_enemy / vm_vec_dot(&vec_to_enemy, &aip->big_attack_surface_normal)));
else // I'm fairly sure this retail code VVV meant to use / instead of *
dist_normal_to_enemy = fl_abs((dist_to_enemy * vm_vec_dot(&vec_to_enemy, &aip->big_attack_surface_normal)));
} else {
// don;t have normal so use a conservative value here
dist_normal_to_enemy = 0.3f * dist_to_enemy;
}
float dist_normal_to_enemy;
// float time_to_enemy = dist_normal_to_enemy / Pl_objp->phys_info.speed * fl_abs(vm_vec_dot(&Pl_objp->phys_info.vel, &aip->big_attack_surface_normal));
// if (Framecount % 30 == 1) {
// mprintf(("normal dist; %.1f, time: %.1f\n", dist_normal_to_enemy, time_to_enemy));
// }
// since we're not in strafe and we may get a bad normal, cap dist_normal_to_enemy as MIN(0.3*dist_to_enemy, self)
// this will allow us to get closer on a bad normal
dist_normal_to_enemy = MAX(0.3f*dist_to_enemy, dist_normal_to_enemy);
if (dist_to_enemy < ATTACK_COLLIDE_BASE_DIST) {
// within 50m or 1sec
float time_to_enemy;
if (vm_vec_mag_squared(&aip->big_attack_surface_normal) > 0.9) {
if (Pl_objp->phys_info.speed > 0.1) {
time_to_enemy = dist_normal_to_enemy / fl_abs(vm_vec_dot(&Pl_objp->phys_info.vel, &aip->big_attack_surface_normal));
} else {
// a big time
time_to_enemy = 100.0f;
}
} else {
if (Pl_objp->phys_info.speed > 0.1) {
time_to_enemy = dist_normal_to_enemy / Pl_objp->phys_info.speed;
} else {
// a big time
time_to_enemy = 100.0f;
}
if (aip->ai_profile_flags[AI::Profile_Flags::Improved_subsystem_attack_pathing])
dist_normal_to_enemy = fl_abs((dist_to_enemy / vm_vec_dot(&vec_to_enemy, &aip->big_attack_surface_normal)));
else // I'm fairly sure this retail code VVV meant to use / instead of *
dist_normal_to_enemy = fl_abs((dist_to_enemy * vm_vec_dot(&vec_to_enemy, &aip->big_attack_surface_normal)));
}
else {
// don;t have normal so use a conservative value here
dist_normal_to_enemy = 0.3f * dist_to_enemy;
}
float speed_dist = MAX(0.0f, (Pl_objp->phys_info.speed-50) * 2);
if ((dist_normal_to_enemy < ATTACK_COLLIDE_AVOID_DIST + speed_dist) || (time_to_enemy < ATTACK_COLLIDE_AVOID_TIME) ) {
// get away, simulate crsh recovery (don't use avoid)
// accelerate_ship(aip, -1.0f);
big_ship_collide_recover_start(Pl_objp, En_objp, nullptr);
// aip->submode = SM_AVOID;
// aip->submode_start_time = Missiontime;
} else if ((dist_normal_to_enemy < ATTACK_COLLIDE_SLOW_DIST) || (time_to_enemy < ATTACK_COLLIDE_SLOW_TIME) ) {
// slow down
accelerate_ship(aip, -1.0f);
}
}
// float time_to_enemy = dist_normal_to_enemy / Pl_objp->phys_info.speed * fl_abs(vm_vec_dot(&Pl_objp->phys_info.vel, &aip->big_attack_surface_normal));
// if (Framecount % 30 == 1) {
// mprintf(("normal dist; %.1f, time: %.1f\n", dist_normal_to_enemy, time_to_enemy));
// }
/*
if ((dot_to_enemy > 1.0f - 0.1f * En_objp->radius/(dist_to_enemy + 1.0f)) && (Pl_objp->phys_info.speed > dist_to_enemy/5.0f)) {
if (might_collide_with_ship(Pl_objp, En_objp, dot_to_enemy, dist_to_enemy, (aip->targeted_subsys == NULL)*2.0f + 1.5f)) {
if ((Missiontime - aip->last_hit_time > F1_0*4) && (dist_to_enemy < Pl_objp->radius*2 + En_objp->radius*2)) {
// since we're not in strafe and we may get a bad normal, cap dist_normal_to_enemy as MIN(0.3*dist_to_enemy, self)
// this will allow us to get closer on a bad normal
dist_normal_to_enemy = MAX(0.3f * dist_to_enemy, dist_normal_to_enemy);
if (dist_to_enemy < ATTACK_COLLIDE_BASE_DIST) {
// within 50m or 1sec
float time_to_enemy;
if (vm_vec_mag_squared(&aip->big_attack_surface_normal) > 0.9) {
if (Pl_objp->phys_info.speed > 0.1) {
time_to_enemy = dist_normal_to_enemy / fl_abs(vm_vec_dot(&Pl_objp->phys_info.vel, &aip->big_attack_surface_normal));
}
else {
// a big time
time_to_enemy = 100.0f;
}
}
else {
if (Pl_objp->phys_info.speed > 0.1) {
time_to_enemy = dist_normal_to_enemy / Pl_objp->phys_info.speed;
}
else {
// a big time
time_to_enemy = 100.0f;
}
}
float speed_dist = MAX(0.0f, (Pl_objp->phys_info.speed - 50) * 2);
if ((dist_normal_to_enemy < ATTACK_COLLIDE_AVOID_DIST + speed_dist) || (time_to_enemy < ATTACK_COLLIDE_AVOID_TIME)) {
// get away, simulate crsh recovery (don't use avoid)
// accelerate_ship(aip, -1.0f);
big_ship_collide_recover_start(Pl_objp, En_objp, nullptr);
// aip->submode = SM_AVOID;
// aip->submode_start_time = Missiontime;
}
else if ((dist_normal_to_enemy < ATTACK_COLLIDE_SLOW_DIST) || (time_to_enemy < ATTACK_COLLIDE_SLOW_TIME)) {
// slow down
accelerate_ship(aip, -1.0f);
} else {
aip->submode = SM_AVOID;
aip->submode_start_time = Missiontime;
}
}
} */
/*
if ((dot_to_enemy > 1.0f - 0.1f * En_objp->radius/(dist_to_enemy + 1.0f)) && (Pl_objp->phys_info.speed > dist_to_enemy/5.0f)) {
if (might_collide_with_ship(Pl_objp, En_objp, dot_to_enemy, dist_to_enemy, (aip->targeted_subsys == NULL)*2.0f + 1.5f)) {
if ((Missiontime - aip->last_hit_time > F1_0*4) && (dist_to_enemy < Pl_objp->radius*2 + En_objp->radius*2)) {
accelerate_ship(aip, -1.0f);
} else {
aip->submode = SM_AVOID;
aip->submode_start_time = Missiontime;
}
}
} */
}
}
switch (aip->submode) {
+31 -5
View File
@@ -4936,14 +4936,24 @@ void ai_fly_to_target_position(const vec3d* target_pos, bool* pl_done_p=NULL, bo
}
float dist_to_cover_this_frame = (Pl_objp->phys_info.speed * flFrametime);
if ( (dist_to_goal < MIN_DIST_TO_WAYPOINT_GOAL) || dist_to_cover_this_frame > 0.1f ) {
// Retail's completion distance of sqrt(radius) is only ~32m for a capital ship with a 1000m radius --
// far smaller than such a ship's turning circle -- and retail also skipped the completion check
// entirely unless the ship covered 0.1m in a single frame, a framerate-dependent gate that excludes
// exactly the slow speeds at which a big ship could aim precisely. Together these could trap a
// capital ship in an endless overshoot-turnaround-miss loop at its final waypoint. With the flag
// set, big ships complete a waypoint anywhere within their own radius of it, and the check always runs.
bool fix_waypoint_completion = The_mission.ai_profile->flags[AI::Profile_Flags::Fix_big_ship_waypoint_completion];
float waypoint_tolerance = MIN_DIST_TO_WAYPOINT_GOAL + ((fix_waypoint_completion && sip->is_big_or_huge()) ? Pl_objp->radius : fl_sqrt(Pl_objp->radius));
if ( fix_waypoint_completion || (dist_to_goal < MIN_DIST_TO_WAYPOINT_GOAL) || dist_to_cover_this_frame > 0.1f ) {
vec3d nearest_point;
float r;
r = find_nearest_point_on_line(&nearest_point, &Pl_objp->last_pos, &Pl_objp->pos, target_pos);
if ( (dist_to_goal < (MIN_DIST_TO_WAYPOINT_GOAL + fl_sqrt(Pl_objp->radius) + dist_to_cover_this_frame))
|| (((r >= 0.0f) && (r <= 1.0f)) && (vm_vec_dist_quick(&nearest_point, target_pos) < (MIN_DIST_TO_WAYPOINT_GOAL + fl_sqrt(Pl_objp->radius)))))
if ( (dist_to_goal < (waypoint_tolerance + dist_to_cover_this_frame))
|| (((r >= 0.0f) && (r <= 1.0f)) && (vm_vec_dist_quick(&nearest_point, target_pos) < waypoint_tolerance)))
{
int treat_as_ship;
@@ -12437,6 +12447,13 @@ void ai_process_subobjects(int objnum)
}
}
}
}
// Goober5000 and wookieejedi - conversely, if the engines are no longer blown (e.g. after repair), resume normal chase behavior.
// The rest of the AI assumes SM_ATTACK_FOREVER implies blown engines,
// so don't leave the submode set after the engines recover, or the ship will never evade and will ignore being hit.
else if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_small_ai_recover_after_engines_repaired] && ((aip->mode == AIM_CHASE) && (aip->submode == SM_ATTACK_FOREVER))) {
aip->submode = SM_ATTACK;
aip->submode_start_time = Missiontime;
}
}
@@ -13789,6 +13806,10 @@ void ai_manage_bay_doors(object *pl_objp, ai_info *aip, bool done)
ship *parent_ship = &Ships[shipp->bay_doors_parent_shipnum];
// the parent may have already been destroyed/departed, leaving stale bay door state behind
if (parent_ship->objnum < 0 || parent_ship->model_instance_num < 0)
return;
if (done)
parent_ship->bay_doors_wanting_open--;
@@ -15492,8 +15513,13 @@ void ai_frame(int objnum)
En_objp = NULL;
}
if (aip->mode == AIM_CHASE) {
if (En_objp == NULL) {
if (aip->mode == AIM_CHASE || ((The_mission.ai_profile->flags[AI::Profile_Flags::Fix_target_updating_with_strafe]) && aip->mode == AIM_STRAFE)) {
// If we're chasing or strafing against large ship but have lost our target, clear the active goal
// to ensure ai_process_mission_orders() re-evaluates our orders next frame.
// Without this 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.
if (En_objp == nullptr) {
aip->active_goal = -1;
}
}
+26 -15
View File
@@ -83,7 +83,12 @@ static bool bm_inited = false;
static uint Bm_next_signature = 0x1234;
static int Bm_low_mem = 0;
SCP_map<int,ubyte*> bm_lookup_cache;
struct bm_lookup_cache_entry {
ubyte* data;
int width;
int height;
};
SCP_map<int, bm_lookup_cache_entry> bm_lookup_cache;
/**
* How much RAM bmpman can use for textures.
@@ -144,30 +149,31 @@ bitmap_slot* bm_get_slot(int handle, bool separate_ani_frames) {
// Declaration of private functions and templates(declared as static type func(type param);)
bitmap_lookup::bitmap_lookup(int bitmap_num):
Bitmap_data(NULL)
Bitmap_data(nullptr),
Width(0),
Height(0),
Num_channels(3)
{
if ( !bm_is_valid(bitmap_num) ) return;
Num_channels = 3;
if ( bm_has_alpha_channel(bitmap_num) ) {
Num_channels = 4;
}
bitmap_entry *be = bm_get_entry(bitmap_num);
Width = be->bm.w;
Height = be->bm.h;
auto cache_search = bm_lookup_cache.find(bitmap_num);
if (cache_search == bm_lookup_cache.end()) {
Bitmap_data = (ubyte*)vm_malloc(Width * Height * Num_channels * sizeof(ubyte));
// The texture in graphics memory may be smaller than the bitmap itself, e.g. when mipmap levels
// are culled at lower texture detail settings, so the readback reports the dimensions of the
// data it actually returns.
Bitmap_data = gr_get_bitmap_from_texture(bitmap_num, &Width, &Height);
gr_get_bitmap_from_texture((void*)Bitmap_data, bitmap_num);
bm_lookup_cache.insert({bitmap_num, Bitmap_data});
if (Bitmap_data != nullptr) {
bm_lookup_cache.insert({bitmap_num, {Bitmap_data, Width, Height}});
}
} else {
Bitmap_data = cache_search->second;
Bitmap_data = cache_search->second.data;
Width = cache_search->second.width;
Height = cache_search->second.height;
}
}
@@ -190,6 +196,11 @@ float bitmap_lookup::get_channel_alpha(float u, float v)
{
Assert( Bitmap_data != NULL );
// without an alpha channel there is nothing to look up, and indexing channel 3 would read out of bounds
if ( Num_channels < 4 ) {
return 1.0f;
}
int x = fl2i(map_texture_address(u) * (Width-1));
int y = fl2i(map_texture_address(v) * (Height-1));
@@ -198,7 +209,7 @@ float bitmap_lookup::get_channel_alpha(float u, float v)
void clear_bm_lookup_cache() {
for(auto &iter: bm_lookup_cache) {
free(iter.second);
vm_free(iter.second.data);
}
bm_lookup_cache.clear();
}
+39 -20
View File
@@ -56,6 +56,10 @@ APPLY_TO_FOV_T(-, sub)
// Used to set the default value for in-game options
static float fov_default = DEFAULT_FOV;
static float cockpit_fov_default = DEFAULT_FOV;
static bool cockpit_fov_toggle_default = false;
bool Use_cockpit_fov = false;
static SCP_string fov_display(float val)
{
@@ -73,6 +77,21 @@ static void parse_fov_func()
fov_default = value;
}
static void parse_cockpit_fov_func()
{
float value;
stuff_float(&value);
CLAMP(value, 0.436332f, 1.5708f);
cockpit_fov_default = value;
}
static void parse_cockpit_fov_toggle_func()
{
bool value;
stuff_boolean(&value);
cockpit_fov_toggle_default = value;
}
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto FovOption = options::OptionBuilder<float>("Graphics.FOV",
std::pair<const char*, int>{"Field Of View", 1703},
@@ -90,25 +109,6 @@ auto FovOption = options::OptionBuilder<float>("Graphics.FOV",
.parser(parse_fov_func)
.finish();
bool Use_cockpit_fov = false;
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto CockpitFOVToggleOption = options::OptionBuilder<bool>("Graphics.CockpitFOVToggle",
std::pair<const char*, int>{"Cockpit FOV Toggle", 1838},
std::pair<const char*, int>{"Whether or not to use a different FOV for cockpit rendering from normal rendering", 1839})
.category(std::make_pair("Graphics", 1825))
.default_val(false)
.change_listener([](bool val, bool) {
if (!val) {
COCKPIT_ZOOM_DEFAULT = VIEWER_ZOOM_DEFAULT;
}
return true; // This option will always persist so we never return false
})
.level(options::ExpertLevel::Advanced)
.bind_to(&Use_cockpit_fov)
.importance(61)
.finish();
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto CockpitFovOption = options::OptionBuilder<float>("Graphics.CockpitFOV",
std::pair<const char*, int>{"Cockpit Field Of View", 1840},
@@ -124,9 +124,28 @@ auto CockpitFovOption = options::OptionBuilder<float>("Graphics.CockpitFOV",
return true;
})
.display(fov_display)
.default_val(fov_default)
.default_func([]() { return cockpit_fov_default; })
.level(options::ExpertLevel::Advanced)
.importance(62)
.parser(parse_cockpit_fov_func)
.finish();
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto CockpitFOVToggleOption = options::OptionBuilder<bool>("Graphics.CockpitFOVToggle",
std::pair<const char*, int>{"Cockpit FOV Toggle", 1838},
std::pair<const char*, int>{"Whether or not to use a different FOV for cockpit rendering from normal rendering", 1839})
.category(std::make_pair("Graphics", 1825))
.default_func([]() {return cockpit_fov_toggle_default;})
.change_listener([](bool val, bool) {
if (!val) {
COCKPIT_ZOOM_DEFAULT = VIEWER_ZOOM_DEFAULT;
}
return true; // This option will always persist so we never return false
})
.level(options::ExpertLevel::Advanced)
.bind_to(&Use_cockpit_fov)
.importance(61)
.parser(parse_cockpit_fov_toggle_func)
.finish();
//*************************CLASS: camera*************************
+1 -1
View File
@@ -249,7 +249,7 @@ void photo_mode_compute_hud_layout()
if (param.type == photo_mode_param_type::INT_RANGE) {
snprintf(buf, sizeof(buf), "%s: %d", XSTR(param.label, param.xstr_id), param.max_val);
} else {
snprintf(buf, sizeof(buf), "%s: %s", XSTR(param.label, param.xstr_id), XSTR("Off", 1285));
snprintf(buf, sizeof(buf), "%s: %s", XSTR(param.label, param.xstr_id), XSTR("Off", 1286));
}
measure(buf);
}
+4 -1
View File
@@ -831,7 +831,10 @@ typedef struct screen {
std::function<void()> gf_clear_states;
std::function<void(int bitmap_handle, int bpp, const ubyte* data, int width, int height)> gf_update_texture;
std::function<void(void* data_out, int bitmap_num)> gf_get_bitmap_from_texture;
// Reads the texture uploaded for a bitmap back from graphics memory. Returns a vm_malloc'd buffer
// (freed by the caller with vm_free) and reports the dimensions of the returned data, which may be
// smaller than the bitmap if the texture was culled at upload; returns nullptr on failure.
std::function<ubyte*(int bitmap_num, int* width_out, int* height_out)> gf_get_bitmap_from_texture;
std::function<void(matrix4* shadow_view_matrix, const matrix* light_matrix, vec3d* eye_pos)> gf_shadow_map_start;
std::function<void()> gf_shadow_map_end;
+4 -2
View File
@@ -259,9 +259,11 @@ void gr_stub_update_texture(int /*bitmap_handle*/, int /*bpp*/, const ubyte *
{
}
void gr_stub_get_bitmap_from_texture(void* /*data_out*/, int /*bitmap_num*/)
ubyte* gr_stub_get_bitmap_from_texture(int /*bitmap_num*/, int* width_out, int* height_out)
{
*width_out = 0;
*height_out = 0;
return nullptr;
}
int gr_stub_bm_make_render_target(int /*n*/, int * /*width*/, int * /*height*/, int * /*bpp*/, int * /*mm_lvl*/, int /*flags*/)
+42 -15
View File
@@ -1413,15 +1413,20 @@ int opengl_get_texture( GLenum target, GLenum pixel_format, GLenum data_format,
return m_offset;
}
void gr_opengl_get_bitmap_from_texture(void* data_out, int bitmap_num)
ubyte* gr_opengl_get_bitmap_from_texture(int bitmap_num, int* width_out, int* height_out)
{
*width_out = 0;
*height_out = 0;
float u,v;
uint32_t array_index = 0;
gr_opengl_tcache_set(bitmap_num, TCACHE_TYPE_NORMAL, &u, &v, &array_index);
if ( !gr_opengl_tcache_set(bitmap_num, TCACHE_TYPE_NORMAL, &u, &v, &array_index) ) {
return nullptr;
}
auto *ts = bm_get_gr_info<tcache_slot_opengl>(bitmap_num, true);
GLenum pixel_format = GL_RGB;
GLenum data_format = GL_UNSIGNED_BYTE;
int bytes_per_pixel = 3 * sizeof(ubyte);
@@ -1431,24 +1436,46 @@ void gr_opengl_get_bitmap_from_texture(void* data_out, int bitmap_num)
bytes_per_pixel = 4 * sizeof(ubyte);
}
// We can't read a specific layer of the texture so we need to read the entire texture and then memcpy the right part from that...
int num_frames = 0;
bm_get_info(bitmap_num, nullptr, nullptr, nullptr, &num_frames);
if (!bm_is_texture_array(bitmap_num)) {
num_frames = 1;
Assertion(ts->texture_target == GL_TEXTURE_2D_ARRAY, "Unexpected texture target encountered!");
// The texture in graphics memory may not match the dimensions or frame count that bmpman reports for
// this handle, e.g. when mipmap levels are culled at lower texture detail settings, so size the
// readback from what OpenGL will actually write, not from the bitmap or the texture cache slot.
GLint gl_width = 0, gl_height = 0, gl_layers = 0;
glGetTexLevelParameteriv(ts->texture_target, 0, GL_TEXTURE_WIDTH, &gl_width);
glGetTexLevelParameteriv(ts->texture_target, 0, GL_TEXTURE_HEIGHT, &gl_height);
glGetTexLevelParameteriv(ts->texture_target, 0, GL_TEXTURE_DEPTH, &gl_layers);
if ( (gl_width < 1) || (gl_height < 1) || (gl_layers < 1) || (array_index >= static_cast<uint32_t>(gl_layers)) ) {
mprintf(("Cannot read bitmap %d (%s) back from its texture; OpenGL reports level 0 as %dx%d with %d layer(s) but layer %u is needed.\n",
bitmap_num, bm_get_filename(bitmap_num), gl_width, gl_height, gl_layers, array_index));
return nullptr;
}
// The size of a single frame in the array
auto slice_size = ts->w * ts->h * bytes_per_pixel;
std::unique_ptr<std::uint8_t[]> buffer(new std::uint8_t[num_frames * slice_size]);
// At maximum texture detail no culling occurs on upload, so the texture must match the dimensions
// recorded in the texture cache slot; if this trips, we have another slot mismatch.
Assertion((Detail.hardware_textures < 4) || ((gl_width == ts->w) && (gl_height == ts->h)),
"Texture readback size mismatch for bitmap %d (%s): OpenGL reports level 0 as %dx%d but the texture cache slot expects %dx%d.",
bitmap_num, bm_get_filename(bitmap_num), gl_width, gl_height, ts->w, ts->h);
Assertion(ts->texture_target == GL_TEXTURE_2D_ARRAY, "Unexpected texture target encountered!");
// The size of a single layer in the array
size_t slice_size = static_cast<size_t>(gl_width) * gl_height * bytes_per_pixel;
// We can't read a specific layer of the texture so we need to read the entire texture and then memcpy the right part from that...
// Some drivers write tiny compressed textures back in whole 4x4 block granularity, so pad each layer
// of the staging buffer to full blocks to keep any such overwrite within bounds.
size_t padded_slice_size = static_cast<size_t>((gl_width + 3) & ~3) * ((gl_height + 3) & ~3) * bytes_per_pixel;
std::unique_ptr<std::uint8_t[]> buffer(new std::uint8_t[padded_slice_size * gl_layers]);
// Copy the entire texture level into the bitmap
glGetTexImage(ts->texture_target, 0, pixel_format, data_format, buffer.get());
auto buffer_offset = array_index * slice_size;
memcpy(data_out, buffer.get() + buffer_offset, slice_size);
auto data_out = reinterpret_cast<ubyte*>(vm_malloc(slice_size));
memcpy(data_out, buffer.get() + array_index * slice_size, slice_size);
*width_out = gl_width;
*height_out = gl_height;
return data_out;
}
void gr_opengl_get_texture_scale(int bitmap_handle, float *u_scale, float *v_scale)
@@ -1494,7 +1521,7 @@ size_t opengl_export_render_target( int slot, int width, int height, int alpha,
return 0;
}
if ( (ts->w != width) && (ts->h != height) ) {
if ( (ts->w != width) || (ts->h != height) ) {
mprintf(("OpenGL ERROR: Passed width and height do not match values for texture!\n"));
return 0;
}
+1 -1
View File
@@ -83,7 +83,7 @@ void opengl_preload_init();
void opengl_kill_render_target(bitmap_slot* slot);
int opengl_make_render_target(int handle, int *w, int *h, int *bpp, int *mm_lvl, int flags);
int opengl_set_render_target(int slot, int face = -1, int is_static = 0);
void gr_opengl_get_bitmap_from_texture(void* data_out, int bitmap_num);
ubyte* gr_opengl_get_bitmap_from_texture(int bitmap_num, int* width_out, int* height_out);
size_t opengl_export_render_target( int slot, int width, int height, int alpha, int num_mipmaps, ubyte *image_data );
void opengl_set_texture_target(GLenum target = GL_TEXTURE_2D);
void opengl_set_texture_face(GLenum face = GL_TEXTURE_2D);
+6 -1
View File
@@ -106,7 +106,12 @@ void stub_clear_states() {}
void stub_update_texture(int /*bitmap_handle*/, int /*bpp*/, const ubyte* /*data*/, int /*width*/, int /*height*/) {}
void stub_get_bitmap_from_texture(void* /*data_out*/, int /*bitmap_num*/) {}
ubyte* stub_get_bitmap_from_texture(int /*bitmap_num*/, int* width_out, int* height_out)
{
*width_out = 0;
*height_out = 0;
return nullptr;
}
int stub_bm_make_render_target(int /*n*/, int* /*width*/, int* /*height*/, int* /*bpp*/, int* /*mm_lvl*/, int /*flags*/)
{
+45 -4
View File
@@ -20,6 +20,8 @@
#include "hud/hud.h"
#include "utils/unicode.h"
#include <utility>
#define MAX_TEXT_STREAMS 2 // how many concurrent streams of text can be displayed
// ------------------------------------------------------------------------
@@ -163,15 +165,54 @@ public:
bool draw_grid;
color grid_color;
brief_stage( )
: camera_time( 0 ), flags( 0 ), formula( -1 ), num_icons(0), icons(nullptr), num_lines(0), lines(nullptr),
draw_grid(true)
{
brief_stage( )
{
reset();
}
// Restore this stage to its default-constructed state. Does not free
// the icons/lines buffers; the caller is responsible for those.
void reset()
{
text.clear();
voice[ 0 ] = 0;
camera_pos = vmd_zero_vector;
camera_orient = vmd_identity_matrix;
camera_time = 0;
flags = 0;
formula = -1;
num_icons = 0;
icons = nullptr;
num_lines = 0;
lines = nullptr;
draw_grid = true;
grid_color = Color_briefing_grid;
}
// icons and lines are raw owning pointers, so the memberwise copy/move
// operations would alias those buffers across two stages; use swap() to
// rearrange stages, or copy the buffer contents explicitly
brief_stage(const brief_stage&) = delete;
brief_stage& operator=(const brief_stage&) = delete;
brief_stage(brief_stage&&) = delete;
brief_stage& operator=(brief_stage&&) = delete;
friend void swap(brief_stage& a, brief_stage& b)
{
std::swap(a.text, b.text);
std::swap(a.voice, b.voice);
std::swap(a.camera_pos, b.camera_pos);
std::swap(a.camera_orient, b.camera_orient);
std::swap(a.camera_time, b.camera_time);
std::swap(a.flags, b.flags);
std::swap(a.formula, b.formula);
std::swap(a.num_icons, b.num_icons);
std::swap(a.icons, b.icons);
std::swap(a.num_lines, b.num_lines);
std::swap(a.lines, b.lines);
std::swap(a.draw_grid, b.draw_grid);
std::swap(a.grid_color, b.grid_color);
}
};
class debrief_stage
+6
View File
@@ -4810,6 +4810,12 @@ int parse_wing_create_ships( wing *wingp, int num_to_create, bool force_create,
Ship_registry_map[p_objp->name] = static_cast<int>(Ship_registry.size() - 1);
}
}
// make sure we still handle wing display names in a single-wave wing
else if (wingp->has_display_name())
{
wing_bash_ship_name(p_objp->display_name, wingp->get_display_name(), wingp->total_arrived_count + wingp->red_alert_skipped_ships);
p_objp->flags.set(Mission::Parse_Object_Flags::SF_Has_display_name);
}
// also, if multiplayer, set the parse object's net signature to be wing's net signature
// base + total_arrived_count (before adding 1)
+1 -1
View File
@@ -268,7 +268,7 @@ int Fred_campaign_save::save_campaign_file(const char* pathname, const SCP_vecto
fout(")");
}
if (mission_fork) {
fout("Option: ", Campaign.missions[link.to].name);
fout("\n+Option: %s", Campaign.missions[link.to].name);
}
}
}
+21
View File
@@ -1,7 +1,9 @@
// methods and members common to any mission editor FSO may have
#include "common.h"
#include "globalincs/linklist.h"
#include "mission/missionparse.h"
#include "iff_defs/iff_defs.h"
#include "object/object.h"
#include "ship/ship.h"
// to keep track of data
@@ -145,3 +147,22 @@ void generate_weaponry_usage_list_wing(int wing_num, int* arr)
}
}
}
void ensure_valid_player_start_shipnum()
{
// nothing to do if the current player start is still valid
if (Player_start_shipnum >= 0 && Player_start_shipnum < MAX_SHIPS
&& Ships[Player_start_shipnum].objnum >= 0
&& Objects[Ships[Player_start_shipnum].objnum].type == OBJ_START) {
return;
}
// otherwise repoint to the first remaining player start, or -1 if there are none
Player_start_shipnum = -1;
for (auto *objp : list_range(&obj_used_list)) {
if (objp->type == OBJ_START) {
Player_start_shipnum = objp->instance;
break;
}
}
}
+5
View File
@@ -48,3 +48,8 @@ anchor_t target_to_anchor(int target);
void generate_weaponry_usage_list_team(int team, int* arr);
void generate_weaponry_usage_list_wing(int wing_num, int* arr);
// If Player_start_shipnum no longer refers to a valid player start ship, repoint it to the
// first remaining player start in the mission (or -1 if there are none). Call this after
// changing a ship to or from an OBJ_START via demotion, deletion, etc.
void ensure_valid_player_start_shipnum();
+44 -20
View File
@@ -2858,22 +2858,29 @@ int Fred_mission_save::save_mission_info()
// this is compatible with non-SCP variants - Goober5000
fout(" %d", (The_mission.support_ships.max_support_ships == 0) ? 1 : 0);
// version-gated support options; see also check_for_25_1_data()
auto more_support_options_version = gameversion::version(25, 1);
if (save_config.save_format != MissionFormat::RETAIL) {
if (optional_string_fred("+Disallow Support Rearm:")) {
parse_comments(2);
} else {
fout("\n+Disallow Support Rearm:");
if (The_mission.required_fso_version >= more_support_options_version) {
if (optional_string_fred("+Disallow Support Rearm:")) {
parse_comments(2);
} else {
fout("\n+Disallow Support Rearm:");
}
fout(" %d", The_mission.support_ships.disallow_rearm ? 1 : 0);
}
fout(" %d", The_mission.support_ships.disallow_rearm ? 1 : 0);
if (The_mission.required_fso_version >= more_support_options_version) {
if (optional_string_fred("+Allow Support Rearm Weapon Precedence:")) {
parse_comments(2);
} else {
fout("\n+Allow Support Rearm Weapon Precedence:");
}
if (optional_string_fred("+Allow Support Rearm Weapon Precedence:")) {
parse_comments(2);
} else {
fout("\n+Allow Support Rearm Weapon Precedence:");
fout(" %d", The_mission.support_ships.allow_rearm_weapon_precedence ? 1 : 0);
}
fout(" %d", The_mission.support_ships.allow_rearm_weapon_precedence ? 1 : 0);
}
// here be WMCoolmon's hull and subsys repair stuff
@@ -2892,12 +2899,14 @@ int Fred_mission_save::save_mission_info()
}
fout(" %f", The_mission.support_ships.max_subsys_repair_val);
if (optional_string_fred("+Support Rearm Pool From Loadout:")) {
parse_comments(1);
} else {
fout("\n+Support Rearm Pool From Loadout:");
if (The_mission.required_fso_version >= more_support_options_version) {
if (optional_string_fred("+Support Rearm Pool From Loadout:")) {
parse_comments(1);
} else {
fout("\n+Support Rearm Pool From Loadout:");
}
fout(" %d", The_mission.support_ships.rearm_pool_from_loadout ? 1 : 0);
}
fout(" %d", The_mission.support_ships.rearm_pool_from_loadout ? 1 : 0);
}
if (Mission_all_attack) {
@@ -3602,7 +3611,7 @@ int Fred_mission_save::save_objects()
fout(" %s", save_config.fred_alt_names[i]);
}
Assertion(save_config.fred_callsigns != nullptr, "save_config.fred_alt_names is null!");
Assertion(save_config.fred_callsigns != nullptr, "save_config.fred_callsigns is null!");
// optional callsign
if (save_config.save_format != MissionFormat::RETAIL && strlen(save_config.fred_callsigns[i])) {
@@ -4390,8 +4399,7 @@ int Fred_mission_save::save_players()
for (i = 0; i < Num_teams; i++) {
required_string_fred("$Starting Shipname:");
parse_comments();
Assert(Player_start_shipnum >= 0);
fout(" %s", Ships[Player_start_shipnum].ship_name);
fout(" %s", (Player_start_shipnum >= 0) ? Ships[Player_start_shipnum].ship_name : "<none>");
if (save_config.save_format != MissionFormat::RETAIL) {
if (Team_data[i].do_not_validate) {
@@ -4509,7 +4517,23 @@ int Fred_mission_save::save_players()
fout(")");
if (save_config.save_format != MissionFormat::RETAIL && !The_mission.support_ships.rearm_pool_from_loadout) {
// version-gated support options; see also check_for_25_1_data()
auto more_support_options_version = gameversion::version(25, 1);
bool has_rearm_pool_entries = false;
if (save_config.save_format != MissionFormat::RETAIL && The_mission.required_fso_version >= more_support_options_version && !The_mission.support_ships.rearm_pool_from_loadout) {
for (j = 0; j < weapon_info_size(); j++) {
// mirror the skip conditions used when writing the pool below
if (!Weapon_info[j].wi_flags[Weapon::Info_Flags::Player_allowed] || Weapon_info[j].disallow_rearm) {
continue;
}
if (The_mission.support_ships.rearm_weapon_pool[i][j] != -1) {
has_rearm_pool_entries = true;
break;
}
}
}
if (has_rearm_pool_entries) {
if (optional_string_fred("+Support Rearm Pool:", "$Starting Shipname:")) {
parse_comments(2);
} else {
+2 -2
View File
@@ -821,7 +821,7 @@ void brief_compact_stages()
Briefing->stages[num].num_icons = 0;
for ( i = num+1; i < Briefing->num_stages; i++ ) {
Briefing->stages[i-1] = Briefing->stages[i];
swap(Briefing->stages[i-1], Briefing->stages[i]);
}
Briefing->num_stages--;
continue;
@@ -832,7 +832,7 @@ void brief_compact_stages()
// completely clear out the old entries (if any) so we don't access them by mistake - taylor
if ( before > Briefing->num_stages ) {
for ( i = Briefing->num_stages; i < before; i++ ) {
Briefing->stages[i] = brief_stage();
Briefing->stages[i].reset();
}
}
}
+5 -1
View File
@@ -1560,8 +1560,12 @@ int restore_wss_data(ubyte *data)
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)
{
// Can't draw a non-model
if (model_id < 0)
return;
lighting_profiles::set_non_mission_profile non_mission_lighting_profile;
matrix object_orient = IDENTITY_MATRIX;
angles rot_angles = vmd_zero_angles;
+4
View File
@@ -1697,6 +1697,9 @@ void draw_ship_icon_with_number(int screen_offset, int ship_class)
ss_icon->model_index = model_load(sip, true);
mprintf(("SL WARNING: Had to attempt to page in model for %s paged in manually! Result: %d\n", sip->name, ss_icon->model_index));
}
// the icon bitmap can be invalid without a color having been chosen, e.g. when the icon ani is missing frames
if (color_to_draw == nullptr)
color_to_draw = &Icon_colors[ICON_FRAME_NORMAL];
gr_set_color_fast(color_to_draw);
graphics::line_draw_list line_draw_list;
@@ -2184,6 +2187,7 @@ void draw_wing_block(int wb_num, int hot_slot, int selected_slot, int class_sele
GR_DEBUG_SCOPE("Single ship");
bitmap_to_draw = -1;
color_to_draw = nullptr;
ws = &wb->ss_slots[i];
slot_index = wb_num*MAX_WING_SLOTS + i;
+3
View File
@@ -3132,6 +3132,9 @@ void wl_render_icon(int index, int x, int y, int num, int draw_num_flag, int hot
}
else
{
// the icon bitmap can be invalid without a color having been chosen, e.g. when the icon ani is missing frames
if (color_to_draw == nullptr)
color_to_draw = &Icon_colors[ICON_FRAME_NORMAL];
gr_set_color_fast(color_to_draw);
graphics::line_draw_list line_draw_list;
+2 -2
View File
@@ -1300,7 +1300,7 @@ void determine_submodel_movement(bool is_rotation, const char *filename, bsp_inf
if (in(p, props, axis_string))
{
if (get_user_vec3d_value(p + 20, movement_axis, true, sm->name, filename))
if (get_user_vec3d_value(p + strlen(axis_string), movement_axis, true, sm->name, filename))
{
if (!fl_near_zero(vm_vec_mag(movement_axis)))
vm_vec_normalize(movement_axis);
@@ -4075,7 +4075,7 @@ void submodel_canonicalize_translation(bsp_info *sm, submodel_instance *smi)
smi->canonical_prev_offset = smi->canonical_offset;
// get the vector
switch (sm->rotation_axis_id)
switch (sm->translation_axis_id)
{
case MOVEMENT_AXIS_X:
vm_vec_copy_scale(&smi->canonical_offset, &vmd_x_vector, smi->cur_offset);
+1 -1
View File
@@ -12808,7 +12808,7 @@ int sexp_is_iff_or_species(int n, bool iff)
// ship is in the EXITED state but probably in the process of exploding
else if (oswpt.has_shipp())
{
UNREACHABLE("With the addition of the ShipStatus::DEATH_ROLL state, this shouldn't happen");
Assertion(false, "With the addition of the ShipStatus::DEATH_ROLL state, this shouldn't happen");
return SEXP_KNOWN_FALSE;
}
// ship has vanished
+9
View File
@@ -243,6 +243,15 @@ namespace internal {
SCP_vector<int> handles;
for (auto const &name: bitmap_strings) {
// FRED never renders or pages in particle effects,
// so skip the (potentially expensive) animation loading here.
// Still record placeholder handle so the effect stays "defined" and
// can be resolved by name from the weapons/ships tables without spurious errors.
if (Fred_running) {
handles.push_back(0);
continue;
}
auto handle = bm_load_animation(name.c_str());
if (handle >= 0) {
handles.push_back(handle);
+3 -6
View File
@@ -473,9 +473,8 @@ int prop_create(const matrix* orient, const vec3d* pos, int prop_type, const cha
Error(LOCATION, "Cannot create prop %s; pof file is not valid", pip->name.c_str());
return -1;
}
if (pip->model_num == -1) {
pip->model_num = model_load(pip->pof_file.c_str());
}
pip->model_num = model_load(pip->pof_file.c_str());
polymodel* pm = model_get(pip->model_num);
@@ -613,9 +612,7 @@ static void prop_model_change(int n, int prop_type)
//polymodel_instance* pmi = model_get_instance(sp->model_instance_num);
// get new model
if (sip->model_num == -1) {
sip->model_num = model_load(sip->pof_file.c_str());
}
sip->model_num = model_load(sip->pof_file.c_str());
polymodel* pm = model_get(sip->model_num);
Objects[sp->objnum].radius = model_get_radius(pm->id);
+6 -5
View File
@@ -55,11 +55,12 @@ ADE_INDEXER(l_WeaponBankType, "number Index", "Array of weapon banks", "weaponba
idx--; //Lua->FS2
if(ADE_SETTING_VAR && newbank && newbank->isValid()) {
sb->sw->primary_bank_weapons[idx] = newbank->typeh.sw->primary_bank_weapons[idx];
sb->sw->next_primary_fire_stamp[idx] = timestamp(0);
sb->sw->primary_bank_ammo[idx] = newbank->typeh.sw->primary_bank_ammo[idx];
sb->sw->primary_bank_start_ammo[idx] = newbank->typeh.sw->primary_bank_start_ammo[idx];
sb->sw->primary_bank_capacity[idx] = newbank->typeh.sw->primary_bank_capacity[idx];
sb->sw->secondary_bank_weapons[idx] = newbank->typeh.sw->secondary_bank_weapons[idx];
sb->sw->next_secondary_fire_stamp[idx] = timestamp(0);
sb->sw->secondary_bank_ammo[idx] = newbank->typeh.sw->secondary_bank_ammo[idx];
sb->sw->secondary_bank_start_ammo[idx] = newbank->typeh.sw->secondary_bank_start_ammo[idx];
sb->sw->secondary_bank_capacity[idx] = newbank->typeh.sw->secondary_bank_capacity[idx];
sb->sw->secondary_bank_rearm_time[idx] = timestamp(0);
}
break;
case SWH_TERTIARY:
+20
View File
@@ -6914,6 +6914,25 @@ void ship_level_init()
Man_thruster_reset_timestamp = timestamp(0);
}
void ship_level_close()
{
// at this point ships have all gone through ship_delete;
// clean up any ships that are still present so we don't
// leave any stray references behind (e.g. for sexps)
for (auto &ship_entry : Ship_registry)
{
if (ship_entry.status == ShipStatus::PRESENT || ship_entry.status == ShipStatus::DEATH_ROLL)
{
ship_entry.cleanup_mode = (ship_entry.status == ShipStatus::DEATH_ROLL) ? SHIP_DESTROYED : SHIP_PRESENT_AT_MISSION_END;
ship_entry.status = ShipStatus::EXITED;
ship_entry.objnum = -1;
ship_entry.shipnum = -1;
}
}
ship_close_cockpit_displays(Player_ship);
}
/**
* Add a ship onto the exited ships list.
*
@@ -8745,6 +8764,7 @@ void ship_close_cockpit_displays(ship* shipp)
{
if (shipp && shipp->cockpit_model_instance >= 0) {
model_delete_instance(shipp->cockpit_model_instance);
shipp->cockpit_model_instance = -1;
}
for ( int i = 0; i < (int)Player_displays.size(); i++ ) {
+9 -7
View File
@@ -1688,6 +1688,7 @@ extern SCP_vector<wing_formation> Wing_formations;
extern void ship_init(); // called once at game start
extern void ship_level_init(); // called before the start of each level
extern void ship_level_close(); // called after the end of each level
//returns -1 if failed
extern int ship_create(matrix* orient, vec3d* pos, int ship_type, const char* ship_name = nullptr, bool standalone_ship = false);
@@ -1700,13 +1701,14 @@ extern void ship_delete( object * objp );
extern int ship_check_collision_fast( object * obj, object * other_obj, vec3d * hitpos );
extern int ship_get_num_ships();
#define SHIP_VANISHED (1<<0)
#define SHIP_DESTROYED (1<<1)
#define SHIP_DEPARTED_WARP (1<<2)
#define SHIP_DEPARTED_BAY (1<<3)
#define SHIP_DEPARTED ( SHIP_DEPARTED_BAY | SHIP_DEPARTED_WARP )
#define SHIP_DESTROYED_REDALERT (1<<4)
#define SHIP_DEPARTED_REDALERT (1<<5)
#define SHIP_VANISHED (1<<0)
#define SHIP_DESTROYED (1<<1)
#define SHIP_DEPARTED_WARP (1<<2)
#define SHIP_DEPARTED_BAY (1<<3)
#define SHIP_DEPARTED ( SHIP_DEPARTED_BAY | SHIP_DEPARTED_WARP )
#define SHIP_DESTROYED_REDALERT (1<<4)
#define SHIP_DEPARTED_REDALERT (1<<5)
#define SHIP_PRESENT_AT_MISSION_END (1<<6)
/**
* @brief Deletes and de-inits a ship.
+4
View File
@@ -1,4 +1,5 @@
#include "Random.h"
#include "RandomRange.h"
#include <limits>
#include <random>
@@ -8,6 +9,9 @@
#include "globalincs/pstypes.h"
namespace util {
thread_local std::mt19937 seeder {std::random_device()()};
namespace {
template <typename RngType>
class RandomImpl {
+4 -4
View File
@@ -54,6 +54,10 @@ size_t parse_number_list(T (&list)[N])
}
} // namespace
//Sampling a random_device is REALLY expensive.
//Instead of sampling one for each seed, create a pseudorandom seeder which is initialized ONCE from a random_device.
extern thread_local std::mt19937 seeder;
/**
* @brief Generic class for generating numbers in a specific range
*
@@ -77,10 +81,6 @@ class RandomRange {
ValueType m_minValue;
ValueType m_maxValue;
//Sampling a random_device is REALLY expensive.
//Instead of sampling one for each seed, create a pseudorandom seeder which is initialized ONCE from a random_device.
inline static thread_local std::mt19937 seeder {std::random_device()()};
public:
template <typename T, typename... Ts, typename = typename std::enable_if<(sizeof... (Ts) >=1 || !std::is_convertible<T, ValueType>::value) && !std::is_same_v<std::decay_t<T>, RandomRange>, int>::type>
explicit RandomRange(T&& distributionFirstParameter, Ts&&... distributionParameters)
+13 -1
View File
@@ -308,7 +308,13 @@ void shockwave_move(object *shockwave_objp, float frametime)
CLAMP(sw->radius, 0.0f, sw->outer_radius);
}
if ( sw->time_elapsed > sw->total_time ) {
// It is possible the shockwave may have lifetime shorter than one frame (e.g. high speed, small radius),
// which by default results in shockwave applying no damage.
// Provide optional fix to ensure shockwave is not killed until after this frame's damage pass has run.
bool sw_expired = sw->time_elapsed > sw->total_time;
bool sw_expire_fix = The_mission.ai_profile->flags[AI::Profile_Flags::Fix_shockwave_expire_before_do_damage];
if ( sw_expired && !sw_expire_fix ) {
shockwave_objp->flags.set(Object::Object_Flags::Should_be_dead);
return;
}
@@ -443,6 +449,12 @@ void shockwave_move(object *shockwave_objp, float frametime)
}
} // end for
// See the sw_expired check above: shockwave has now applied its final damage pass this frame, so let it die.
// With the flag off we already returned before the loop.
if ( sw_expired && sw_expire_fix ) {
shockwave_objp->flags.set(Object::Object_Flags::Should_be_dead);
}
}
/**
+87 -27
View File
@@ -432,7 +432,18 @@ void briefing_editor_dlg::update_data(int update)
if (m_last_icon >= 0) {
valid = (m_id != ptr->icons[m_last_icon].id);
if (m_id >= 0) {
if (m_id < 0) {
// briefing icon ids must never be negative; revert to the previous id
if (valid) {
char msg[1024];
sprintf(msg, "Icon ID #%d is invalid. Briefing icon IDs cannot be negative.\n"
"Icon ID has been reset back to %d", m_id, ptr->icons[m_last_icon].id);
m_id = ptr->icons[m_last_icon].id;
MessageBox(msg);
}
} else {
if (valid && !m_change_local) {
for (i=m_last_stage+1; i<Briefing->num_stages; i++) {
if (find_icon(m_id, i) >= 0) {
@@ -473,7 +484,7 @@ void briefing_editor_dlg::update_data(int update)
string_copy(buf, m_icon_label, MAX_LABEL_LEN - 1);
lcl_fred_replace_stuff(buf, MAX_LABEL_LEN - 1);
if (stricmp(ptr->icons[m_last_icon].label, buf) && !m_change_local) {
if (strcmp(ptr->icons[m_last_icon].label, buf) && !m_change_local) {
set_modified();
reset_icon_loop(m_last_stage);
while (get_next_icon(m_id))
@@ -483,7 +494,7 @@ void briefing_editor_dlg::update_data(int update)
string_copy(buf, m_icon_closeup_label, MAX_LABEL_LEN - 1);
lcl_fred_replace_stuff(buf, MAX_LABEL_LEN - 1);
if (stricmp(ptr->icons[m_last_icon].closeup_label, buf) && !m_change_local) {
if (strcmp(ptr->icons[m_last_icon].closeup_label, buf) && !m_change_local) {
set_modified();
reset_icon_loop(m_last_stage);
while (get_next_icon(m_id))
@@ -491,28 +502,33 @@ void briefing_editor_dlg::update_data(int update)
}
strcpy_s(ptr->icons[m_last_icon].closeup_label, buf);
if (m_icon_scale > 0)
ptr->icons[m_last_icon].scale_factor = m_icon_scale / 100.0f;
if (m_icon_scale > 0) {
float new_scale = m_icon_scale / 100.0f;
if ((ptr->icons[m_last_icon].scale_factor != new_scale) && !m_change_local) {
set_modified();
reset_icon_loop(m_last_stage);
while (get_next_icon(m_id))
iconp->scale_factor = new_scale;
}
ptr->icons[m_last_icon].scale_factor = new_scale;
}
if ( m_hilight )
ptr->icons[m_last_icon].flags |= BI_HIGHLIGHT;
else
ptr->icons[m_last_icon].flags &= ~BI_HIGHLIGHT;
// build the new flags for this icon from the checkboxes, preserving any non-editable bits
const int editable_flags = BI_HIGHLIGHT | BI_MIRROR_ICON | BI_USE_WING_ICON | BI_USE_CARGO_ICON;
int new_flags = ptr->icons[m_last_icon].flags;
if (m_hilight) new_flags |= BI_HIGHLIGHT; else new_flags &= ~BI_HIGHLIGHT;
if (m_flipicon) new_flags |= BI_MIRROR_ICON; else new_flags &= ~BI_MIRROR_ICON;
if (m_use_wing) new_flags |= BI_USE_WING_ICON; else new_flags &= ~BI_USE_WING_ICON;
if (m_use_cargo) new_flags |= BI_USE_CARGO_ICON; else new_flags &= ~BI_USE_CARGO_ICON;
if (m_flipicon)
ptr->icons[m_last_icon].flags |= BI_MIRROR_ICON;
else
ptr->icons[m_last_icon].flags &= ~BI_MIRROR_ICON;
if (m_use_wing)
ptr->icons[m_last_icon].flags |= BI_USE_WING_ICON;
else
ptr->icons[m_last_icon].flags &= ~BI_USE_WING_ICON;
if (m_use_cargo)
ptr->icons[m_last_icon].flags |= BI_USE_CARGO_ICON;
else
ptr->icons[m_last_icon].flags &= ~BI_USE_CARGO_ICON;
if ((ptr->icons[m_last_icon].flags != new_flags) && !m_change_local) {
set_modified();
reset_icon_loop(m_last_stage);
// apply only the editable bits to later icons, preserving their other flags
while (get_next_icon(m_id))
iconp->flags = (iconp->flags & ~editable_flags) | (new_flags & editable_flags);
}
ptr->icons[m_last_icon].flags = new_flags;
if ((ptr->icons[m_last_icon].type != m_icon_image) && !m_change_local) {
set_modified();
@@ -942,6 +958,7 @@ void briefing_editor_dlg::copy_stage(int from, int to)
Briefing->stages[to].camera_time = 500;
Briefing->stages[to].num_icons = 0;
Briefing->stages[to].formula = Locked_sexp_true;
Briefing->stages[to].draw_grid = true;
Briefing->stages[to].grid_color = Color_briefing_grid;
return;
}
@@ -956,6 +973,7 @@ void briefing_editor_dlg::copy_stage(int from, int to)
Briefing->stages[to].num_icons = Briefing->stages[from].num_icons;
Briefing->stages[to].num_lines = Briefing->stages[from].num_lines;
Briefing->stages[to].formula = Briefing->stages[from].formula;
Briefing->stages[to].draw_grid = Briefing->stages[from].draw_grid;
// For now let's just always set this back to default. Eventually when we have a UI color picker in qtFRED, we can copy from stage to stage
Briefing->stages[to].grid_color = Color_briefing_grid;
@@ -1089,12 +1107,13 @@ void briefing_editor_dlg::OnMakeIcon()
strncpy(biconp->label, name, len);
biconp->label[len] = 0;
biconp->closeup_label[0] = 0;
// iconp->text[0] = 0;
biconp->type = 0;
biconp->team = team;
biconp->pos = pos;
biconp->flags = 0;
biconp->id = Cur_brief_id++;
biconp->id = get_unused_briefing_icon_id();
biconp->scale_factor = 1.0f;
biconp->modelnum = -1;
@@ -1198,7 +1217,7 @@ void briefing_editor_dlg::OnDeleteIcon()
void briefing_editor_dlg::delete_icon(int num)
{
int i, z;
int i, l, z;
if (num < 0)
num = m_cur_icon;
@@ -1217,6 +1236,24 @@ void briefing_editor_dlg::delete_icon(int num)
m_cur_icon = -1;
update_data(1);
obj_delete(icon_obj[num]);
// remove any lines that reference the icon being deleted
i = Briefing->stages[m_cur_stage].num_lines;
while (i--)
if ((Briefing->stages[m_cur_stage].lines[i].start_icon == num) || (Briefing->stages[m_cur_stage].lines[i].end_icon == num)) {
Briefing->stages[m_cur_stage].num_lines--;
for (l=i; l<Briefing->stages[m_cur_stage].num_lines; l++)
Briefing->stages[m_cur_stage].lines[l] = Briefing->stages[m_cur_stage].lines[l+1];
}
// fix the indexes of lines that reference the icons being shifted down
for (i=0; i<Briefing->stages[m_cur_stage].num_lines; i++) {
if (Briefing->stages[m_cur_stage].lines[i].start_icon > num)
Briefing->stages[m_cur_stage].lines[i].start_icon--;
if (Briefing->stages[m_cur_stage].lines[i].end_icon > num)
Briefing->stages[m_cur_stage].lines[i].end_icon--;
}
for (i=num+1; i<Briefing->stages[m_cur_stage].num_icons; i++) {
Briefing->stages[m_cur_stage].icons[i-1] = Briefing->stages[m_cur_stage].icons[i];
icon_obj[i-1] = icon_obj[i];
@@ -1282,10 +1319,13 @@ int briefing_editor_dlg::check_mouse_hit(int x, int y)
return -1;
}
void briefing_editor_dlg::OnPropagateIcons()
void briefing_editor_dlg::OnPropagateIcons()
{
object *ptr;
// commit any pending edits in the icon fields before propagating the icons forward
update_data(1);
ptr = GET_FIRST(&obj_used_list);
while (ptr != END_OF_LIST(&obj_used_list)) {
if ((ptr->type == OBJ_POINT) && (ptr->flags[Object::Object_Flags::Marked])) {
@@ -1325,6 +1365,26 @@ int briefing_editor_dlg::find_icon(int id, int stage)
return -1;
}
// is the given id already used by any icon in any stage of the current briefing?
bool briefing_editor_dlg::briefing_icon_id_used(int id)
{
for (int s=0; s<Briefing->num_stages; s++)
if (find_icon(id, s) >= 0)
return true;
return false;
}
// find an id that isn't already used by an icon in the current briefing, advancing
// Cur_brief_id past any values that are already taken (e.g. from manual id edits)
int briefing_editor_dlg::get_unused_briefing_icon_id()
{
while (briefing_icon_id_used(Cur_brief_id))
Cur_brief_id++;
return Cur_brief_id++;
}
void briefing_editor_dlg::reset_icon_loop(int stage)
{
stage_loop = stage + 1;
@@ -1337,7 +1397,7 @@ int briefing_editor_dlg::get_next_icon(int id)
icon_loop++;
if (icon_loop >= Briefing->stages[stage_loop].num_icons) {
stage_loop++;
if (stage_loop > Briefing->num_stages)
if (stage_loop >= Briefing->num_stages)
return 0;
icon_loop = -1;
+2
View File
@@ -34,6 +34,8 @@ public:
void OnOK();
void OnCancel();
int find_icon(int id, int stage);
bool briefing_icon_id_used(int id);
int get_unused_briefing_icon_id();
void propagate_icon(int num);
void reset_editor();
int check_mouse_hit(int x, int y);
+15
View File
@@ -1674,6 +1674,21 @@ int event_annotation_lookup(HTREEITEM handle)
return -1;
}
// The tree recreated (moved) or deleted the item for a node. Re-point any
// annotation on the old handle to the new one so it follows the node; a null
// new_handle means the node was deleted, which clears the handle. The path is
// left intact, so an annotation cleared this way is dropped at save (its path
// can no longer be rebuilt) but survives a Cancel (the path still resolves).
void event_sexp_tree::on_node_handle_changed(HTREEITEM old_handle, HTREEITEM new_handle)
{
if (!old_handle)
return;
int i = event_annotation_lookup(old_handle);
if (i >= 0)
Event_annotations[i].handle = new_handle;
}
void event_annotation_swap_image(event_sexp_tree *tree, HTREEITEM handle, int annotation_index)
{
event_annotation_swap_image(tree, handle, Event_annotations[annotation_index]);
+4
View File
@@ -31,6 +31,10 @@ protected:
virtual void PreSubclassWindow();
virtual void OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult);
// keep event annotations attached to their node when the tree recreates or
// deletes its handle (new_handle == nullptr means the node was deleted)
void on_node_handle_changed(HTREEITEM old_handle, HTREEITEM new_handle) override;
CStringA m_tooltiptextA;
CStringW m_tooltiptextW;
+13
View File
@@ -36,6 +36,7 @@
#include "localization/fhash.h"
#include "localization/localize.h"
#include "mission/missiongoals.h"
#include "mission/missiongrid.h"
#include "mission/missionparse.h"
#include "object/object.h"
#include "render/3d.h"
@@ -233,6 +234,14 @@ bool CFREDDoc::load_mission(const char *pathname, int flags) {
Parse_viewer_pos = view_pos;
Parse_viewer_orient = view_orient;
// preserve the editor grid across the reload; it's display state, not part of the mission,
// but clear_mission() recreates it via fred_render_init(). (See the view_pos/view_orient handling above and below.)
matrix grid_orient = The_grid->gmatrix;
vec3d grid_center = The_grid->center;
int grid_nrows = The_grid->nrows;
int grid_ncols = The_grid->ncols;
float grid_square_size = The_grid->square_size;
// activate the localizer hash table
fhash_flush();
@@ -392,6 +401,10 @@ bool CFREDDoc::load_mission(const char *pathname, int flags) {
view_pos = Parse_viewer_pos;
view_orient = Parse_viewer_orient;
// restore the editor grid that was preserved above
create_grid(The_grid, &grid_orient.vec.fvec, &grid_orient.vec.rvec, &grid_center, grid_nrows, grid_ncols, grid_square_size);
set_modified(0);
stars_post_level_init();
+14 -6
View File
@@ -1424,21 +1424,29 @@ int object_check_collision(object *objp, vec3d *p0, vec3d *p1, vec3d *hitpos) {
return 0;
}
// PROP HERE?
if (objp->type == OBJ_PROP) {
if (!Props[objp->instance].has_value())
return 0;
}
if (objp->flags[Object::Object_Flags::Hidden, Object::Object_Flags::Locked_from_editing])
return 0;
if ((Show_ship_models || Show_outlines) && (objp->type == OBJ_SHIP)) {
mc.model_num = Ship_info[Ships[objp->instance].ship_info_index].model_num; // Fill in the model to check
mc.model_instance_num = -1;
} else if ((Show_ship_models || Show_outlines) && (objp->type == OBJ_START)) {
mc.model_num = Ship_info[Ships[objp->instance].ship_info_index].model_num; // Fill in the model to check
if ((Show_ship_models || Show_outlines) && (objp->type == OBJ_SHIP || objp->type == OBJ_START)) {
auto& shp = Ships[objp->instance];
mc.model_num = Ship_info[shp.ship_info_index].model_num; // Fill in the model to check
mc.model_instance_num = shp.model_instance_num;
} else if ((Show_ship_models || Show_outlines) && (objp->type == OBJ_PROP)) {
auto& prp = Props[objp->instance].value();
mc.model_num = Prop_info[prp.prop_info_index].model_num; // Fill in the model to check
mc.model_instance_num = prp.model_instance_num;
} else
return fvi_ray_sphere(hitpos, p0, p1, &objp->pos, (objp->radius > 0.1f) ? objp->radius : LOLLIPOP_SIZE);
mc.model_instance_num = -1;
mc.orient = &objp->orient; // The object's orient
mc.pos = &objp->pos; // The object's position
mc.p0 = p0; // Point 1 of ray to check
+8 -2
View File
@@ -32,6 +32,7 @@
#include "ai/aigoals.h"
#include "ship/ship.h" // for ship names
#include "prop/prop.h" // for prop names
#include "missioneditor/common.h"
#include "MissionGoalsDlg.h"
#include "MissionCutscenesDlg.h"
#include "wing.h"
@@ -2466,7 +2467,7 @@ int query_single_wing_marked()
void CFREDView::OnDisbandWing()
{
if (query_single_wing_marked()) {
remove_wing(cur_wing);
disband_wing(cur_wing);
FREDDoc_ptr->autosave("wing disband");
} else
@@ -2641,6 +2642,9 @@ int CFREDView::global_error_check()
ptr->type = OBJ_SHIP;
Player_starts--;
t--;
ensure_valid_player_start_shipnum();
if (error("Invalid ship type for a player. Ship has been reset to non-player ship.")){
return 1;
}
@@ -3130,7 +3134,9 @@ int CFREDView::global_error_check()
return -1;
}*/
Assert((Player_start_shipnum >= 0) && (Player_start_shipnum < MAX_SHIPS) && (Ships[Player_start_shipnum].objnum >= 0));
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");
}
i = global_error_check_player_wings(multi);
if (i){
return i;
+2 -13
View File
@@ -775,7 +775,7 @@ int create_object(vec3d *pos, int waypoint_instance, bool prop)
int obj, n;
if (prop) {
int prop_class = m_new_prop_type_combo_box.GetCurSel();
int prop_class = m_new_prop_type_combo_box.GetPropClass(m_new_prop_type_combo_box.GetCurSel());
if (prop_class < 0 || prop_class >= prop_info_size())
return -1;
@@ -1235,7 +1235,6 @@ int common_object_delete(int obj)
char msg[255];
const char *name;
int i, z, r, type;
object *objp;
SCP_list<CJumpNode>::iterator jnp;
type = Objects[obj].type;
@@ -1271,17 +1270,7 @@ int common_object_delete(int obj)
ai_do_objects_undocked_stuff(&Objects[obj], dock_get_first_docked_object(&Objects[obj]));
}
if (Player_start_shipnum == i) { // need a new single player start.
objp = GET_FIRST(&obj_used_list);
while (objp != END_OF_LIST(&obj_used_list)) {
if (objp->type == OBJ_START) {
Player_start_shipnum = objp->instance;
break;
}
objp = GET_NEXT(objp);
}
}
ensure_valid_player_start_shipnum(); // may need a new single player start
Player_starts--;
+12
View File
@@ -479,6 +479,11 @@ void sexp_tree::free_node2(int node)
Assert(tree_nodes[node].type != SEXPT_UNUSED);
Assert(total_nodes > 0);
*modified = 1;
// the node is being deleted; let subclasses drop anything attached to its handle
if (tree_nodes[node].handle)
on_node_handle_changed(tree_nodes[node].handle, nullptr);
tree_nodes[node].type = SEXPT_UNUSED;
total_nodes--;
if (tree_nodes[node].child != -1)
@@ -2640,6 +2645,9 @@ void sexp_tree::NodeDelete()
Assert(theNode >= 0);
free_node2(theNode);
// the event-root item is not a tree_nodes entry, so free_node2 above does
// not cover an annotation attached to the event root itself
on_node_handle_changed(item_handle, nullptr);
DeleteItem(item_handle);
*modified = 1;
return;
@@ -4668,6 +4676,10 @@ HTREEITEM sexp_tree::move_branch(HTREEITEM source, HTREEITEM parent, HTREEITEM a
h = insert(GetItemText(source), image1, image2, parent, after);
}
// the item was recreated with a new handle; let subclasses follow it
// (covers both tree_nodes entries and the event-root item, which is not one)
on_node_handle_changed(source, h);
SetItemData(h, GetItemData(source));
child = GetChildItem(source);
while (child) {
+5
View File
@@ -367,6 +367,11 @@ protected:
virtual void NodeReplacePaste();
virtual void NodeAddPaste();
// Notifies that the tree item for a node changed handle (moved/recreated), or
// was deleted (new_handle == nullptr). The event tree overrides this to keep
// event annotations attached to their node. Default: no-op.
virtual void on_node_handle_changed(HTREEITEM /*old_handle*/, HTREEITEM /*new_handle*/) {}
void update_item(HTREEITEM handle);
int load_branch(int index, int parent);
+17 -12
View File
@@ -1258,17 +1258,7 @@ int CShipEditorDlg::update_data(int redraw)
}
}
if (Player_start_shipnum < 0 || Objects[Ships[Player_start_shipnum].objnum].type != OBJ_START) { // need a new single player start.
ptr = GET_FIRST(&obj_used_list);
while (ptr != END_OF_LIST(&obj_used_list)) {
if (ptr->type == OBJ_START) {
Player_start_shipnum = ptr->instance;
break;
}
ptr = GET_NEXT(ptr);
}
}
ensure_valid_player_start_shipnum(); // may need a new single player start
if (modified)
set_modified();
@@ -1518,7 +1508,9 @@ int CShipEditorDlg::update_ship(int ship)
Objects[Ships[ship].objnum].type = OBJ_SHIP;
break;
}
}
ensure_valid_player_start_shipnum();
Update_ship = 1;
return 0;
@@ -2448,12 +2440,22 @@ void CShipEditorDlg::OnSetAsPlayerShip()
if (objp->flags[Object::Object_Flags::Marked]) // there should only be one selected ship
{
// set as player ship
if (objp->type != OBJ_START)
{
Player_starts++;
set_modified();
}
objp->type = OBJ_START;
objp->flags.set(Object::Object_Flags::Player_ship);
}
else
{
// set as regular ship
if (objp->type == OBJ_START)
{
Player_starts--;
set_modified();
}
objp->type = OBJ_SHIP;
objp->flags.remove(Object::Object_Flags::Player_ship);
}
@@ -2461,6 +2463,9 @@ void CShipEditorDlg::OnSetAsPlayerShip()
objp = GET_NEXT(objp);
}
// fix up Player_start_shipnum if it became invalid
ensure_valid_player_start_shipnum();
// finally set editor dialog
m_player_ship.SetCheck(1);
update_map_window();
+5 -2
View File
@@ -227,7 +227,8 @@ int create_wing() {
}
count = 0;
if (Objects[Ships[Player_start_shipnum].objnum].flags[Object::Object_Flags::Marked])
if ((Player_start_shipnum >= 0) && (Player_start_shipnum < MAX_SHIPS) && (Ships[Player_start_shipnum].objnum >= 0)
&& Objects[Ships[Player_start_shipnum].objnum].flags[Object::Object_Flags::Marked])
count = 1;
ptr = GET_FIRST(&obj_used_list);
@@ -442,7 +443,7 @@ void remove_ship_from_wing(int ship, int min) {
rename_ship(ship, buf);
}
void remove_wing(int wing_num)
void disband_wing(int wing_num)
{
int i, total;
object *ptr;
@@ -450,6 +451,8 @@ void remove_wing(int wing_num)
if (check_wing_dependencies(wing_num))
return;
delete_reinforcement(Wings[wing_num].name);
Ship_editor_dialog.bypass_errors = Wing_editor_dialog.bypass_errors = 1;
Ship_editor_dialog.update_data(0);
total = Wings[wing_num].wave_count;
+1 -1
View File
@@ -27,7 +27,7 @@ int create_wing();
*
* @param[in] wing_num Index of the wing
*/
void remove_wing(int wing_num);
void disband_wing(int wing_num);
/**
* @brief Takes a ship out of a wing, deleting the wing if that was the only ship in it.
+4 -1
View File
@@ -1078,7 +1078,7 @@ void wing_editor::OnDeleteWing()
void wing_editor::OnDisbandWing()
{
modified = 0; // no need to run update checks, since wing will be gone shortly anyway.
remove_wing(cur_wing);
disband_wing(cur_wing);
}
void wing_editor::OnGoals2()
@@ -1475,6 +1475,9 @@ void wing_editor::OnWingFormationAlign()
get_absolute_wing_pos(&objp->pos, leader_objp, cur_wing, i, false);
objp->orient = leader_objp->orient;
// drag any docked partners along (no-op for undocked ships)
object_moved(objp);
}
// roll back temporary formation
+1 -1
View File
@@ -984,7 +984,7 @@ void game_level_close()
particle::ParticleManager::get()->clearSources();
particle::close();
trail_level_close();
ship_close_cockpit_displays(Player_ship);
ship_level_close();
hud_level_close();
hud_escort_clear_all();
model_instance_free_all();
+4 -13
View File
@@ -973,7 +973,6 @@ int Editor::common_object_delete(int obj) {
char msg[255];
const char *name;
int i, z, r, type;
object* objp;
SCP_list<CJumpNode>::iterator jnp;
type = Objects[obj].type;
@@ -1012,17 +1011,7 @@ int Editor::common_object_delete(int obj) {
ai_do_objects_undocked_stuff(&Objects[obj], dock_get_first_docked_object(&Objects[obj]));
}
if (Player_start_shipnum == i) { // need a new single player start.
objp = GET_FIRST(&obj_used_list);
while (objp != END_OF_LIST(&obj_used_list)) {
if (objp->type == OBJ_START) {
Player_start_shipnum = objp->instance;
break;
}
objp = GET_NEXT(objp);
}
}
ensure_valid_player_start_shipnum(); // may need a new single player start
Player_starts--;
@@ -1562,7 +1551,7 @@ int Editor::set_reinforcement(const char* name, int state) {
return 0;
}
void Editor::remove_wing(int wing_num) {
void Editor::disband_wing(int wing_num) {
int i, total;
object* ptr;
@@ -1571,6 +1560,8 @@ void Editor::remove_wing(int wing_num) {
return;
}
delete_reinforcement(Wings[wing_num].name);
total = Wings[wing_num].wave_count;
for (i = 0; i < total; i++) {
ptr = &Objects[wing_objects[wing_num][i]];
+1 -1
View File
@@ -233,7 +233,7 @@ class Editor : public QObject {
*
* @param[in] wing_num Index of the wing
*/
void remove_wing(int wing_num);
void disband_wing(int wing_num);
void delete_marked();
+10 -6
View File
@@ -712,15 +712,19 @@ int EditorViewport::object_check_collision(object* objp, vec3d* p0, vec3d* p1, v
return 0;
}
if ((view.Show_ship_models || view.Show_outlines) && (objp->type == OBJ_SHIP)) {
mc.model_num = Ship_info[Ships[objp->instance].ship_info_index].model_num; // Fill in the model to check
} else if ((view.Show_ship_models || view.Show_outlines) && (objp->type == OBJ_START)) {
mc.model_num = Ship_info[Ships[objp->instance].ship_info_index].model_num; // Fill in the model to check
mc.model_instance_num = -1;
if ((view.Show_ship_models || view.Show_outlines) && (objp->type == OBJ_SHIP || objp->type == OBJ_START)) {
auto& shp = Ships[objp->instance];
mc.model_num = Ship_info[shp.ship_info_index].model_num; // Fill in the model to check
mc.model_instance_num = shp.model_instance_num;
} else if ((view.Show_ship_models || view.Show_outlines) && (objp->type == OBJ_PROP)) {
auto& prp = Props[objp->instance].value();
mc.model_num = Prop_info[prp.prop_info_index].model_num; // Fill in the model to check
mc.model_instance_num = prp.model_instance_num;
} else {
return fvi_ray_sphere(hitpos, p0, p1, &objp->pos, (objp->radius > 0.1f) ? objp->radius : LOLLIPOP_SIZE);
}
mc.model_instance_num = -1;
mc.orient = &objp->orient; // The object's orient
mc.pos = &objp->pos; // The object's position
mc.p0 = p0; // Point 1 of ray to check
+2 -1
View File
@@ -240,7 +240,8 @@ int Editor::create_wing()
}
count = 0;
if (Objects[Ships[Player_start_shipnum].objnum].flags[Object::Object_Flags::Marked]) {
if ((Player_start_shipnum >= 0) && (Player_start_shipnum < MAX_SHIPS) && (Ships[Player_start_shipnum].objnum >= 0)
&& Objects[Ships[Player_start_shipnum].objnum].flags[Object::Object_Flags::Marked]) {
count = 1;
}
@@ -695,9 +695,68 @@ int BriefingEditorDialogModel::getIconId() const
return s.icons[_currentIcon].id;
}
void BriefingEditorDialogModel::setIconId(int id)
bool BriefingEditorDialogModel::setIconId(int id)
{
applyToSelectedIconsCurrentAndForward([&](brief_icon& ic) { modify(ic.id, id); });
auto& briefing = _wipBriefings[_currentTeam];
if (briefing.num_stages <= 0 || _currentStage < 0 || _currentStage >= briefing.num_stages)
return true;
auto& stage = briefing.stages[_currentStage];
if (_currentIcon < 0 || _currentIcon >= stage.num_icons)
return true;
// an id applies to a single icon, so operate on the current icon rather than the
// whole selection; applying one id to several icons would itself create a collision
const int oldId = stage.icons[_currentIcon].id;
if (id == oldId)
return true; // no change
// briefing icon ids must never be negative (the spin box also enforces this)
if (id < 0)
return false;
// an icon id must be unique within its stage
for (int i = 0; i < stage.num_icons; ++i) {
if (i != _currentIcon && stage.icons[i].id == id) {
QMessageBox::warning(nullptr,
tr("Icon ID"),
tr("Icon ID %1 is already used by another icon in this stage. The ID has not been changed.").arg(id));
return false;
}
}
// when propagating forward, the new id must not already be used by a different icon in a
// later stage, or the global rename would merge two distinct icon chains into one
if (!_changeLocally) {
for (int st = _currentStage + 1; st < briefing.num_stages; ++st) {
const auto& s = briefing.stages[st];
for (int i = 0; i < s.num_icons; ++i) {
if (s.icons[i].id == id) {
QMessageBox::warning(nullptr,
tr("Icon ID"),
tr("Icon ID %1 is already used in a later stage. You can only change to that ID "
"locally. The ID has not been changed.").arg(id));
return false;
}
}
}
}
// apply the change to the current icon, and (unless local-only) to the same icon in later stages
stage.icons[_currentIcon].id = id;
if (!_changeLocally) {
for (int st = _currentStage + 1; st < briefing.num_stages; ++st) {
auto& s = briefing.stages[st];
for (int i = 0; i < s.num_icons; ++i) {
if (s.icons[i].id == oldId)
s.icons[i].id = id;
}
}
}
set_modified();
modelChanged();
return true;
}
SCP_string BriefingEditorDialogModel::getIconLabel() const
@@ -70,7 +70,8 @@ class BriefingEditorDialogModel : public AbstractDialogModel {
vec3d getIconPosition() const;
void setIconPosition(const vec3d& pos);
int getIconId() const;
void setIconId(int id);
// returns false if the requested id was rejected (e.g. it collides with another icon)
bool setIconId(int id);
SCP_string getIconLabel() const;
void setIconLabel(const SCP_string& text);
SCP_string getIconCloseupLabel() const;
@@ -609,8 +609,12 @@ void MissionEventsDialogModel::reorderByRootFormulaOrder(const SCP_vector<int>&
// Keep selection reasonable (select the first event after reorder)
setCurrentlySelectedEvent(m_events.empty() ? -1 : 0);
// Rebuild applied annotations against new handles/order if needed
initializeEventAnnotations();
// A root reorder moves top-level items via take/insert, which preserves their
// handles (see sexp_tree::move_root), so the cached annotation handles are still
// valid and still point at the correct nodes. Do NOT re-resolve handles from the
// stored paths here: the paths carry the pre-reorder event index, so re-resolving
// would re-point annotations at the wrong event. applyAnnotations() rebuilds each
// path from its live handle at save time.
set_modified();
}
@@ -1138,6 +1142,20 @@ void MissionEventsDialogModel::setNodeBgColor(IEventTreeOps::Handle h, int r, in
set_modified();
}
// The tree recreated (moved) or deleted the item for a node. Re-point any
// annotation on the old handle to the new one so it follows the node; a null
// new_handle means the node was deleted, which clears the cached handle so
// applyAnnotations() resolves it from the path (and drops it if the node is gone).
void MissionEventsDialogModel::onNodeHandleChanged(IEventTreeOps::Handle old_handle, IEventTreeOps::Handle new_handle)
{
if (!old_handle)
return;
for (auto& ea : m_event_annotations)
if (ea.handle == old_handle)
ea.handle = new_handle;
}
void MissionEventsDialogModel::createMessage()
{
MMessage msg;
@@ -142,6 +142,10 @@ class MissionEventsDialogModel : public AbstractDialogModel {
void setNodeAnnotation(IEventTreeOps::Handle h, const SCP_string& note);
void setNodeBgColor(IEventTreeOps::Handle h, int r, int g, int b, bool has_color);
// Keep annotations attached to their node when the tree recreates (move) or
// deletes (new_handle == nullptr) the item's handle.
void onNodeHandleChanged(IEventTreeOps::Handle old_handle, IEventTreeOps::Handle new_handle);
// Message Management
void createMessage();
void insertMessage();
@@ -1138,15 +1138,8 @@ void ShipEditorDialogModel::setPlayer(const bool m_player)
}
}
}
// Fix up Player_start_shipnum if it became invalid
if (Player_start_shipnum < 0 || Objects[Ships[Player_start_shipnum].objnum].type != OBJ_START) {
for (auto* p = GET_FIRST(&obj_used_list); p != END_OF_LIST(&obj_used_list); p = GET_NEXT(p)) {
if (p->type == OBJ_START) {
Player_start_shipnum = p->instance;
break;
}
}
}
// fix up Player_start_shipnum if it became invalid
ensure_valid_player_start_shipnum();
setModified();
_editor->missionChanged();
modelChanged();
@@ -4,6 +4,7 @@
#include "iff_defs/iff_defs.h"
#include "mission/missionhotkey.h"
#include "mission/missionparse.h"
#include "mission/object.h"
#include "missioneditor/common.h"
#include <QObject>
#include <QMessageBox>
@@ -635,6 +636,9 @@ void WingEditorDialogModel::alignWingFormation()
get_absolute_wing_pos(&objp->pos, leader_objp, _currentWingIndex, i, false);
objp->orient = leader_objp->orient;
// drag any docked partners along (no-op for undocked ships)
object_moved(objp);
}
// roll back temporary formation
@@ -729,7 +733,7 @@ void WingEditorDialogModel::disbandCurrentWing()
if (!wingIsValid())
return;
_editor->remove_wing(_currentWingIndex);
_editor->disband_wing(_currentWingIndex);
reloadFromCurWing();
}
+1 -1
View File
@@ -2688,7 +2688,7 @@ void FredView::on_actionWingForm_triggered(bool /*enabled*/) {
}
void FredView::on_actionWingDisband_triggered(bool /*enabled*/) {
if (fred->query_single_wing_marked()) {
fred->remove_wing(fred->cur_wing);
fred->disband_wing(fred->cur_wing);
} else {
showButtonDialog(DialogType::Error,
"Error",
+10 -1
View File
@@ -89,6 +89,10 @@ BriefingEditorDialog::BriefingEditorDialog(FredView* parent, EditorViewport* vie
ui->iconCloseupLabelLineEdit->setMaxLength(MAX_LABEL_LEN - 1);
ui->voiceFileLineEdit->setMaxLength(MAX_FILENAME_LEN - 1);
// validate the icon id when the edit is committed (focus-out/Enter) rather than on every
// keystroke, so a transient value while typing can't pop a collision warning
ui->iconIdSpinBox->setKeyboardTracking(false);
setupMapWidget();
initializeUi();
updateUi();
@@ -557,7 +561,12 @@ void BriefingEditorDialog::on_disableGridCheckBox_toggled(bool checked)
void BriefingEditorDialog::on_iconIdSpinBox_valueChanged(int arg1)
{
_model->setIconId(arg1);
if (!_model->setIconId(arg1)) {
// the id was rejected; reset the spin box to the model's current value
ui->iconIdSpinBox->blockSignals(true);
ui->iconIdSpinBox->setValue(_model->getIconId());
ui->iconIdSpinBox->blockSignals(false);
}
}
void BriefingEditorDialog::on_iconLabelLineEdit_textChanged(const QString& string)
@@ -245,6 +245,10 @@ void MissionEventsDialog::initEventWidgets() {
_model->setNodeBgColor(h, c.red(), c.green(), c.blue(), c.isValid());
});
connect(ui->eventTree, &sexp_tree::nodeHandleChanged, this, [this](void* old_h, void* new_h) {
_model->onNodeHandleChanged(old_h, new_h);
});
connect(ui->eventTree, &sexp_tree::rootOrderChanged, this, [this] {
SCP_vector<int> order;
order.reserve(ui->eventTree->topLevelItemCount());
+2
View File
@@ -254,6 +254,8 @@ int ErrorChecker::checkObjectList() {
ptr->type = OBJ_SHIP;
Player_starts--;
t--;
ensure_valid_player_start_shipnum();
}
warning("Invalid ship type for a player.%s",
_viewport->Error_checker_apply_auto_corrections
+12
View File
@@ -673,6 +673,11 @@ void sexp_tree::free_node2(int node) {
Assert(tree_nodes[node].type != SEXPT_UNUSED);
Assert(total_nodes > 0);
modified();
// the node is being deleted; let the model drop anything attached to its handle
if (tree_nodes[node].handle)
nodeHandleChanged(tree_nodes[node].handle, nullptr);
tree_nodes[node].type = SEXPT_UNUSED;
total_nodes--;
if (tree_nodes[node].child != -1) {
@@ -2739,6 +2744,9 @@ QTreeWidgetItem* sexp_tree::move_branch(QTreeWidgetItem* source, QTreeWidgetItem
h->setData(0, BgColorRole, source->data(0, BgColorRole));
applyVisuals(h);
// the item was recreated with a new handle; let the model follow it
nodeHandleChanged(source, h);
// Move children safely
while (source->childCount() > 0) {
auto* child = source->child(0);
@@ -7297,6 +7305,10 @@ void sexp_tree::deleteActionHandler()
free_node2(formulaNode);
}
// the event-root item is not a tree_nodes entry, so free_node2 above does
// not cover an annotation attached to the event root itself
nodeHandleChanged(item, nullptr);
// Remove the UI item and reset selection/index
delete item;
setCurrentItemIndex(-1);
+5
View File
@@ -410,6 +410,11 @@ class sexp_tree: public QTreeWidget {
void nodeAnnotationChanged(void* handle, const QString& note);
void nodeBgColorChanged(void* handle, const QColor& color);
// Emitted when a node's tree item changed handle (moved/recreated), or was
// deleted (new_handle == nullptr). Lets the model keep event annotations
// attached to their node across tree mutations.
void nodeHandleChanged(void* old_handle, void* new_handle);
// Generated message map functions
protected:
void keyPressEvent(QKeyEvent* e) override;
+5
View File
@@ -0,0 +1,5 @@
set(FSO_VERSION_MAJOR 26)
set(FSO_VERSION_MINOR 0)
set(FSO_VERSION_BUILD 1)
set(FSO_VERSION_REVISION 0)
set(FSO_VERSION_REVISION_STR RC1)