Files
fs2open.github.com/code/missionui/redalert.cpp
T
Goober5000andClaude Fable 5.1 b37cfda56d Fix a model refcount bug on loadout change, and harden model_unload against stale handles (#7851)
* take a model reference when a ship changes class

ship_model_change() only loaded the model when the class had none, so a ship
switched to a class whose model had been loaded only by a select-screen
preview adopted that preview's single reference.  Closing the screen then
freed the model from under the ship, and the first simulation frame asserted
in model_get().  This is what happened on a replayed mission with a restored
loadout: ship select was skipped, the weapon-select overhead loaded the new
class once, the commit switched the player to it, and the screen close
released it.

Load unconditionally, as ship_create() already does, so the ship holds its own
reference.

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

* reject stale model ids in model_unload and release select-screen handles at level close

model_unload only used the slot part of the id it was given, so a handle kept
across missions would free whatever model had since been loaded into that slot.
Validate the id against the slot's occupant and warn instead.

Two holders could present such handles: the weapon-select icon models under a
scripted UI that overrides the state hooks (the native close never runs), and
the ship-select 3D model global, which was never reset between missions.
Release the select-screen handles from game_level_close, and unload the
ship-select model on screen close and on every selection change.

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

* consolidate the select-screen resource release

The weapon side released its icons, anims, and ship class data from two
functions keyed on whether the screen was open, and the level-close path added
a third copy.  Give the weapon side the same shape as the ship side: one
unconditional weapon_select_common_close(), with weapon_select_close() left as
pure UI teardown.  common_select_level_close() now just calls the two common
closes, and common_select_close() calls it in turn.  The ship-select 3D model
is released from ship_select_common_close() so both paths cover it.

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

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-10-13 02:19:48 -04:00

1270 lines
37 KiB
C++

/*
* Copyright (C) Volition, Inc. 1999. All rights reserved.
*
* All source code herein is the property of Volition, Inc. You may not sell
* or otherwise commercially exploit the source or things you created based on the
* source.
*
*/
#define REDALERT_INTERNAL
#include "ai/aigoals.h"
#include "cfile/cfile.h"
#include "freespace.h"
#include "gamesequence/gamesequence.h"
#include "gamesnd/gamesnd.h"
#include "globalincs/alphacolors.h"
#include "globalincs/linklist.h"
#include "graphics/font.h"
#include "hud/hudwingmanstatus.h"
#include "io/key.h"
#include "io/mouse.h"
#include "io/timer.h"
#include "mission/missionbriefcommon.h"
#include "mission/missioncampaign.h"
#include "mission/missiongoals.h"
#include "missionui/missionscreencommon.h"
#include "missionui/missionweaponchoice.h"
#include "missionui/redalert.h"
#include "network/multi.h"
#include "network/multiteamselect.h"
#include "network/multi_endgame.h"
#include "mod_table/mod_table.h"
#include "model/model.h"
#include "object/deadobjectdock.h"
#include "object/objectdock.h"
#include "ship/ship.h"
#include "ship/shiphit.h"
#include "sound/audiostr.h"
#include "sound/fsspeech.h"
#include "weapon/weapon.h"
#include <stdexcept>
/////////////////////////////////////////////////////////////////////////////
// Red Alert Mission-Level
/////////////////////////////////////////////////////////////////////////////
static TIMESTAMP Red_alert_new_mission_timestamp; // timestamp used to give user a little warning for red alerts
static int Red_alert_voice_started;
SCP_vector<red_alert_ship_status> Red_alert_ship_status;
SCP_vector<red_alert_wing_status> Red_alert_wing_status;
SCP_string Red_alert_precursor_mission;
/////////////////////////////////////////////////////////////////////////////
// Red Alert Interface
/////////////////////////////////////////////////////////////////////////////
const char *Red_alert_fname[GR_NUM_RESOLUTIONS] = {
"RedAlert",
"2_RedAlert"
};
const char *Red_alert_mask[GR_NUM_RESOLUTIONS] = {
"RedAlert-m",
"2_RedAlert-m"
};
// font to use for "incoming transmission"
int Ra_flash_font[GR_NUM_RESOLUTIONS] = {
font::FONT2,
font::FONT2
};
int Ra_flash_y[GR_NUM_RESOLUTIONS] = {
140,
200
};
/*
static int Ra_flash_coords[GR_NUM_RESOLUTIONS][2] = {
{
61, 108 // GR_640
},
{
61, 108 // GR_1024
}
};
*/
#define NUM_BUTTONS 2
#define RA_REPLAY_MISSION 0
#define RA_CONTINUE 1
static ui_button_info Buttons[GR_NUM_RESOLUTIONS][NUM_BUTTONS] = {
{ // GR_640
ui_button_info("RAB_00", 2, 445, -1, -1, 0),
ui_button_info("RAB_01", 575, 432, -1, -1, 1),
},
{ // GR_1024
ui_button_info("2_RAB_00", 4, 712, -1, -1, 0),
ui_button_info("2_RAB_01", 920, 691, -1, -1, 1),
}
};
#define RED_ALERT_NUM_TEXT 3
UI_XSTR Red_alert_text[GR_NUM_RESOLUTIONS][RED_ALERT_NUM_TEXT] = {
{ // GR_640
{ "Replay", 1405, 46, 451, UI_XSTR_COLOR_PINK, -1, &Buttons[0][RA_REPLAY_MISSION].button },
{ "Previous Mission", 1452, 46, 462, UI_XSTR_COLOR_PINK, -1, &Buttons[0][RA_REPLAY_MISSION].button },
{ "Continue", 1069, 564, 413, UI_XSTR_COLOR_PINK, -1, &Buttons[0][RA_CONTINUE].button },
},
{ // GR_1024
{ "Replay", 1405, 75, 722, UI_XSTR_COLOR_PINK, -1, &Buttons[1][RA_REPLAY_MISSION].button },
{ "Previous Mission", 1452, 75, 733, UI_XSTR_COLOR_PINK, -1, &Buttons[1][RA_REPLAY_MISSION].button },
{ "Continue", 1069, 902, 661, UI_XSTR_COLOR_PINK, -1, &Buttons[1][RA_CONTINUE].button },
}
};
// indicies for coordinates
#define RA_X_COORD 0
#define RA_Y_COORD 1
#define RA_W_COORD 2
#define RA_H_COORD 3
static UI_TIMESTAMP Text_delay;
int Ra_brief_text_wnd_coords[GR_NUM_RESOLUTIONS][4] = {
{
14, 151, 522, 289
},
{
52, 241, 785, 463
}
};
static UI_WINDOW Ui_window;
// static hud_anim Flash_anim;
static int Background_bitmap;
static int Red_alert_inited = 0;
static int Red_alert_voice;
// open and pre-load the stream buffers for the different voice streams
void red_alert_voice_load()
{
Assert( Briefing != NULL );
if ( strnicmp(Briefing->stages[0].voice, NOX("none"), 4) != 0 && (Briefing->stages[0].voice[0] != '\0') ) {
Red_alert_voice = audiostream_open( Briefing->stages[0].voice, ASF_VOICE );
}
}
// close all the briefing voice streams
void red_alert_voice_unload()
{
if ( Red_alert_voice != -1 ) {
audiostream_close_file(Red_alert_voice, 0);
Red_alert_voice = -1;
}
}
// start playback of the red alert voice
void red_alert_voice_play()
{
if ( !Briefing_voice_enabled ) {
return;
}
if ( Red_alert_voice < 0 ) {
// play simulated speech?
if (fsspeech_play_from(FSSPEECH_FROM_BRIEFING)) {
if (fsspeech_playing()) {
return;
}
fsspeech_play(FSSPEECH_FROM_BRIEFING, Briefing->stages[0].text.c_str());
Red_alert_voice_started = 1;
}
} else {
if (audiostream_is_playing(Red_alert_voice)) {
return;
}
audiostream_play(Red_alert_voice, Master_voice_volume, 0);
Red_alert_voice_started = 1;
}
}
// stop playback of the red alert voice
void red_alert_voice_stop()
{
if ( !Red_alert_voice_started )
return;
if (Red_alert_voice < 0) {
fsspeech_stop();
} else {
audiostream_stop(Red_alert_voice, 1, 0); // stream is automatically rewound
}
}
// pausing and unpausing of red alert voice
void red_alert_voice_pause()
{
if ( !Red_alert_voice_started )
return;
if (Red_alert_voice < 0) {
fsspeech_pause(true);
} else {
audiostream_pause(Red_alert_voice);
}
}
void red_alert_voice_unpause()
{
if ( !Red_alert_voice_started )
return;
if (Red_alert_voice < 0) {
fsspeech_pause(false);
} else {
audiostream_unpause(Red_alert_voice);
}
}
// a button was pressed, deal with it
void red_alert_button_pressed(int n)
{
switch (n) {
case RA_CONTINUE:
// warp the mouse cursor the the middle of the screen for those who control with a mouse
mouse_set_pos( gr_screen.max_w/2, gr_screen.max_h/2 );
if(Game_mode & GM_MULTIPLAYER){
multi_ts_commit_pressed();
} else {
gameseq_post_event(GS_EVENT_ENTER_GAME);
}
break;
case RA_REPLAY_MISSION:
if ( (Game_mode & GM_CAMPAIGN_MODE) && !(Game_mode & GM_MULTIPLAYER) ) {
// TODO: make call to campaign code to set correct mission for loading
// mission_campaign_play_previous_mission(Red_alert_precursor_mission);
if ( !mission_campaign_previous_mission() ) {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
break;
}
gameseq_post_event(GS_EVENT_START_GAME);
} else {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
}
break;
}
}
// blit "incoming transmission"
#define RA_FLASH_CYCLE 0.25f
float Ra_flash_time = 0.0f;
int Ra_flash_up = 0;
void red_alert_blit_title()
{
const char *str = XSTR("Incoming Transmission", 1406);
int w, h;
// get the string size
font::set_font(Ra_flash_font[gr_screen.res]);
gr_get_string_size(&w, &h, str);
// set alpha color
color flash_color;
if(Ra_flash_up){
gr_init_alphacolor(&flash_color, (int)(255.0f * (Ra_flash_time / RA_FLASH_CYCLE)), 0, 0, 255);
} else {
gr_init_alphacolor(&flash_color, (int)(255.0f * (1.0f - (Ra_flash_time / RA_FLASH_CYCLE))), 0, 0, 255);
}
// draw
gr_set_color_fast(&flash_color);
gr_string(Ra_brief_text_wnd_coords[gr_screen.res][0] + ((Ra_brief_text_wnd_coords[gr_screen.res][2] - w) / 2), Ra_flash_y[gr_screen.res] - h - 5, str, GR_RESIZE_MENU);
gr_set_color_fast(&Color_normal);
// increment flash time
Ra_flash_time += flFrametime;
if(Ra_flash_time >= RA_FLASH_CYCLE){
Ra_flash_time = 0.0f;
Ra_flash_up = !Ra_flash_up;
}
// back to the original font
font::set_font(font::FONT1);
}
// Called once when red alert interface is started
void red_alert_init()
{
int i;
ui_button_info *b;
if ( Red_alert_inited ) {
return;
}
// for multiplayer, change the state in my netplayer structure
if (Game_mode & GM_MULTIPLAYER) {
Net_player->state = NETPLAYER_STATE_RED_ALERT;
}
Ui_window.create(0, 0, gr_screen.max_w_unscaled, gr_screen.max_h_unscaled, 0);
Ui_window.set_mask_bmap(Red_alert_mask[gr_screen.res]);
for (i=0; i<NUM_BUTTONS; i++) {
b = &Buttons[gr_screen.res][i];
b->button.create(&Ui_window, "", b->x, b->y, 60, 30, 0, 1);
// set up callback for when a mouse first goes over a button
b->button.set_highlight_action(common_play_highlight_sound);
b->button.set_bmaps(b->filename);
b->button.link_hotspot(b->hotspot);
}
// all text
for(i=0; i<RED_ALERT_NUM_TEXT; i++){
Ui_window.add_XSTR(&Red_alert_text[gr_screen.res][i]);
}
// set up red alert hotkeys
Buttons[gr_screen.res][RA_CONTINUE].button.set_hotkey(KEY_CTRLED | KEY_ENTER);
// hud_anim_init(&Flash_anim, Ra_flash_coords[gr_screen.res][RA_X_COORD], Ra_flash_coords[gr_screen.res][RA_Y_COORD], NOX("AlertFlash"));
// hud_anim_load(&Flash_anim);
Red_alert_voice = -1;
if ( !Briefing ) {
Briefing = &Briefings[0];
}
// load in background image and flashing red alert animation
Background_bitmap = mission_ui_background_load(Briefing->background[gr_screen.res], Red_alert_fname[gr_screen.res]);
// Make sure word wrapping and rendering use the same font
font::set_font(font::FONT1);
if ( Briefing->num_stages > 0 ) {
brief_color_text_init(Briefing->stages[0].text.c_str(), Ra_brief_text_wnd_coords[gr_screen.res][RA_W_COORD], default_redalert_briefing_color, 0);
}
red_alert_voice_load();
Text_delay = ui_timestamp(200);
Red_alert_voice_started = 0;
Red_alert_inited = 1;
}
// Called once when the red alert interface is exited
void red_alert_close()
{
if (Red_alert_inited) {
red_alert_voice_stop();
red_alert_voice_unload();
weapon_select_common_close();
if (Background_bitmap >= 0) {
bm_release(Background_bitmap);
}
Ui_window.destroy();
// bm_unload(&Flash_anim);
common_free_interface_palette(); // restore game palette
game_flush();
}
Red_alert_inited = 0;
fsspeech_stop();
}
// called once per frame when game state is GS_STATE_RED_ALERT
void red_alert_do_frame(float frametime)
{
int i, k;
// ensure that the red alert interface has been initialized
if (!Red_alert_inited) {
Int3();
return;
}
// commit if skipping briefing, but not in multi - Goober5000
if (!(Game_mode & GM_MULTIPLAYER)) {
if (The_mission.flags[Mission::Mission_Flags::No_briefing])
{
red_alert_button_pressed(RA_CONTINUE);
return;
}
}
k = Ui_window.process() & ~KEY_DEBUGGED;
switch (k) {
case KEY_ESC:
// gameseq_post_event(GS_EVENT_ENTER_GAME);
if (Game_mode & GM_MULTIPLAYER) {
gamesnd_play_iface(InterfaceSounds::USER_SELECT);
multi_quit_game(PROMPT_ALL);
} else {
gameseq_post_event(GS_EVENT_MAIN_MENU);
}
break;
} // end switch
for (i=0; i<NUM_BUTTONS; i++){
if (Buttons[gr_screen.res][i].button.pressed()){
red_alert_button_pressed(i);
}
}
GR_MAYBE_CLEAR_RES(Background_bitmap);
if (Background_bitmap >= 0) {
gr_set_bitmap(Background_bitmap);
gr_bitmap(0, 0, GR_RESIZE_MENU);
}
Ui_window.draw();
// hud_anim_render(&Flash_anim, frametime);
font::set_font(font::FONT1);
if ( ui_timestamp_elapsed(Text_delay) ) {
int finished_wipe = 0;
if ( Briefing->num_stages > 0 ) {
finished_wipe = brief_render_text(0, Ra_brief_text_wnd_coords[gr_screen.res][RA_X_COORD], Ra_brief_text_wnd_coords[gr_screen.res][RA_Y_COORD], Ra_brief_text_wnd_coords[gr_screen.res][RA_H_COORD], frametime, 0);
}
if (finished_wipe) {
red_alert_voice_play();
}
}
// blit incoming transmission
red_alert_blit_title();
gr_flip();
}
// set the red alert status for the current mission
void red_alert_invalidate_timestamp()
{
Red_alert_new_mission_timestamp = TIMESTAMP::invalid();
}
// Store a ships weapons into a wingman status structure
void red_alert_store_weapons(red_alert_ship_status *ras, ship_weapon *swp)
{
int i;
weapon_info *wip;
wep_t weapons;
// Make sure there isn't any data from the previous ship.
ras->primary_weapons.clear();
ras->secondary_weapons.clear();
if (swp == NULL) {
return;
}
// edited to accommodate ballistics - Goober5000
for (i = 0; i < swp->num_primary_banks; i++) {
weapons.index = swp->primary_bank_weapons[i];
if (weapons.index < 0) {
continue;
}
wip = &Weapon_info[weapons.index];
if (wip->wi_flags[Weapon::Info_Flags::Ballistic]) {
// to avoid triggering the below condition: this way, minimum ammo will be 2...
// since the red-alert representation of a conventional primary is 0 -> not used,
// 1 -> used, I added the representation 2 and above -> ballistic primary
weapons.count = swp->primary_bank_ammo[i] + 2;
} else {
weapons.count = 1;
}
ras->primary_weapons.push_back( weapons );
}
for (i = 0; i < swp->num_secondary_banks; i++) {
weapons.index = swp->secondary_bank_weapons[i];
if (weapons.index < 0) {
continue;
}
weapons.count = swp->secondary_bank_ammo[i];
ras->secondary_weapons.push_back( weapons );
}
}
// Take the weapons stored in a ship_status struct, and bash them into the ship weapons struct
void red_alert_bash_weapons(const red_alert_ship_status *ras, ship_weapon *swp)
{
int i, list_size = 0;
// restore from ship_exited
if ( (ras->ship_class == RED_ALERT_DESTROYED_SHIP_CLASS) || (ras->ship_class == RED_ALERT_PLAYER_DEL_SHIP_CLASS) ) {
return;
}
if (!ras->primary_weapons.empty()) {
// modified to accommodate ballistics - Goober5000
list_size = static_cast<int>(ras->primary_weapons.size());
CLAMP(list_size, 0, MAX_SHIP_PRIMARY_BANKS);
for (i = 0; i < list_size; i++) {
Assert( ras->primary_weapons[i].index >= 0 );
swp->primary_bank_weapons[i] = ras->primary_weapons[i].index;
swp->primary_bank_ammo[i] = ras->primary_weapons[i].count;
if (Weapon_info[swp->primary_bank_weapons[i]].wi_flags[Weapon::Info_Flags::Ballistic]) {
// adjust to correct ammo count, per red_alert_store_weapons()
swp->primary_bank_ammo[i] -= 2;
}
}
swp->num_primary_banks = list_size;
}
if (!ras->secondary_weapons.empty()) {
// bash secondary weapons
list_size = static_cast<int>(ras->secondary_weapons.size());
CLAMP(list_size, 0, MAX_SHIP_SECONDARY_BANKS);
for (i = 0; i < list_size; i++) {
Assert( ras->secondary_weapons[i].index >= 0 );
swp->secondary_bank_weapons[i] = ras->secondary_weapons[i].index;
swp->secondary_bank_ammo[i] = ras->secondary_weapons[i].count;
}
swp->num_secondary_banks = list_size;
}
}
void red_alert_bash_weapons(const red_alert_ship_status *ras, p_object *pobjp)
{
int i, list_size = 0;
ship_info *sip;
subsys_status *sssp = NULL;
// restore from ship_exited
if ( (ras->ship_class == RED_ALERT_DESTROYED_SHIP_CLASS) || (ras->ship_class == RED_ALERT_PLAYER_DEL_SHIP_CLASS) )
return;
sip = &Ship_info[ras->ship_class];
// parse objects use the "pilot" subsystem
for (i = 0; i < pobjp->subsys_count; i++)
{
if (!stricmp(Subsys_status[pobjp->subsys_index + i].name, "pilot"))
{
sssp = &Subsys_status[pobjp->subsys_index + i];
break;
}
}
if (sssp == NULL)
{
Warning(LOCATION, "Parse object data for ship '%s' doesn't contain the 'Pilot' subsystem!", pobjp->name);
return;
}
if (!ras->primary_weapons.empty())
{
// bash primary weapons
list_size = static_cast<int>(ras->primary_weapons.size());
CLAMP(list_size, 0, MAX_SHIP_PRIMARY_BANKS);
for (i = 0; i < list_size; i++)
{
Assert( ras->primary_weapons[i].index >= 0 );
sssp->primary_banks[i] = ras->primary_weapons[i].index;
if (Weapon_info[sssp->primary_banks[i]].wi_flags[Weapon::Info_Flags::Ballistic])
{
float max_count = sip->primary_bank_ammo_capacity[i] / Weapon_info[sssp->primary_banks[i]].cargo_size;
sssp->primary_ammo[i] = fl2i(std::lround(100.0f * (ras->primary_weapons[i].count - 2) / max_count));
}
else
sssp->primary_ammo[i] = 100;
}
}
if (!ras->secondary_weapons.empty())
{
// bash secondary weapons
list_size = static_cast<int>(ras->secondary_weapons.size());
CLAMP(list_size, 0, MAX_SHIP_SECONDARY_BANKS);
for (i = 0; i < list_size; i++)
{
Assert( ras->secondary_weapons[i].index >= 0 );
sssp->secondary_banks[i] = ras->secondary_weapons[i].index;
float max_count = sip->secondary_bank_ammo_capacity[i] / Weapon_info[sssp->secondary_banks[i]].cargo_size;
sssp->secondary_ammo[i] = fl2i(std::lround(100.0f * ras->secondary_weapons[i].count / max_count));
}
}
}
void red_alert_bash_subsys_status(const red_alert_ship_status *ras, ship *shipp)
{
ship_subsys *ss;
int i, count = 0;
int list_size;
// restore from ship_exited
if ( (ras->ship_class == RED_ALERT_DESTROYED_SHIP_CLASS) || (ras->ship_class == RED_ALERT_PLAYER_DEL_SHIP_CLASS) ) {
return;
}
if (!ras->subsys_current_hits.empty()) {
ss = GET_FIRST(&shipp->subsys_list);
while ( ss != END_OF_LIST( &shipp->subsys_list ) ) {
bool was_destroyed = (ss->current_hits <= 0.0f);
// using at() here for the bounds check, although out-of-bounds should
// probably never happen here
try {
ss->current_hits = ras->subsys_current_hits.at(count);
} catch (const std::out_of_range&) {
break;
}
if (ss->current_hits <= 0) {
// subsystems with no hitpoints at all are never destroyed
if (ss->max_hits > 0.0f)
blow_off_subsystem_submodels(shipp, ss);
} else if (was_destroyed) {
// the mission file may have started this subsystem destroyed
restore_subsystem_submodels(shipp, ss);
}
ss = GET_NEXT( ss );
count++;
}
}
if (!ras->subsys_aggregate_current_hits.empty()) {
list_size = static_cast<int>(ras->subsys_aggregate_current_hits.size());
CLAMP(list_size, 0, SUBSYSTEM_MAX);
for (i = 0; i < list_size; i++) {
shipp->subsys_info[i].aggregate_current_hits = ras->subsys_aggregate_current_hits[i];
}
}
}
extern int insert_subsys_status(p_object *pobjp);
void red_alert_bash_subsys_status(const red_alert_ship_status *ras, p_object *pobjp)
{
int i, j;
ship_info *sip;
model_subsystem *psub = NULL;
subsys_status *sssp = NULL;
// restore from ship_exited
if ( (ras->ship_class == RED_ALERT_DESTROYED_SHIP_CLASS) || (ras->ship_class == RED_ALERT_PLAYER_DEL_SHIP_CLASS) )
return;
sip = &Ship_info[ras->ship_class];
// do this differently than the other bash_subsys_status... since the p_object may not contain
// all the subsystems, iterate on the red_alert_ship_status entries rather than the p_object's
// and create missing subsystems where necessary
for (i = 0; i < (int) ras->subsys_current_hits.size(); i++)
{
psub = &sip->subsystems[i];
// in the p_object, subsystem 0 is the pilot, and afterwards the subsystems go in order
j = i + 1;
// this subsystem is in the p_object
if (j < pobjp->subsys_count)
{
sssp = &Subsys_status[pobjp->subsys_index + j];
}
// must create subsystem (same technique as in parse_copy_damage)
else
{
// jam in the new subsystem at the end of the existing list for this parse object
int new_idx = insert_subsys_status(pobjp);
Assert(new_idx == pobjp->subsys_index + j);
sssp = &Subsys_status[new_idx];
strcpy_s(sssp->name, psub->subobj_name);
}
float max_hits = psub->max_subsys_strength * (pobjp->ship_max_hull_strength / sip->max_hull_strength);
float current_hits = ras->subsys_current_hits[i];
sssp->percent = 100.0f * (max_hits - current_hits) / max_hits;
}
}
void red_alert_store_subsys_status(red_alert_ship_status *ras, ship *shipp)
{
ship_subsys *ss;
int i;
// Make sure there isn't any data from the previous ship.
ras->subsys_current_hits.clear();
ras->subsys_aggregate_current_hits.clear();
if (shipp == NULL) {
return;
}
ss = GET_FIRST(&shipp->subsys_list);
while ( ss != END_OF_LIST( &shipp->subsys_list ) ) {
ras->subsys_current_hits.push_back( ss->current_hits );
ss = GET_NEXT( ss );
}
for (i = 0; i < SUBSYSTEM_MAX; i++)
// Pyro3d - Fixes AP8 Based RA crashes
ras->subsys_aggregate_current_hits.push_back(shipp->subsys_info[i].aggregate_current_hits);
}
/*
* Record the current state of the players wingman & ships with the "red-alert-carry" flag
* Wingmen without the red-alert-carry flag are only stored if they survive
* dead wingmen must still be handled in red_alert_bash_ship_status
*/
void red_alert_store_ship_status()
{
SCP_set<int> needed_wingnums;
// store the mission filename for the red alert precursor mission
Red_alert_precursor_mission = Game_current_mission_filename;
// Pyro3d - Clear stored red alert data
// Probably not the best solution, but it prevents an assertion in change_ship_type()
red_alert_clear();
// store status for all existing ships
for (const auto so : list_range(&Ship_obj_list)) {
auto ship_objp = &Objects[so->objnum];
if (ship_objp->flags[Object::Object_Flags::Should_be_dead])
continue;
Assert(ship_objp->type == OBJ_SHIP);
auto shipp = &Ships[ship_objp->instance];
if ( shipp->flags[Ship::Ship_Flags::Dying] ) {
continue;
}
if ( !(shipp->flags[Ship::Ship_Flags::From_player_wing]) && !(shipp->flags[Ship::Ship_Flags::Red_alert_store_status]) ) {
continue;
}
red_alert_ship_status ras;
ras.name = shipp->ship_name;
ras.hull = Objects[shipp->objnum].hull_strength;
ras.ship_class = shipp->ship_info_index;
red_alert_store_weapons(&ras, &shipp->weapons);
red_alert_store_subsys_status(&ras, shipp);
Red_alert_ship_status.push_back( std::move(ras) );
// niffiwan: trying to track down red alert bug creating HUGE pilot files
Assert( (Red_alert_ship_status.size() <= MAX_SHIPS) );
if (shipp->wingnum >= 0) {
needed_wingnums.insert(shipp->wingnum);
}
}
// store exited ships that did not die
for (const auto &exited : Ships_exited) {
if ( !(exited.flags[Ship::Exit_Flags::From_player_wing]) && !(exited.flags[Ship::Exit_Flags::Red_alert_carry]) ) {
continue;
}
red_alert_ship_status ras;
ras.name = exited.ship_name;
ras.hull = float(exited.hull_strength);
// if a ship has been destroyed or removed manually by the player, then mark it as such ...
if ( exited.flags[Ship::Exit_Flags::Destroyed]) {
ras.ship_class = RED_ALERT_DESTROYED_SHIP_CLASS;
}
else if (exited.flags[Ship::Exit_Flags::Player_deleted]) {
ras.ship_class = RED_ALERT_PLAYER_DEL_SHIP_CLASS;
}
// ... otherwise we want to make sure and carry over the ship class
else {
Assert( exited.ship_class >= 0 );
ras.ship_class = exited.ship_class;
}
red_alert_store_weapons(&ras, NULL);
red_alert_store_subsys_status(&ras, NULL);
Red_alert_ship_status.push_back( std::move(ras) );
// niffiwan: trying to track down red alert bug creating HUGE pilot files
Assert( (Red_alert_ship_status.size() <= MAX_SHIPS) );
if (exited.wingnum >= 0) {
needed_wingnums.insert(exited.wingnum);
}
}
Assert( !Red_alert_ship_status.empty() );
// store status for all wings related to a stored ship
for (int wingnum : needed_wingnums) {
auto wingp = &Wings[wingnum];
red_alert_wing_status rws;
rws.name = wingp->name;
rws.latest_wave = wingp->current_wave;
rws.wave_count = wingp->wave_count;
rws.total_arrived_count = wingp->total_arrived_count;
rws.total_departed = wingp->total_departed;
rws.total_destroyed = wingp->total_destroyed;
rws.total_vanished = wingp->total_vanished;
Red_alert_wing_status.push_back( std::move(rws) );
// see comments from niffiwan above
Assert( (Red_alert_wing_status.size() <= MAX_WINGS) );
}
}
// Delete a ship in a red alert mission (since it must have died/departed in the previous mission)
void red_alert_delete_ship(int shipnum, int ship_state)
{
ship* shipp = &Ships[shipnum];
int cleanup_mode; // See ship.h for cleanup mode defines. SHIP_VANISHED etc.
switch (ship_state) {
case RED_ALERT_DESTROYED_SHIP_CLASS:
cleanup_mode = SHIP_DESTROYED_REDALERT;
break;
case RED_ALERT_PLAYER_DEL_SHIP_CLASS:
cleanup_mode = SHIP_DEPARTED_REDALERT;
break;
default:
UNREACHABLE("Red-alert: Unknown delete state '%i'\n", ship_state);
cleanup_mode = SHIP_VANISHED;
break;
}
Objects[shipp->objnum].flags.set(Object::Object_Flags::Should_be_dead);
dock_undock_all(&Objects[shipp->objnum]);
ship_cleanup(shipnum, cleanup_mode);
}
// just mark the parse object as never going to arrive
void red_alert_delete_ship(p_object *pobjp, int ship_state)
{
if (ship_state == RED_ALERT_DESTROYED_SHIP_CLASS || ship_state == RED_ALERT_PLAYER_DEL_SHIP_CLASS)
{
pobjp->flags.set(Mission::Parse_Object_Flags::Red_alert_deleted);
if (pobjp->wingnum < 0)
pobjp->flags.set(Mission::Parse_Object_Flags::SF_Cannot_arrive);
}
else
Error(LOCATION, "Red Alert: asked to delete ship (%s) with invalid ship state (%d)", pobjp->name, ship_state);
}
/*
* Take the red alert status information, and adjust the red alert ships accordingly
* "red alert ships" are wingmen and any ship with the red-alert-carry flag
* Wingmen without red alert data still need to be handled / removed
*/
void red_alert_bash_ship_status()
{
SCP_unordered_map<int, int> Wing_pobjects_marked_for_deletion;
SCP_unordered_map<SCP_string, const red_alert_ship_status*, SCP_string_lcase_hash, SCP_string_lcase_equal_to> red_alert_ship_map;
SCP_unordered_map<SCP_string, const red_alert_wing_status*, SCP_string_lcase_hash, SCP_string_lcase_equal_to> red_alert_wing_map;
if ( !(Game_mode & GM_CAMPAIGN_MODE) ) {
return;
}
if ( Red_alert_ship_status.empty() ) {
return;
}
// if there is no wing status, that's ok; the code will just behave as if there was one wave in the previous mission's wing
// create maps for data lookup
for (const auto& ras : Red_alert_ship_status) {
red_alert_ship_map[ras.name] = &ras;
}
for (const auto& rws : Red_alert_wing_status) {
red_alert_wing_map[rws.name] = &rws;
}
// first, go through all wings and see if we need to skip any ships
for (int wingnum = 0; wingnum < Num_wings; ++wingnum)
{
auto wingp = &Wings[wingnum];
auto rws_iter = red_alert_wing_map.find(wingp->name);
if (rws_iter != red_alert_wing_map.end())
{
auto rws = rws_iter->second;
// if this wing has been stored, we need to skip based on the previous mission's latest wave, but *this mission's* wave count
if (rws->latest_wave > 1)
wingp->red_alert_skipped_ships += (rws->latest_wave - 1) * wingp->wave_count;
}
}
// go through all ships in the game, and see if there is red alert status data for any
auto so = GET_FIRST(&Ship_obj_list);
for ( ; so != END_OF_LIST(&Ship_obj_list); )
{
object *ship_objp = &Objects[so->objnum];
if (ship_objp->flags[Object::Object_Flags::Should_be_dead]) {
so = GET_NEXT(so);
continue;
}
Assert(ship_objp->type == OBJ_SHIP);
ship *shipp = &Ships[ship_objp->instance];
if ( !(shipp->flags[Ship::Ship_Flags::From_player_wing]) && !(shipp->flags[Ship::Ship_Flags::Red_alert_store_status]) ) {
so = GET_NEXT(so);
continue;
}
// see if this ship belongs to a wing that was stored
if (shipp->wingnum >= 0)
{
auto wingp = &Wings[shipp->wingnum];
auto rws_iter = red_alert_wing_map.find(wingp->name);
if (rws_iter != red_alert_wing_map.end())
{
auto rws = rws_iter->second;
// assign the wave number to the wing
wingp->current_wave = rws->latest_wave;
// make sure that num_waves can accommodate the new value of current_wave.
// this would happen if the mission we are entering has too few waves.
if (wingp->num_waves < wingp->current_wave)
{
mprintf(("Red alert warning: wing %s in the current mission has fewer waves than in the last mission. Bashing the number of waves from %i to %i.\n", wingp->name, wingp->num_waves, wingp->current_wave));
wingp->num_waves = wingp->current_wave;
}
if (rws->latest_wave > 1)
{
// find this ship's position in the wing
int ship_entry_index = ship_registry_get_index(shipp->ship_name);
Assertion(Ship_registry[ship_entry_index].has_p_objp(), "Ship %s must have a parse object!", shipp->ship_name);
int pos_in_wing = Ship_registry[ship_entry_index].p_objp()->pos_in_wing;
// give the ship its name from the latest wave
// (this will make the ship match to the correct red-alert data)
wing_bash_ship_name(shipp, wingp, ((rws->latest_wave - 1) * wingp->wave_count) + 1 + pos_in_wing);
// need to update the ship registry too; keep the old key because the previous name was a real ship name that may still be referenced
ship_registry_rename(ship_entry_index, shipp->ship_name, false);
}
}
}
bool ship_data_restored = false;
int ship_state = RED_ALERT_DESTROYED_SHIP_CLASS;
auto ras_iter = red_alert_ship_map.find(shipp->ship_name);
// red-alert data matches this ship!
if (ras_iter != red_alert_ship_map.end())
{
auto ras = ras_iter->second;
// we only want to restore ships which haven't been destroyed, or were removed by the player
if ( (ras->ship_class != RED_ALERT_DESTROYED_SHIP_CLASS) && (ras->ship_class != RED_ALERT_PLAYER_DEL_SHIP_CLASS) )
{
// if necessary, restore correct ship class
if ( ras->ship_class != shipp->ship_info_index )
{
if (ras->ship_class >= 0 && ras->ship_class < ship_info_size())
change_ship_type(SHIP_INDEX(shipp), ras->ship_class);
else
mprintf(("Invalid ship class specified in red alert data for ship %s. Using mission default class.\n", shipp->ship_name));
}
float max_hull;
if (shipp->special_hitpoints)
max_hull = shipp->ship_max_hull_strength;
else
max_hull = Ship_info[shipp->ship_info_index].max_hull_strength;
// restore hull (but not shields)
if (ras->hull >= 0.0f && ras->hull <= max_hull)
ship_objp->hull_strength = ras->hull;
else
mprintf(("Invalid hull strength in red alert data for ship %s. Using mission default hull.\n", shipp->ship_name));
// restore weapons and subsys
red_alert_bash_weapons(ras, &shipp->weapons);
red_alert_bash_subsys_status(ras, shipp);
ship_data_restored = true;
}
// must be destroyed or deleted
else
{
ship_state = ras->ship_class;
}
}
// remove ship if it was destroyed, or if there's no red-alert data for it
if ( !ship_data_restored ) {
// we need to be a little tricky here because deletion invalidates the ship_obj
ship_obj *next_so = GET_NEXT(so);
red_alert_delete_ship(ship_objp->instance, ship_state);
so = next_so;
} else {
so = GET_NEXT(so);
}
}
// NOTE: in retail, red alert data was not loaded for ships that arrived later in the mission
if (!Red_alert_applies_to_delayed_ships)
return;
// go through all ships yet to arrive, and see if there is red alert status data for any
for (auto &pobj : Parse_objects)
{
auto pobjp = &pobj;
// objects that have already arrived would have been handled in the above loop
if ( pobjp->created_object != NULL )
continue;
// same condition as in ship_obj loop
if ( !(pobjp->flags[Mission::Parse_Object_Flags::SF_From_player_wing]) && !(pobjp->flags[Mission::Parse_Object_Flags::SF_Red_alert_store_status]) ) {
continue;
}
char pobjp_name_to_check[NAME_LENGTH];
strcpy_s(pobjp_name_to_check, pobjp->name);
// see if this parse object belongs to a wing that was stored
if (pobjp->wingnum >= 0)
{
auto wingp = &Wings[pobjp->wingnum];
auto rws_iter = red_alert_wing_map.find(wingp->name);
if (rws_iter != red_alert_wing_map.end())
{
auto rws = rws_iter->second;
// assign the wave number to the wing
// note that since the wing hasn't arrived yet, and the wave is incremented upon arrival, we need to subtract 1
wingp->current_wave = rws->latest_wave - 1;
if (rws->latest_wave > 1)
{
// use the name from the latest wave for the purposes of matching
// (this will make the pobjp match to the correct red-alert data)
wing_bash_ship_name(pobjp_name_to_check, wingp->name, ((rws->latest_wave - 1) * wingp->wave_count) + 1 + pobjp->pos_in_wing);
}
}
}
bool ship_data_restored = false;
int ship_state = RED_ALERT_DESTROYED_SHIP_CLASS;
auto ras_iter = red_alert_ship_map.find(pobjp_name_to_check);
// red-alert data matches this ship!
if (ras_iter != red_alert_ship_map.end())
{
auto ras = ras_iter->second;
// we only want to restore ships which haven't been destroyed, or were removed by the player
if ( (ras->ship_class != RED_ALERT_DESTROYED_SHIP_CLASS) && (ras->ship_class != RED_ALERT_PLAYER_DEL_SHIP_CLASS) )
{
// if necessary, restore correct ship class
if ( ras->ship_class != pobjp->ship_class )
{
if (ras->ship_class >= 0 && ras->ship_class < ship_info_size())
swap_parse_object(pobjp, ras->ship_class);
else
mprintf(("Invalid ship class specified in red alert data for ship %s. Using mission default class.\n", pobjp->name));
}
float max_hull;
if (pobjp->special_hitpoints)
max_hull = pobjp->ship_max_hull_strength;
else
max_hull = Ship_info[pobjp->ship_class].max_hull_strength;
// restore hull (but not shields)
if (ras->hull >= 0.0f && ras->hull <= max_hull)
pobjp->initial_hull = (int) (ras->hull * 100.0f / max_hull);
else
mprintf(("Invalid hull strength in red alert data for ship %s. Using mission default hull.\n", pobjp->name));
// restore weapons and subsys
red_alert_bash_weapons(ras, pobjp);
red_alert_bash_subsys_status(ras, pobjp);
ship_data_restored = true;
}
// must be destroyed or deleted
else
{
ship_state = ras->ship_class;
}
}
// remove ship if it was destroyed, or if there's no red-alert data for it
if ( !ship_data_restored )
{
red_alert_delete_ship(pobjp, ship_state);
if (pobjp->wingnum >= 0)
++Wing_pobjects_marked_for_deletion[pobjp->wingnum];
}
}
// if all parse objects in a wing have been removed, clear the count for that wing so the next wave can arrive, if applicable
for (const auto &ii: Wing_pobjects_marked_for_deletion)
{
wing *wingp = &Wings[ii.first];
if (wingp->num_waves > 0 && wingp->wave_count == ii.second)
{
// skip over this wave
wingp->red_alert_skipped_ships += wingp->wave_count;
wingp->current_wave++;
bool waves_spent = wingp->current_wave >= wingp->num_waves;
if (waves_spent)
wingp->flags.set(Ship::Wing_Flags::Gone);
// look through all ships yet to arrive...
for (auto pobjp : list_range(&Ship_arrival_list))
{
// ...and mark the ones in this wing
if (pobjp->wingnum == ii.first)
{
// no waves left to arrive, so mark ships accordingly
if (waves_spent)
pobjp->flags.set(Mission::Parse_Object_Flags::SF_Cannot_arrive);
// we skipped a complete wave, so clear the flag so the next wave creates all ships
else
pobjp->flags.remove(Mission::Parse_Object_Flags::Red_alert_deleted);
}
}
}
}
}
// return !0 if this is a red alert mission, otherwise return 0
int red_alert_mission()
{
return (The_mission.flags[Mission::Mission_Flags::Red_alert]);
}
// called from sexpression code to start a red alert mission
void red_alert_start_mission()
{
// if we are not in campaign mode, go to debriefing
// if ( !(Game_mode & GM_CAMPAIGN_MODE) ) {
// gameseq_post_event( GS_EVENT_DEBRIEF ); // proceed to debriefing
// return;
// }
// check player health here, but only in single-player
// if we're dead (or about to die), don't start red alert mission.
if ( (Game_mode & GM_MULTIPLAYER) ||
((Player_obj->type == OBJ_SHIP) && !Player_ship->flags[Ship::Ship_Flags::Dying] && (Player_obj->hull_strength > 0)) )
{
if (Player_obj->type == OBJ_SHIP)
{
// make sure we don't die
Player_ship->ship_guardian_threshold = SHIP_GUARDIAN_THRESHOLD_DEFAULT;
}
// do normal red alert stuff
Red_alert_new_mission_timestamp = _timestamp(RED_ALERT_WARN_TIME);
// throw down a sound here to make the warning seem ultra-important
// gamesnd_play_iface(SND_USER_SELECT);
snd_play(gamesnd_get_game_sound(GameSounds::DIRECTIVE_COMPLETE));
}
}
// called from HUD code to see if we're red-alerting
int red_alert_in_progress()
{
// it is specifically a question of whether the timestamp is running
return Red_alert_new_mission_timestamp.isValid();
}
// called from the game loop to check if we should actually do the red-alert
void red_alert_maybe_move_to_next_mission()
{
// if the timestamp is invalid, do nothing.
if ( !Red_alert_new_mission_timestamp.isValid() )
return;
// return if the timestamp hasn't elapsed yet
if ( !timestamp_elapsed(Red_alert_new_mission_timestamp) )
return;
// basic premise here is to stop the current mission, and then set the next mission in the campaign
// which better be a red alert mission
if ( Game_mode & GM_CAMPAIGN_MODE ) {
red_alert_store_ship_status();
mission_goal_fail_incomplete();
if (Game_mode & GM_MULTIPLAYER) {
// this should handle close-out and moving to next mission
gameseq_post_event(GS_EVENT_DEBRIEF);
} else {
mission_campaign_store_goals_and_events_and_variables(true);
scoring_level_close();
mission_campaign_eval_next_mission();
mission_campaign_mission_over();
// CD CHECK
gameseq_post_event(GS_EVENT_START_GAME);
}
} else {
if (Game_mode & GM_MULTIPLAYER) {
gameseq_post_event(GS_EVENT_DEBRIEF);
} else {
gameseq_post_event(GS_EVENT_END_GAME);
}
}
}
/*
* red_alert_clear()
*
* clear all red alert "wingman" data
* Allows data to be cleared from outside REDALERT_INTERNAL code
*/
void red_alert_clear()
{
Red_alert_ship_status.clear();
Red_alert_wing_status.clear();
}