The numeric branch of the OPF_GAME_SND check in check_sexp_syntax never
actually ran until the type2 variable-overload bug was fixed, so missions
using a sound index of -1 ("no sound" / "default") loaded fine. Now that
the check is live, explicitly allow -1, which the runtime already treats as
an invalid (unset) sound. Also return the more descriptive
SEXP_CHECK_INVALID_GAME_SND / SEXP_CHECK_INVALID_FIREBALL error codes, and
let nested-operator (SEXP_ATOM_LIST) arguments fall through since their type
is already validated upstream.
Also route the Lua SEXP soundentry argument through sexp_get_sound_index so
it accepts a table index, an operator, or a name like the core SEXPs do,
instead of only a name.
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
If invalid UTF-8 is encountered, return the standard Unicode replacement character 0xFFFD. When incrementing or decrementing, make a best effort to follow the code's instructions. It might produce one or more � characters, but that's better than an Error and a hard crash.
Also move character constants to standardized locations.
The OPF_SUBSYSTEM argument-conversion path in LuaSEXP only verified
has_shipp() before calling ship_entry->objp(). A ship registry entry
retains its shipnum after the ship is destroyed or departed, but its
objnum is reset to -1, so objp() either asserts (debug) or dereferences
Objects[-1] (release) when the entry is looked up by Lua scripting
during, for example, an OnMissionAboutToEnd hook.
Tighten the check to has_objp() so absent ships short-circuit to an
empty handle, and correct the early-return type from l_Ship to
l_Subsystem. Apply the matching type correction to OPF_DOCKER_POINT,
which has the same return-type mismatch but no CTD because it never
dereferences objp().
Fixes#7206.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Fix#4089, part 2:
- properly link special-point subsystems with -destroyed submodels, if such a relationship exists and a game_settings.tbl flag is set
- add some helper functions for -destroyed variants of submodels (and now subsystems)
Fix a few items flagged by Coverity and Claude:
- properly check for an unassigned ship node, rather than just Asserting, since this could be affected by mission data
- properly check for null containers for the same reason
- rename `ai_goal_find_empty_slot` to `ai_goal_allocate_slot` and edit comments to reflect its not-quite-side-effect-free role
* use std::move() for local variables that are copied but not reused
Addresses ~140 Coverity "Variable copied when it could be moved"
defects across 74 files. Every std::move() targets a local variable
whose value is consumed by push_back, emplace, assignment, lambda
capture, or a function call and is never read again afterward.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* clang: use std::make_shared
* clang: use std::make_unique
* address feedback
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* draft set-target-player-sexp
* Missed a few entries and duplicates
* Missed another entry
* Yet another missed entry
* Fix subsystem not showing up being targeted
* Fix subsystem targeting in set-player-target SEXP
* remove reduntant return
* change sexp category to better suit feature
Clean up the syntax checker, using clearer variable names for each check, and most importantly unique variable names. Fixes several bugs where syntax checking failed because the same variable was overwritten with incompatible type information.
Also fixes a bug in `check_operator_argument_count` where the count check never ran due to a mixup of `op_index` and `op_const`.
* Implement Photo Mode
* fix gr string errors
* return raw char array
* control conflict groups and better photo param defines
* better param identification in real time
* make photo mode hude dynamically sized
* fix xstrs after all changes
* missed a few
* use modern loop
* Fix multiplayer standalone crash: client stats race + host accept code
Fixes a crash in multiplayer PXO/tracker games hosted on a standalone
server where clients crash at debrief close after scoring a kill:
pilotfile.cpp:295: Ship kills of '<ship class>' not found,
should have been added by pilotfile::update_stats
Two independent root causes were found, both of which must be fixed for
correct behavior:
--- Root cause 1: race condition in multi_sexp_end_mission() (sexp.cpp) ---
The previous code called send_debrief_event() before
multi_handle_end_mission_request() as a "hack" to skip the warp-out
sequence when an end-mission SEXP fires. On standalone servers this is
a race condition: scoring_eval_kill only runs on the master, and the
master hasn't broadcast m_okKills to clients yet at the point
multi_sexp_end_mission() is called. Entering debrief this early causes
scoring_level_close() to run with zeroed m_okKills, so
Pilot.update_stats() creates no kill entries for that session.
When debrief_close() later calls scoring_backout_accept() it cannot
find those entries and hits the UNREACHABLE.
Fix: Remove the send_debrief_event() call entirely. The normal flow
via multi_handle_end_mission_request() has the server broadcast stats
first and then send MISSION_END, which causes the client to enter
debrief with correct kill data already populated. The master enters
debrief through sexp_end_mission() -> send_debrief_event() as before.
The behavioral side-effect of removing this hack: clients that go
through the normal warp-out sequence will no longer skip it on
end-mission. Clients in a state where they cannot warp still receive
immediate debrief via multi_handle_sudden_mission_end().
--- Root cause 2: host accept code never set on PXO standalone (multiui.cpp) ---
In multi_debrief_accept_hit() and multi_debrief_esc_hit(), when the
game is a PXO/tracker game and the local player is the game host
(NETINFO_FLAG_GAME_HOST), the code calls send_store_stats_packet() to
tell clients to record their stats. It then sets
Multi_debrief_stats_accept_code = 1 via the per-client response path
inside the NETINFO_FLAG_AM_MASTER block.
On a standalone server the game host is NOT the master
(NETINFO_FLAG_AM_MASTER is false for the client-side host). The
NETINFO_FLAG_AM_MASTER block is skipped entirely, and
send_store_stats_packet() uses multi_io_send_to_all_reliable() which
skips the local player — so the host never receives its own packet.
Multi_debrief_stats_accept_code stays at -1 (unset).
When debrief_close() checks this value and sees it is not 1, it
assumes stats were tossed or the window was closed without accepting,
and runs scoring_backout_accept() unconditionally. With fix 1 in place
this no longer crashes (entries exist), but silently strips all session
kill credit from the host's pilot record.
Fix: After the send_store_stats_packet() block, call
multi_debrief_stats_accept() unconditionally for the game host so the
accept code is set locally regardless of whether the host is also the
standalone master.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* Preserve end-mission warp-out skip via deferred debrief entry
The previous fix removed send_debrief_event() from
multi_sexp_end_mission() entirely, which was correct for the stats
race condition but had a side effect: clients would no longer skip the
warp-out sequence when an end-mission SEXP fires. The warp-out skip
was an intentional feature — process_endgame_packet() checks whether
the client is already in debrief state and, if so, bypasses
multi_warpout_all_players().
Restore the original behavior by deferring the send_debrief_event()
call to process_endgame_packet() rather than eliminating it.
multi_sexp_end_mission() now sets Multi_sexp_end_mission_pending=true
before calling multi_handle_end_mission_request(). When the client
receives the MISSION_END packet, process_endgame_packet() checks the
flag, calls send_debrief_event() there, then falls through to the
debrief-state check which skips the warp-out exactly as before.
This is safe because send_endgame_packet() calls multi_broadcast_stats()
before sending MISSION_END. Both use reliable delivery, so stats are
guaranteed to be received and applied before MISSION_END is processed,
meaning scoring_level_close() now sees correct kill data when
send_debrief_event() fires.
Alt-J-initiated mission ends are unaffected: the flag is never set,
so the warp-out proceeds normally for those paths.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* Revert multiui.cpp accept code change — tracker master owns stats
The previous change added multi_debrief_stats_accept() calls for the
game host in multi_debrief_accept_hit() and multi_debrief_esc_hit() to
work around Multi_debrief_stats_accept_code staying -1 on a standalone
server. This was based on incorrect analysis.
In PXO tracker games the standalone master is intentionally the sole
authority for stat storage (via multi_fs_std_tracker_store_stats() in
multi_standalone_postgame_close()). The host and clients must not save
stats to their local pilot files independently. Multi_debrief_stats_
accept_code remaining -1 and triggering scoring_backout_accept() on the
host is correct behavior: kills are backed out of the local pilot file
because the tracker holds the authoritative record, not the local file.
The race condition fix (deferred send_debrief_event()) is sufficient to
prevent the crash: Pilot.update_stats() now creates kill entries with
correct m_okKills data, so scoring_backout_accept() can find and remove
them cleanly rather than hitting the UNREACHABLE.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* Move Multi_sexp_end_mission_pending from multiutil to multi and reset in multi_vars_init()
Addresses review feedback: the flag has no relation to multiutil, and
resetting it in multi_vars_init() ensures it is always cleared between
missions regardless of which code path was taken.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
The waypoint class stored its position in m_position separately from the game object's pos field, requiring callers to manually keep both in sync. Now get_pos() and set_pos() delegate to the object position when the game object exists, falling back to the parsed position only if the object does not exist.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1. Add optional `start_index` and `num_items` parameters to the find/count functions in utility.h.
2. Fix an accidental recursion bug.
3. Change appropriate parameters to `size_t` and cast appropriately.
4. Add two forms of `SCP_vector_contains_lcase`.
5. Enhance `sprintf` and add `sprintf_concat` for `SCP_string`.
Testing in FotG exposed a bug that dates back to retail. During mission parsing, arrival and departure anchors were represented by indexes into `Parse_names`. After parsing, they were converted to ship indexes -- but only in FRED. Since some functions are shared between FRED and FS2, this can produce inconsistent results, which can be hard to detect since parse indexes and ship indexes are usually the same.
This refactors arrival and departure anchors to be represented by ship registry indexes in all cases. It also adds conversion functions for the benefit of FRED and QtFRED since those dialogs were designed with ship indexes in mind.
The existing `cannot-perform-scan` flag prevents the cargo from being shown on the HUD, but some modders would like to see the cargo while still preventing a ship from being able to perform a scan. So, this renames the existing flag to `cannot-perform-scan-hide-cargo` and adds a new `cannot-perform-scan-show-cargo` flag. Compatibility code in both mission parsing and flag evaluation ensures that missions using the old flag will still work.
Note that if both `cannot-perform-scan-hide-cargo` and `cannot-perform-scan-show-cargo` are set, the former will take precedence.
Add a useful function for getting a ship_info pointer from a ship registry entry. Also rename `ship_has_dock_bay` to `ship_has_hangar_bay` for clarity, since docking bays and docking points are not the same thing. Finally, add a `model_has_hangar_bay` function.
* First draft of prop object type
* Add prop editor dialog in FRED
* prop sexps
* lua table prop support
* lab support
* lua mission access to props
* prop collision support
* use std::optional so the object->instance id is always valid
* allow props to block suns
* minor cleanup and remove arbitrary limit. Still limited to 500 objects.
* Sort props by category
* cleanup
* implement collision hooks
* parse prop flags and further ensure props are inited and closed correctly
* Add Cyborg's multi suggestions
* fix missing header
* add props to empty slots if available
* add multi signature
* fix rebase issues and match collisions to the new multithread style
* appease Clang the Conquerer
* save/parse mission prop classes by name
* Clean up comments and finish TODOs
* fix string errors
* clang wants this to be static now
* address feedback
* address feedback
* Use model num from prop info instead
* merge prop and ship collision group sexps together
* fix rebase bug
* address feedback
* fix name shadowing
* ship heavy submodel
* clang
---------
Co-authored-by: Asteroth <[email protected]>
These were coded for the `split_str` refactor, but are generally useful elsewhere as well.
1. Use SCP_string in some utility functions where appropriate
2. Avoid copying C-strings in some utility functions where it isn't necessary
3. Add concat functions to `SCP_vector`
4. Change return type `int` to `bool` on some common parselo functions
5. Add `find_white_space` and `find_gray_space` parselo functions
Just as special tokens are replaced in debriefings, replace them in command briefings and briefings as well.
Change `string_replace_tokens_with_keys` to `message_translate_tokens` (since it's the same thing) and add a `replaceContainers` script API function to complement `replaceVariables` and `replaceTokens`.
Also replace every category of token in `sexp_modify_variable` and add comments to places where token replacement might be expected but happens elsewhere.
All other display names are `SCP_string`, so make these `SCP_string` as well to be consistent and to allow arbitrary-length display names.
Also fix a few infelicities with German translation and the script API.
The `play-sound-from-file` sexp did not have any way to automatically recycle audio handles, instead relying on other sexps to explicitly close them. It is more modder-friendly to implicitly free any handles of streams that have finished playing, so this PR adds that capability. It also adds a Warning in the event that an unused audio handle cannot be found.
Fixes#7108.
Infamously, in FSO, a wing whose mothership is destroyed will not, itself, be considered destroyed if it has more waves remaining, because wing cleanup and logging typically only happen when the last ship in the last wave exits the mission. Instead, with no mothership to arrive from, waves will simply no longer arrive. Events that check to see if the wing is destroyed or departed will never fire.
This PR fixes that behavior, ensuring that the wing is cleaned up and logged properly, by using the same logic as the enhanced `cancel-future-waves` SEXP operator. This is tied to a new `$cancel future waves of any wing launched from an exited ship:` AI profiles flag which is enabled by default on 25.0+.
If the `cancel-future-waves` sexp is called when a wave is not present, it would previously not handle wing cleanup, such as setting flags and updating the mission log.
For type safety, use enum class rather than int.
Also make a few miscellaneous tweaks such as adding a new casting macro and making detail level settings a bit cleaner.
1. Add new SEXP operators for setting and fading poofs that will work with one or more arguments, or no arguments (which will affect all poofs at once).
2. Update SEXP help, including clarification that the three `nebula-change-` operators require a nebula to be active to have any effect.
Parse errors that are actually errors should use `error_display(1, ...)`, rather than merely logging them (which doesn't abort parsing) or using `Error` (which omits the line number and file name).
Fixes#7002. Tested with a modified script file that produces the same error.
Broken in #6948 but the original didn't work properly either due to conversion
to int32_t and the resulting size limit.
Big assist from Goober5000 for noticing the bug and helping to sort out the
wonky values.