Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 90 additions & 7 deletions code/mission/missioncampaign.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1792,7 +1792,7 @@ void mission_campaign_exit_loop()
* all previous missions marked skipped
* this relies on correct mission ordering in the campaign file
*/
bool mission_campaign_jump_to_mission(const char* filename, bool no_skip)
bool mission_campaign_jump_to_mission(const char* filename, bool no_skip, bool preserve_loadout)
{
int i = 0, mission_num = -1;
constexpr size_t dest_filename_size = 64;
Expand Down Expand Up @@ -1825,12 +1825,14 @@ bool mission_campaign_jump_to_mission(const char* filename, bool no_skip)
// based on player feedback, let's NOT restart the campaign but rather fail gracefully
return false;
} else {
for (SCP_vector<ship_info>::iterator it = Ship_info.begin(); it != Ship_info.end(); it++) {
i = static_cast<int>(std::distance(Ship_info.begin(), it));
Campaign.ships_allowed[i] = 1;
}
for (i = 0; i < weapon_info_size(); i++) {
Campaign.weapons_allowed[i] = 1;
if (!preserve_loadout) {
for (auto it = Ship_info.begin(); it != Ship_info.end(); it++) {
i = static_cast<int>(std::distance(Ship_info.begin(), it));
Campaign.ships_allowed[i] = 1;
}
for (i = 0; i < weapon_info_size(); i++) {
Campaign.weapons_allowed[i] = 1;
}
}

Campaign.next_mission = mission_num;
Expand All @@ -1843,6 +1845,87 @@ bool mission_campaign_jump_to_mission(const char* filename, bool no_skip)
}
}

SCP_vector<SCP_string> mission_campaign_get_valid_next_missions()
{
SCP_vector<SCP_string> valid_missions;

// This can be queried from UI states outside active mission gameplay
// where GM_CAMPAIGN_MODE may not be set even though a campaign is loaded.
if (Campaign.name[0] == '\0' || Campaign.num_missions <= 0) {
return valid_missions;
}

// During normal campaign flow, current_mission is set to -1 after accepting a mission
// and before the next mission actually starts. In that window, prev_mission is the
// mission whose branching formula produced the current next_mission.
// Prefer prev_mission when available since branch selection outside missions is
// generally based on the mission just completed.
int branch_source_mission = Campaign.prev_mission;
if (branch_source_mission < 0) {
branch_source_mission = Campaign.current_mission;
}

if (branch_source_mission < 0 || branch_source_mission >= Campaign.num_missions) {
// Campaigns that haven't started yet can have both current_mission and prev_mission
// unset. In that case, next_mission is the only available entry point.
if (Campaign.next_mission >= 0 && Campaign.next_mission < Campaign.num_missions) {
valid_missions.emplace_back(Campaign.missions[Campaign.next_mission].name);
}
return valid_missions;
}

auto& current = Campaign.missions[branch_source_mission];
if (current.formula < 0) {
return valid_missions;
}

// This helper is intended to enumerate all valid branches from a cond formula.
// If the formula is not cond, return an empty list.
if (get_operator_const(CTEXT(current.formula)) != OP_COND) {
return valid_missions;
}

const int saved_next = Campaign.next_mission;
int clauses = CDR(current.formula);

while (clauses >= 0) {
const int clause = CAR(clauses);
if (clause < 0) {
clauses = CDR(clauses);
continue;
}

flush_sexp_tree(current.formula);

const int condition = CAR(clause);
const int condition_result = eval_sexp(condition);
if (condition_result == SEXP_TRUE) {
Campaign.next_mission = -1;
int actions = CDR(clause);

while (actions >= 0) {
const int exp = CAR(actions);
if (exp >= -1) {
eval_sexp(exp);
}
actions = CDR(actions);
}

if (Campaign.next_mission >= 0 && Campaign.next_mission < Campaign.num_missions && Campaign.next_mission != branch_source_mission) {
const auto& mission_name = Campaign.missions[Campaign.next_mission].name;
if (std::find(valid_missions.begin(), valid_missions.end(), mission_name) == valid_missions.end()) {
valid_missions.emplace_back(mission_name);
}
}
}

clauses = CDR(clauses);
}

Campaign.next_mission = saved_next;
return valid_missions;
}

void mission_campaign_load_failure_popup()
{
if (Campaign_load_failure == 0) {
Expand Down
5 changes: 4 additions & 1 deletion code/mission/missioncampaign.h
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,10 @@ void mission_campaign_skip_to_next();
void mission_campaign_exit_loop();

// jump to specified mission
bool mission_campaign_jump_to_mission(const char* filename, bool no_skip = false);
bool mission_campaign_jump_to_mission(const char* filename, bool no_skip = false, bool preserve_loadout = false);

// get a list of all valid next missions in the campaign
SCP_vector<SCP_string> mission_campaign_get_valid_next_missions();

// stuff for the end of the campaign of the single player game
void mission_campaign_end_init();
Expand Down
18 changes: 15 additions & 3 deletions code/scripting/api/libs/mission.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3207,17 +3207,29 @@ ADE_FUNC(getPrevMissionFilename, l_Campaign, NULL, "Gets previous mission filena
}

// DahBlount - This jumps to a mission, the reason it accepts a boolean value is so that players can return to campaign maps
ADE_FUNC(jumpToMission, l_Campaign, "string filename, [boolean hub]", "Jumps to a mission based on the filename. Optionally, the player can be sent to a hub mission without setting missions to skipped.", "boolean", "Jumps to a mission, returning true if successful, false if unsuccessful (e.g. the mission could not be found in the campaign), or nil if no mission was specified.")
ADE_FUNC(jumpToMission, l_Campaign, "string filename, [boolean hub, boolean preserve]", "Jumps to a mission based on the filename. Optionally, the player can be sent to a hub mission without setting missions to skipped or preserve loadout.", "boolean", "Jumps to a mission, returning true if successful, false if unsuccessful (e.g. the mission could not be found in the campaign), or nil if no mission was specified.")
{
const char* filename = nullptr;
bool hub = false;
if (!ade_get_args(L, "s|b", &filename, &hub))
bool preserve = false;
if (!ade_get_args(L, "s|b", &filename, &hub, &preserve))
return ADE_RETURN_NIL;

bool success = mission_campaign_jump_to_mission(filename, hub);
bool success = mission_campaign_jump_to_mission(filename, hub, preserve);
return ade_set_args(L, "b", success);
}

ADE_FUNC(getValidNextMissions, l_Campaign, nullptr, "Gets all valid next mission filenames for the current campaign.", "table", "A list of mission filenames, or an empty table if none are valid.")
{
auto table = luacpp::LuaTable::create(L);
auto valid_missions = mission_campaign_get_valid_next_missions();
for (size_t i = 0; i < valid_missions.size(); ++i) {
table.addValue(static_cast<int>(i + 1), valid_missions[i]);
}

return ade_set_args(L, "t", &table);
}

ADE_VIRTVAR(CustomData, l_Campaign, nullptr, "Gets the custom data table for this campaign", "table", "The campaign's custom data table")
{
if (ADE_SETTING_VAR) {
Expand Down
Loading