Skip to content

Conversation

@Goober5000
Copy link
Contributor

Storage of persistent variables had been broken since #1631 -- the player-persistent variable code would inadvertently not run if there were no campaign-persistent variables. Additionally, there were several places where the persistent variable logic was hard to follow, which could have hidden other related bugs. This fixes the check and cleans up and consolidates the relevant functions.

@Goober5000 Goober5000 added this to the Release 25.0 milestone Jan 18, 2026
@Goober5000 Goober5000 added cleanup A modification or rewrite of code to make it more understandable or easier to maintain. fix A fix for bugs, not-a-bugs, and/or regressions. labels Jan 18, 2026
@Goober5000 Goober5000 force-pushed the variable_fix branch 4 times, most recently from 0f0de2a to ef67aec Compare January 19, 2026 01:43
Storage of persistent variables had been broken since scp-fs2open#1631 -- the player-persistent variable code would inadvertently not run if there were no campaign-persistent variables.  Additionally, there were several places where the persistent variable logic was hard to follow, which could have hidden other related bugs.  This fixes the check and cleans up and consolidates the relevant functions.
@Goober5000 Goober5000 marked this pull request as ready for review January 19, 2026 21:37
Copy link
Member

@jg18 jg18 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still reading, but adding a few thoughts.

}
if (store_red_alert) {
for (auto& current_rav : Campaign.red_alert_variables) {
Campaign.persistent_variables.push_back(current_rav);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably check for naming collisions between existing persistent variables and the RA variables we're adding. Ditto for containers.

} else if (any(persistence_type & ContainerType::SAVE_ON_MISSION_PROGRESS) &&
any(container.type & persistence_type) && container.is_eternal()) {
// we might need to save some eternal player-persistent containers
if (!any(container.type & persistence_type)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use none() instead of !any().

Copy link
Member

@jg18 jg18 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still studying, but one concern.

Copy link
Member

@jg18 jg18 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tons and tons of good stuff here, some of which I wanted to call out explicitly, but some concerns and suggestions.

if (add_it) {
Player->variables.push_back(Sexp_variables[i]);
}
if (store_red_alert) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering why we now save red alert variables at the end, instead of at the start?

} else if (any(persistence_type & ContainerType::SAVE_ON_MISSION_PROGRESS) &&
any(container.type & persistence_type) && container.is_eternal()) {
// we might need to save some eternal player-persistent containers
// player-persistent (aka "eternal")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional but strongly encouraged: I think it's worth documenting in code somewhere the 5 types of persistence and what "campaign-persistent"/"player-persistent" originally meant, to avoid potential future confusion, but up to you.

auto ppc_it = std::find_if(Player->containers.begin(),
Player->containers.end(),
[container](const sexp_container &ppc) {
[&container](const sexp_container& ppc) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch about the &container. I wasn't aware of that syntax.

void mission_campaign_end_do()
{
mission_campaign_store_variables(SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS, false);
mission_campaign_store_containers(ContainerType::SAVE_ON_MISSION_PROGRESS, false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused on the reason for this change. Were we failing to save "save on mission progress" persistent data?

break;
else if (Sexp_variables[j].type & SEXP_VARIABLE_IS_PERSISTENT) {
Sexp_variables[j].type = campaign_var.type;
strcpy_s(Sexp_variables[j].text, campaign_var.text);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope, but we should probably create a helper function for assigning variables. Too easy to get it wrong or forget a line.

}

Assert(container.is_eternal());
if (!container.is_eternal()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern as above that we're asserting that a condition is true (i.e., something's wrong if it's false) and taking action if the condition is false.

gameseq_post_event(GS_EVENT_DEBRIEF);
} else {
mission_campaign_store_goals_and_events_and_variables();
mission_campaign_store_goals_and_events_and_variables(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch here.


// eternal variables belong in the player file
Assert(!(temp_var.type & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE));
if (temp_var.type & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same confusion about asserting that X had better be false but then handling the case where X is true, anyway.

This pattern has a very odd smell and is likely to confuse future readers.

If the code needs to handle the case where X is true, even though it shouldn't happen, but the situation doesn't actually indicate a code bug, then an assertion may be the wrong check to use.


// stores containers which will be saved only on mission progression
void mission_campaign_store_containers(ContainerType persistence_type, bool store_red_alert = true);
void mission_campaign_store_containers(ContainerType persistence_type, bool store_red_alert);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call on removing default values. Too much potential for hiding bugs, as we've learned here.

mission_campaign_store_variables(SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS);
mission_campaign_store_containers(ContainerType::SAVE_ON_MISSION_PROGRESS);
mission_campaign_store_variables(SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS, false);
mission_campaign_store_containers(ContainerType::SAVE_ON_MISSION_PROGRESS, false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional but strongly encouraged: given that

  1. this pair of function calls occurs frequently
  2. there's no scenario where we'd want to call one function but not the other

I suggest exporting a function like

void mission_campaign_store_persistent_data(int persistence_type, bool store_red_alert);

and not exporting these two functions anymore.

The new function can internally do the int-to-ContainerType conversion for storing containers.

Copy link
Member

@jg18 jg18 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed a concern about the save_on_close_*() functions getting dropped, even though I think that merging this functionality with the other "store data" function is a good idea overall.


// make sure we are actually playing a single-player campaign
if (!(Game_mode & GM_CAMPAIGN_MODE) || (Campaign.type != CAMPAIGN_TYPE_SINGLE) || (Campaign.current_mission < 0))
return;
Copy link
Member

@jg18 jg18 Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This "save on close"-specific check is now getting lost, which makes me nervous about potentially introducing regressions.

From a quick look, seems like the difference is that "save on close" applies to single-player campaigns only.

Should "save on close" variables even be supported for multi campaigns?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cleanup A modification or rewrite of code to make it more understandable or easier to maintain. fix A fix for bugs, not-a-bugs, and/or regressions.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants