Files
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

2006 lines
59 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.
*
*/
#include <climits> // this is need even when not building debug!!
#include <type_traits>
#include "anim/animplay.h"
#include "cutscene/cutscenes.h"
#include "cutscene/movie.h"
#include "gamehelp/contexthelp.h"
#include "gamesequence/gamesequence.h"
#include "gamesnd/eventmusic.h"
#include "gamesnd/gamesnd.h"
#include "globalincs/linklist.h"
#include "globalincs/vmallocator.h"
#include "graphics/2d.h"
#include "graphics/color.h"
#include "graphics/light.h"
#include "graphics/matrix.h"
#include "graphics/shadows.h"
#include "hud/hudwingmanstatus.h"
#include "io/key.h"
#include "io/mouse.h"
#include "io/timer.h"
#include "lighting/lighting.h"
#include "lighting/lighting_profiles.h"
#include "missionui/chatbox.h"
#include "missionui/missionbrief.h"
#include "missionui/missionscreencommon.h"
#include "missionui/missionshipchoice.h"
#include "missionui/missionweaponchoice.h"
#include "mod_table/mod_table.h"
#include "model/modelrender.h"
#include "network/multi.h"
#include "network/multi_endgame.h"
#include "network/multimsgs.h"
#include "network/multiteamselect.h"
#include "network/multiutil.h"
#include "parse/sexp.h"
#include "popup/popup.h"
#include "render/3d.h"
#include "render/batching.h"
#include "scripting/global_hooks.h"
#include "scripting/scripting.h"
#include "ship/ship.h"
#include "ui/uidefs.h"
#include "weapon/weapon.h"
//////////////////////////////////////////////////////////////////
// Game Globals
//////////////////////////////////////////////////////////////////
int Common_select_inited = 0;
// Dependent on when mouse button goes up
int Drop_icon_mflag, Drop_on_wing_mflag, Brief_mouse_up_flag;
int Mouse_down_last_frame = 0;
// Timers used to flash buttons after timeouts
#define MSC_FLASH_AFTER_TIME 60000 // time before flashing a button
#define MSC_FLASH_INTERVAL 200 // time between flashes
UI_TIMESTAMP Flash_timer; // timestamp used to start flashing
UI_TIMESTAMP Flash_toggle; // timestamp used to toggle flashing
int Flash_bright; // state of button to flash
//////////////////////////////////////////////////////////////////
// Global to modulde
//////////////////////////////////////////////////////////////////
int Current_screen;
int Next_screen;
static int InterfacePaletteBitmap = -1; // PCX file that holds the interface palette
color Icon_colors[NUM_ICON_FRAMES];
shader Icon_shaders[NUM_ICON_FRAMES];
loadout_data Player_loadout; // what the ship and weapon loadout is... used since we want to use the
// same loadout if the mission is played again
wss_unit Wss_slots_teams[MAX_TVT_TEAMS][MAX_WSS_SLOTS];
SCP_map<int, int> Wl_pool_teams[MAX_TVT_TEAMS];
SCP_map<int, int> Ss_pool_teams[MAX_TVT_TEAMS];
int Wss_num_wings_teams[MAX_TVT_TEAMS];
wss_unit *Wss_slots = NULL;
SCP_map<int, int> *Wl_pool = nullptr;
SCP_map<int, int> *Ss_pool = nullptr;
int Wss_num_wings;
//////////////////////////////////////////////////////////////////
// Externs
//////////////////////////////////////////////////////////////////
extern void ss_set_team_pointers(int team);
extern void ss_reset_team_pointers();
extern void wl_set_team_pointers(int team);
extern void wl_reset_team_pointers();
extern void ss_reset_selected_ship();
//////////////////////////////////////////////////////////////////
// UI
//////////////////////////////////////////////////////////////////
UI_WINDOW *Active_ui_window;
brief_common_buttons Common_buttons[3][GR_NUM_RESOLUTIONS][NUM_COMMON_BUTTONS] = {
{ // UGH
{ // GR_640
brief_common_buttons("CB_00", 7, 3, 37, 7, 0),
brief_common_buttons("CB_01", 7, 19, 37, 23, 1),
brief_common_buttons("CB_02", 7, 35, 37, 39, 2),
brief_common_buttons("CB_05", 571, 425, 572, 413, 5),
brief_common_buttons("CB_06", 533, 425, 500, 440, 6),
brief_common_buttons("CB_07", 533, 455, 479, 464, 7),
},
{ // GR_1024
brief_common_buttons("2_CB_00", 12, 5, 59, 12, 0),
brief_common_buttons("2_CB_01", 12, 31, 59, 37, 1),
brief_common_buttons("2_CB_02", 12, 56, 59, 62, 2),
brief_common_buttons("2_CB_05", 914, 681, 937, 671, 5),
brief_common_buttons("2_CB_06", 854, 681, 822, 704, 6),
brief_common_buttons("2_CB_07", 854, 724, 800, 743, 7),
}
},
{ // UGH
{ // GR_640
brief_common_buttons("CB_00", 7, 3, 37, 7, 0),
brief_common_buttons("CB_01", 7, 19, 37, 23, 1),
brief_common_buttons("CB_02", 7, 35, 37, 39, 2),
brief_common_buttons("CB_05", 571, 425, 572, 413, 5),
brief_common_buttons("CB_06", 533, 425, 500, 440, 6),
brief_common_buttons("CB_07", 533, 455, 479, 464, 7),
},
{ // GR_1024
brief_common_buttons("2_CB_00", 12, 5, 59, 12, 0),
brief_common_buttons("2_CB_01", 12, 31, 59, 37, 1),
brief_common_buttons("2_CB_02", 12, 56, 59, 62, 2),
brief_common_buttons("2_CB_05", 914, 681, 937, 671, 5),
brief_common_buttons("2_CB_06", 854, 681, 822, 704, 6),
brief_common_buttons("2_CB_07", 854, 724, 800, 743, 7),
}
},
{ // UGH
{ // GR_640
brief_common_buttons("CB_00", 7, 3, 37, 7, 0),
brief_common_buttons("CB_01", 7, 19, 37, 23, 1),
brief_common_buttons("CB_02", 7, 35, 37, 39, 2),
brief_common_buttons("CB_05", 571, 425, 572, 413, 5),
brief_common_buttons("CB_06", 533, 425, 500, 440, 6),
brief_common_buttons("CB_07", 533, 455, 479, 464, 7),
},
{ // GR_1024
brief_common_buttons("2_CB_00", 12, 5, 59, 12, 0),
brief_common_buttons("2_CB_01", 12, 31, 59, 37, 1),
brief_common_buttons("2_CB_02", 12, 56, 59, 62, 2),
brief_common_buttons("2_CB_05", 914, 681, 937, 671, 5),
brief_common_buttons("2_CB_06", 854, 681, 822, 704, 6),
brief_common_buttons("2_CB_07", 854, 724, 800, 743, 7),
}
}
};
#define COMMON_BRIEFING_BUTTON 0
#define COMMON_SS_BUTTON 1
#define COMMON_WEAPON_BUTTON 2
#define COMMON_COMMIT_BUTTON 3
#define COMMON_HELP_BUTTON 4
#define COMMON_OPTIONS_BUTTON 5
int Background_playing; // Flag to indicate background animation is playing
static anim *Background_anim; // Ids for the anim data that is loaded
// value for which Team_data entry to use
int Common_team;
// Ids for the instance of the anim that is playing
//static anim_instance *Background_anim_instance;
int Wing_slot_empty_bitmap;
int Wing_slot_disabled_bitmap;
// prototypes
int wss_slots_all_empty();
// Display the no ships selected error
void common_show_no_ship_error()
{
popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, XSTR( "At least one ship must be selected before proceeding to weapons loadout", 460));
}
// Check the status of the buttons common to the loadout screens
void common_check_buttons()
{
int i;
UI_BUTTON *b;
for ( i = 0; i < NUM_COMMON_BUTTONS; i++ ) {
b = &Common_buttons[Current_screen-1][gr_screen.res][i].button;
if ( b->pressed() ) {
common_button_do(i);
}
}
/*
// AL 11-23-97: let a joystick button press commit
if ( joy_down_count(0) || joy_down_count(1) ) {
Commit_pressed = 1;
}
*/
}
// -------------------------------------------------------------------
// common_redraw_pressed_buttons()
//
// Redraw any common buttons that are pressed down. This function is needed
// since we sometimes need to draw pressed buttons last to ensure the entire
// button gets drawn (and not overlapped by other buttons)
//
void common_redraw_pressed_buttons()
{
int i;
UI_BUTTON *b;
for ( i = 0; i < NUM_COMMON_BUTTONS; i++ ) {
b = &Common_buttons[Current_screen-1][gr_screen.res][i].button;
if ( b->button_down() ) {
b->draw_forced(2);
}
}
}
void common_buttons_maybe_reload(UI_WINDOW * /*ui_window*/)
{
UI_BUTTON *b;
int i;
for ( i = 0; i < NUM_COMMON_BUTTONS; i++ ) {
b = &Common_buttons[Current_screen-1][gr_screen.res][i].button;
b->set_bmaps(Common_buttons[Current_screen-1][gr_screen.res][i].filename);
}
}
void common_buttons_init(UI_WINDOW *ui_window)
{
UI_BUTTON *b;
int i;
for ( i = 0; i < NUM_COMMON_BUTTONS; i++ ) {
b = &Common_buttons[Current_screen-1][gr_screen.res][i].button;
b->create( ui_window, "", Common_buttons[Current_screen-1][gr_screen.res][i].x, Common_buttons[Current_screen-1][gr_screen.res][i].y, 60, 30, 0, 1);
// set up callback for when a mouse first goes over a button
b->set_highlight_action( common_play_highlight_sound );
b->set_bmaps(Common_buttons[Current_screen-1][gr_screen.res][i].filename);
b->link_hotspot(Common_buttons[Current_screen-1][gr_screen.res][i].hotspot);
}
// add some text
ui_window->add_XSTR("Briefing", 1504, Common_buttons[Current_screen-1][gr_screen.res][COMMON_BRIEFING_BUTTON].xt, Common_buttons[Current_screen-1][gr_screen.res][COMMON_BRIEFING_BUTTON].yt, &Common_buttons[Current_screen-1][gr_screen.res][COMMON_BRIEFING_BUTTON].button, UI_XSTR_COLOR_GREEN);
ui_window->add_XSTR("Ship Selection", 1067, Common_buttons[Current_screen-1][gr_screen.res][COMMON_SS_BUTTON].xt, Common_buttons[Current_screen-1][gr_screen.res][COMMON_SS_BUTTON].yt, &Common_buttons[Current_screen-1][gr_screen.res][COMMON_SS_BUTTON].button, UI_XSTR_COLOR_GREEN);
ui_window->add_XSTR("Weapon Loadout", 1068, Common_buttons[Current_screen-1][gr_screen.res][COMMON_WEAPON_BUTTON].xt, Common_buttons[Current_screen-1][gr_screen.res][COMMON_WEAPON_BUTTON].yt, &Common_buttons[Current_screen-1][gr_screen.res][COMMON_WEAPON_BUTTON].button, UI_XSTR_COLOR_GREEN);
ui_window->add_XSTR("Commit", 1062, Common_buttons[Current_screen-1][gr_screen.res][COMMON_COMMIT_BUTTON].xt, Common_buttons[Current_screen-1][gr_screen.res][COMMON_COMMIT_BUTTON].yt, &Common_buttons[Current_screen-1][gr_screen.res][COMMON_COMMIT_BUTTON].button, UI_XSTR_COLOR_PINK);
ui_window->add_XSTR("Help", 928, Common_buttons[Current_screen-1][gr_screen.res][COMMON_HELP_BUTTON].xt, Common_buttons[Current_screen-1][gr_screen.res][COMMON_HELP_BUTTON].yt, &Common_buttons[Current_screen-1][gr_screen.res][COMMON_HELP_BUTTON].button, UI_XSTR_COLOR_GREEN);
ui_window->add_XSTR("Options", 1036, Common_buttons[Current_screen-1][gr_screen.res][COMMON_OPTIONS_BUTTON].xt, Common_buttons[Current_screen-1][gr_screen.res][COMMON_OPTIONS_BUTTON].yt, &Common_buttons[Current_screen-1][gr_screen.res][COMMON_OPTIONS_BUTTON].button, UI_XSTR_COLOR_GREEN);
common_reset_buttons();
Common_buttons[Current_screen-1][gr_screen.res][COMMON_COMMIT_BUTTON].button.set_hotkey(KEY_CTRLED+KEY_ENTER);
Common_buttons[Current_screen-1][gr_screen.res][COMMON_HELP_BUTTON].button.set_hotkey(KEY_F1);
Common_buttons[Current_screen-1][gr_screen.res][COMMON_OPTIONS_BUTTON].button.set_hotkey(KEY_F2);
// for scramble or training missions, disable the ship/weapon selection regions
if ( brief_only_allow_briefing() ) {
Common_buttons[Current_screen-1][gr_screen.res][COMMON_SS_REGION].button.disable();
Common_buttons[Current_screen-1][gr_screen.res][COMMON_WEAPON_REGION].button.disable();
if (Show_locked_status_scramble_missions) {
ui_window->add_XSTR("Ships/Weapons Are Locked For This Mission", 1882, Common_buttons[Current_screen-1][gr_screen.res][COMMON_WEAPON_BUTTON].xt, Common_buttons[Current_screen-1][gr_screen.res][COMMON_WEAPON_BUTTON].yt + 30, &Common_buttons[Current_screen-1][gr_screen.res][COMMON_WEAPON_BUTTON].button, UI_XSTR_COLOR_GREEN);
}
}
}
// Try to load background bitmaps as appropriate
int mission_ui_background_load(const char *custom_background, const char *single_background, const char *multi_background)
{
int background_bitmap = -1;
if (custom_background && (*custom_background != '\0'))
{
background_bitmap = bm_load(custom_background);
if (background_bitmap < 0)
mprintf(("Failed to load custom background bitmap %s!\n", custom_background));
}
// if special background failed to load, or if no special background was supplied, load the standard bitmap
if (background_bitmap < 0)
{
if (multi_background && (Game_mode & GM_MULTIPLAYER))
background_bitmap = bm_load(multi_background);
else
background_bitmap = bm_load(single_background);
}
// return what we've got
return background_bitmap;
}
void set_active_ui(UI_WINDOW *ui_window)
{
Active_ui_window = ui_window;
}
const char *common_music_get_filename(int score_index)
{
if (Cmdline_freespace_no_music) {
return "";
}
Assertion(score_index >= 0 && score_index < NUM_SCORES, "Invalid score index %d.", score_index);
if (Mission_music[score_index] < 0) {
if (!Spooled_music.empty()) {
Mission_music[score_index] = 0;
nprintf(("Sound",
"No briefing music is selected, so play first briefing track: %s\n",
Spooled_music[Mission_music[score_index]].name));
} else {
return "";
}
}
return Spooled_music[Mission_music[score_index]].filename;
}
void common_music_init(int score_index)
{
const auto file_name = common_music_get_filename(score_index);
if (file_name[0] == '\0') {
return;
}
briefing_load_music(file_name);
// Use this id to trigger the start of music playing on the briefing screen
Briefing_music_begin_timestamp = ui_timestamp(BRIEFING_MUSIC_DELAY);
}
void common_music_do()
{
if ( Cmdline_freespace_no_music ) {
return;
}
// Use this id to trigger the start of music playing on the briefing screen
if ( ui_timestamp_elapsed(Briefing_music_begin_timestamp) ) {
Briefing_music_begin_timestamp = UI_TIMESTAMP::invalid();
briefing_start_music();
}
}
void common_music_close()
{
if ( Cmdline_freespace_no_music ) {
return;
}
if ( Spooled_music.empty() )
return;
briefing_stop_music(true);
}
int common_num_cutscenes_valid(int movie_type)
{
int num_valid_cutscenes = 0;
for (uint i = 0; i < The_mission.cutscenes.size(); i++) {
if (movie_type == The_mission.cutscenes[i].type) {
if (!eval_sexp( The_mission.cutscenes[i].formula )) {
continue;
}
num_valid_cutscenes++;
}
}
return num_valid_cutscenes;
}
void common_maybe_play_cutscene(int movie_type, bool restart_music, int music)
{
bool music_off = false;
for (uint i = 0; i < The_mission.cutscenes.size(); i++) {
if (movie_type == The_mission.cutscenes[i].type) {
if (!eval_sexp( The_mission.cutscenes[i].formula )) {
continue;
}
if ( strlen(The_mission.cutscenes[i].filename) ) {
common_music_close();
music_off = true;
movie::play(The_mission.cutscenes[i].filename); //Play the movie!
}
}
}
if (music_off && restart_music) {
common_music_init(music);
}
}
void common_play_cutscene(const char* filename, bool restart_music, int music)
{
bool music_off = false;
common_music_close();
music_off = true;
movie::play(filename); // Play the movie!
if (music_off && restart_music) {
common_music_init(music);
}
};
// function that sets the current palette to the interface palette. This function
// needs to be followed by common_free_interface_palette() to restore the game palette.
void common_set_interface_palette(const char *filename)
{
static char buf[MAX_FILENAME_LEN + 1] = {0};
if (!filename)
filename = NOX("palette01");
Assert(strlen(filename) <= MAX_FILENAME_LEN);
if ( (InterfacePaletteBitmap != -1) && !stricmp(filename, buf) )
return; // already set to this palette
strcpy_s(buf, filename);
// unload the interface bitmap from memory
if (InterfacePaletteBitmap != -1) {
bm_release(InterfacePaletteBitmap);
InterfacePaletteBitmap = -1;
}
// ugh - we don't need this anymore
/*
InterfacePaletteBitmap = bm_load(filename);
if (InterfacePaletteBitmap < 0) {
Error(LOCATION, "Could not load in \"%s\"!", filename);
}
*/
}
// release the interface palette .pcx file, and restore the game palette
void common_free_interface_palette()
{
// unload the interface bitmap from memory
if (InterfacePaletteBitmap != -1) {
bm_release(InterfacePaletteBitmap);
InterfacePaletteBitmap = -1;
}
}
// Init timers used for flashing buttons
void common_flash_button_init()
{
Flash_timer = ui_timestamp(MSC_FLASH_AFTER_TIME);
Flash_toggle = UI_TIMESTAMP::immediate();
Flash_bright = 0;
}
// determine if we should draw a button as bright
int common_flash_bright()
{
if ( ui_timestamp_elapsed(Flash_timer) ) {
if ( ui_timestamp_elapsed(Flash_toggle) ) {
Flash_toggle = ui_timestamp(MSC_FLASH_INTERVAL);
Flash_bright ^= 1;
}
}
return Flash_bright;
}
// set the necessary pointers
void common_set_team_pointers(int team)
{
Assert( (team >= 0) && (team < MAX_TVT_TEAMS) );
Wss_slots = Wss_slots_teams[team];
Ss_pool = &Ss_pool_teams[team];
Wl_pool = &Wl_pool_teams[team];
ss_set_team_pointers(team);
wl_set_team_pointers(team);
}
// reset the necessary pointers to defaults
void common_reset_team_pointers()
{
ss_reset_team_pointers();
wl_reset_team_pointers();
// these are done last so that we can make use of the Assert()'s in the above
// functions to make sure the screens are exited and this is safe
Wss_slots = NULL;
Ss_pool = nullptr;
Wl_pool = nullptr;
}
// common_select_init() will load in animations and bitmaps that are common to the
// briefing/ship select/weapon select screens. The global Common_select_inited is set
// after this function is called once, and is only cleared when common_select_close()
// is called. This prevents multiple loadings of animations/bitmaps.
//
// This function also sets the palette based on the file palette01.pcx
void common_select_init(bool API_Access)
{
if ( Common_select_inited ) {
nprintf(("Alan","common_select_init() returning without doing anything\n"));
return;
}
nprintf(("Alan","entering common_select_init()\n"));
// No anims are playing
Background_playing = 0;
Background_anim = NULL;
Current_screen = Next_screen = ON_BRIEFING_SELECT;
// load in the icons for the wing slots
load_wing_icons(NOX("iconwing01"));
Current_screen = Next_screen = ON_BRIEFING_SELECT;
Commit_pressed = 0;
Common_select_inited = 1;
// this handles the case where the player played a multiplayer game but now is in single player (in one instance
// of FreeSpace)
if(!(Game_mode & GM_MULTIPLAYER)){
chatbox_close();
}
// get the value of the team
Common_team = 0; // assume the first team -- we'll change this value if we need to
if ( (Game_mode & GM_MULTIPLAYER) && IS_MISSION_MULTI_TEAMS )
Common_team = Net_player->p_info.team;
common_set_team_pointers(Common_team);
ship_select_common_init(API_Access);
weapon_select_common_init(API_Access);
if (!API_Access) {
common_flash_button_init();
}
if ( Game_mode & GM_MULTIPLAYER ) {
multi_ts_common_init();
}
// restore loadout from Player_loadout if this is the same mission as the one previously played
if ( !(Game_mode & GM_MULTIPLAYER) ) {
if ( !stricmp(Player_loadout.filename, Game_current_mission_filename) ) {
wss_maybe_restore_loadout();
ss_synch_interface();
wl_synch_interface();
}
}
if (!API_Access) {
ss_reset_selected_ship();
Drop_icon_mflag = 0;
Drop_on_wing_mflag = 0;
}
// init colors
gr_init_alphacolor(&Icon_colors[ICON_FRAME_NORMAL], 32, 128, 128, 255);
gr_init_alphacolor(&Icon_colors[ICON_FRAME_HOT], 48, 160, 160, 255);
gr_init_alphacolor(&Icon_colors[ICON_FRAME_SELECTED], 64, 192, 192, 255);
gr_init_alphacolor(&Icon_colors[ICON_FRAME_PLAYER], 192, 128, 64, 255);
gr_init_alphacolor(&Icon_colors[ICON_FRAME_DISABLED], 175, 175, 175, 255);
gr_init_alphacolor(&Icon_colors[ICON_FRAME_DISABLED_HIGH], 100, 100, 100, 255);
// init shaders
gr_create_shader(&Icon_shaders[ICON_FRAME_NORMAL], 32, 128, 128, 255);
gr_create_shader(&Icon_shaders[ICON_FRAME_HOT], 48, 160, 160, 255);
gr_create_shader(&Icon_shaders[ICON_FRAME_SELECTED], 64, 192, 192, 255);
gr_create_shader(&Icon_shaders[ICON_FRAME_PLAYER], 192, 128, 64, 255);
gr_create_shader(&Icon_shaders[ICON_FRAME_DISABLED], 175, 175, 175, 255);
gr_create_shader(&Icon_shaders[ICON_FRAME_DISABLED_HIGH], 100, 100, 100, 255);
}
void common_reset_buttons()
{
int i;
UI_BUTTON *b;
for ( i = 0; i < NUM_COMMON_BUTTONS; i++ ) {
b = &Common_buttons[Current_screen-1][gr_screen.res][i].button;
b->reset_status();
}
switch(Current_screen) {
case ON_BRIEFING_SELECT:
Common_buttons[Current_screen-1][gr_screen.res][COMMON_BRIEFING_REGION].button.skip_first_highlight_callback();
break;
case ON_SHIP_SELECT:
Common_buttons[Current_screen-1][gr_screen.res][COMMON_SS_REGION].button.skip_first_highlight_callback();
break;
case ON_WEAPON_SELECT:
Common_buttons[Current_screen-1][gr_screen.res][COMMON_WEAPON_REGION].button.skip_first_highlight_callback();
break;
}
}
// common_select_do() is called once per loop in the interface screens and is used
// for drawing and changing the common animations and blitting common bitmaps.
int common_select_do(float /*frametime*/)
{
int k, new_k;
if ( help_overlay_active(Briefing_overlay_id) || help_overlay_active(Ship_select_overlay_id) || help_overlay_active(Weapon_select_overlay_id) ) {
Common_buttons[0][gr_screen.res][COMMON_HELP_BUTTON].button.reset_status();
Common_buttons[1][gr_screen.res][COMMON_HELP_BUTTON].button.reset_status();
Common_buttons[2][gr_screen.res][COMMON_HELP_BUTTON].button.reset_status();
Active_ui_window->set_ignore_gadgets(1);
} else {
Active_ui_window->set_ignore_gadgets(0);
}
k = chatbox_process();
if ( Game_mode & GM_NORMAL ) {
new_k = Active_ui_window->process(k);
} else {
new_k = Active_ui_window->process(k, 0);
}
if ( (k > 0) || (new_k > 0) || B1_JUST_RELEASED ) {
if ( help_overlay_active(Briefing_overlay_id) || help_overlay_active(Ship_select_overlay_id) || help_overlay_active(Weapon_select_overlay_id) ) {
help_overlay_set_state(Briefing_overlay_id, gr_screen.res, 0);
help_overlay_set_state(Ship_select_overlay_id, gr_screen.res, 0);
help_overlay_set_state(Weapon_select_overlay_id, gr_screen.res, 0);
Active_ui_window->set_ignore_gadgets(0);
k = 0;
new_k = 0;
}
}
// test for mouse buttons, must be done after Active_ui_window->process()
// has been called to work properly
//
Drop_icon_mflag = 0;
Drop_on_wing_mflag = 0;
Brief_mouse_up_flag = 0;
Mouse_down_last_frame = 0;
// if the left mouse button was released...
if ( B1_RELEASED ) {
Drop_icon_mflag = 1;
Drop_on_wing_mflag = 1;
}
// if the left mouse button was pressed...
if ( B1_PRESSED ) {
Mouse_down_last_frame = 1;
}
// basically a "click", only check for the click here to avoid action-on-over on briefing map
if ( B1_JUST_PRESSED ) {
Brief_mouse_up_flag = 1;
}
// reset timers for flashing buttons if key pressed
if ( (k>0) || (new_k>0) ) {
common_flash_button_init();
}
common_music_do();
/*
if ( Background_playing ) {
if ( Background_anim_instance->frame_num == BUTTON_SLIDE_IN_FRAME ) {
gamesnd_play_iface(SND_BTN_SLIDE);
}
if ( Background_anim_instance->frame_num == Background_anim_instance->stop_at ) {
// Free up the big honking background animation, since we won't be playing it again
anim_release_render_instance(Background_anim_instance);
anim_free(Background_anim);
Background_playing = 0;
Current_screen = Next_screen = ON_BRIEFING_SELECT;
}
}
*/
if ( Current_screen != Next_screen ) {
switch( Next_screen ) {
case ON_BRIEFING_SELECT:
gameseq_post_event( GS_EVENT_START_BRIEFING );
break;
case ON_SHIP_SELECT:
// go to the specialized multiplayer team/ship select screen
if(Game_mode & GM_MULTIPLAYER){
gameseq_post_event(GS_EVENT_TEAM_SELECT);
}
// go to the normal ship select screen
else {
gameseq_post_event(GS_EVENT_SHIP_SELECTION);
}
break;
case ON_WEAPON_SELECT:
if ( !wss_slots_all_empty() ) {
gameseq_post_event(GS_EVENT_WEAPON_SELECTION);
} else {
common_show_no_ship_error();
}
break;
} // end switch
}
return new_k;
}
// -------------------------------------------------------------------------------------
// common_render()
//
void common_render(float frametime)
{
if ( !Background_playing ) {
GR_MAYBE_CLEAR_RES(Brief_background_bitmap);
gr_set_bitmap(Brief_background_bitmap);
gr_bitmap(0, 0, GR_RESIZE_MENU);
}
anim_render_all(0, frametime);
anim_render_all(ON_SHIP_SELECT, frametime);
}
// -------------------------------------------------------------------------------------
// common_render_selected_screen_button()
//
// A very ugly piece of special purpose code. This is used to draw the pressed button
// frame for whatever stage of the briefing/ship select/weapons loadout we are on.
//
void common_render_selected_screen_button()
{
Common_buttons[Next_screen-1][gr_screen.res][Next_screen-1].button.draw_forced(2);
}
// -------------------------------------------------------------------------------------
// common_button_do() do the button action for the specified pressed button
//
void common_button_do(int i)
{
if ( i == COMMON_COMMIT_BUTTON ) {
Commit_pressed = 1;
return;
}
if ( Background_playing )
return;
switch ( i ) {
case COMMON_BRIEFING_BUTTON:
if ( Current_screen != ON_BRIEFING_SELECT ) {
gamesnd_play_iface(InterfaceSounds::SCREEN_MODE_PRESSED);
Next_screen = ON_BRIEFING_SELECT;
}
break;
case COMMON_WEAPON_BUTTON:
if ( Current_screen != ON_WEAPON_SELECT ) {
if ( !wss_slots_all_empty() ) {
gamesnd_play_iface(InterfaceSounds::SCREEN_MODE_PRESSED);
Next_screen = ON_WEAPON_SELECT;
} else {
common_show_no_ship_error();
}
}
break;
case COMMON_SS_BUTTON:
if ( Current_screen != ON_SHIP_SELECT ) {
gamesnd_play_iface(InterfaceSounds::SCREEN_MODE_PRESSED);
Next_screen = ON_SHIP_SELECT;
}
break;
case COMMON_OPTIONS_BUTTON:
gamesnd_play_iface(InterfaceSounds::SWITCH_SCREENS);
gameseq_post_event( GS_EVENT_OPTIONS_MENU );
break;
case COMMON_HELP_BUTTON:
gamesnd_play_iface(InterfaceSounds::HELP_PRESSED);
launch_context_help();
break;
} // end switch
}
// common_check_keys() will check for keypresses common to all the interface screens.
void common_check_keys(int k)
{
switch (k) {
case KEY_ESC: {
if ( Current_screen == ON_BRIEFING_SELECT ) {
if ( brief_get_closeup_icon() != NULL ) {
brief_turn_off_closeup_icon();
break;
}
}
// prompt the host of a multiplayer game
if(Game_mode & GM_MULTIPLAYER){
multi_quit_game(PROMPT_ALL);
} else {
// go through the single player quit process
// return to the main menu
/*
int return_to_menu, pf_flags;
pf_flags = PF_USE_AFFIRMATIVE_ICON|PF_USE_NEGATIVE_ICON;
return_to_menu = popup(pf_flags, 2, POPUP_NO, POPUP_YES, XSTR( "Do you want to return to the Main Hall?\n(Your campaign position will be saved)", -1));
if ( return_to_menu == 1 ) {
gameseq_post_event(GS_EVENT_MAIN_MENU);
}
*/
gameseq_post_event(GS_EVENT_MAIN_MENU);
}
break;
}
case KEY_CTRLED + KEY_ENTER:
Commit_pressed = 1;
break;
case KEY_B:
if ( Current_screen != ON_BRIEFING_SELECT && !Background_playing ) {
Next_screen = ON_BRIEFING_SELECT;
}
break;
case KEY_W:
if ( brief_only_allow_briefing() ) {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
break;
}
if ( Current_screen != ON_WEAPON_SELECT && !Background_playing ) {
if ( !wss_slots_all_empty() ) {
Next_screen = ON_WEAPON_SELECT;
} else {
common_show_no_ship_error();
}
}
break;
case KEY_S:
if ( brief_only_allow_briefing() ) {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
break;
}
if ( Current_screen != ON_SHIP_SELECT && !Background_playing ) {
Next_screen = ON_SHIP_SELECT;
}
break;
case KEY_SHIFTED+KEY_TAB:
if ( brief_only_allow_briefing() ) {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
break;
}
if ( !Background_playing ) {
switch ( Current_screen ) {
case ON_BRIEFING_SELECT:
if ( !wss_slots_all_empty() ) {
Next_screen = ON_WEAPON_SELECT;
} else {
common_show_no_ship_error();
}
break;
case ON_SHIP_SELECT:
Next_screen = ON_BRIEFING_SELECT;
break;
case ON_WEAPON_SELECT:
Next_screen = ON_SHIP_SELECT;
break;
default:
Int3();
break;
} // end switch
}
break;
case KEY_TAB:
if ( brief_only_allow_briefing() ) {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
break;
}
if ( !Background_playing ) {
switch ( Current_screen ) {
case ON_BRIEFING_SELECT:
Next_screen = ON_SHIP_SELECT;
break;
case ON_SHIP_SELECT:
if ( !wss_slots_all_empty() ) {
Next_screen = ON_WEAPON_SELECT;
} else {
common_show_no_ship_error();
}
break;
case ON_WEAPON_SELECT:
Next_screen = ON_BRIEFING_SELECT;
break;
default:
Int3();
break;
} // end switch
}
break;
case KEY_P:
if ( Anim_paused )
Anim_paused = 0;
else
Anim_paused = 1;
break;
} // end switch
}
// common_select_close() will release the memory for animations and bitmaps that
// were loaded in common_select_init(). This function will abort if the Common_select_inited
// flag is not set. The last thing common_select_close() does in clear the Common_select_inited
// flag.
//
// weapon_select_close() and ship_select_close() are both called, since common_select_close()
// is the function that is called the interface screens are finally exited.
void common_select_close()
{
if ( !Common_select_inited ) {
nprintf(("Alan","common_select_close() returning without doing anything\n"));
return;
}
nprintf(("Alan","entering common_select_close()\n"));
weapon_select_close();
if(Game_mode & GM_MULTIPLAYER){
multi_ts_close();
}
ship_select_close();
common_select_level_close();
brief_close();
common_free_interface_palette();
// release the bitmpas that were previously extracted from anim files
unload_wing_icons();
// Release any instances that may still exist
anim_release_all_instances();
// free the anim's that were loaded into memory
/*
if ( Background_anim ) {
anim_free(Background_anim);
Background_anim = NULL;
}
*/
common_music_close();
common_reset_team_pointers();
Common_select_inited = 0;
}
// Release the model and bitmap handles held by the select screens, whether or not the screens themselves
// were ever opened. Called from common_select_close() on a state change and from game_level_close(),
// because a scripted UI that overrides the state hooks never reaches the former, and by the next mission
// the model slots those handles name may belong to other models.
void common_select_level_close()
{
weapon_select_common_close();
ship_select_common_close();
}
// ------------------------------------------------------------------------
// load_wing_icons() creates the bitmaps for wing icons
//
void load_wing_icons(const char *filename)
{
int first_frame, num_frames;
first_frame = bm_load_animation(filename, &num_frames);
if ( first_frame == -1 ) {
Error(LOCATION, "Could not load icons from %s\n", filename);
return;
}
Wing_slot_disabled_bitmap = first_frame;
Wing_slot_empty_bitmap = first_frame + 1;
// Wing_slot_player_empty_bitmap = first_frame + 2;
}
// ------------------------------------------------------------------------
// common_scroll_up_pressed()
//
int common_scroll_up_pressed(int *start, int size, int max_show)
{
// check if we even need to scroll at all
if ( size <= max_show ) {
return 0;
}
if ( (size - *start) > max_show ) {
*start += 1;
return 1;
}
return 0;
}
// ------------------------------------------------------------------------
// common_scroll_down_pressed()
//
int common_scroll_down_pressed(int *start, int size, int max_show)
{
// if the whole list fits on screen, the start offset must be 0;
// reset a stale non-zero offset so the up arrow doesn't get stuck
if ( size <= max_show ) {
if ( *start > 0 ) {
*start = 0;
return 1;
}
return 0;
}
if ( *start > 0 ) {
*start -= 1;
return 1;
}
return 0;
}
void common_fire_stage_script_hook(int old_stage, int new_stage)
{
if (scripting::hooks::OnBriefStage->isActive()) {
// call a scripting hook for switching stages
// note that we add 1 because Lua arrays are 1-based
scripting::hooks::OnBriefStage->run(
scripting::hook_param_list(
scripting::hook_param("OldStage", 'i', old_stage + 1),
scripting::hook_param("NewStage", 'i', new_stage + 1)
));
}
}
// NEWSTUFF BEGIN
// save ship selection loadout to the Player_loadout struct
void wss_save_loadout()
{
int i,j;
Assert( (Ss_pool != NULL) && (Wl_pool != NULL) && (Wss_slots != NULL) );
// save the ship pool
Player_loadout.ship_pool = *Ss_pool;
// save the weapons pool
Player_loadout.weapon_pool = *Wl_pool;
// save the ship class / weapons for each slot
for ( i = 0; i < MAX_WSS_SLOTS; i++ ) {
Player_loadout.unit_data[i].ship_class = Wss_slots[i].ship_class;
for ( j = 0; j < MAX_SHIP_WEAPONS; j++ ) {
Player_loadout.unit_data[i].wep[j] = Wss_slots[i].wep[j];
Player_loadout.unit_data[i].wep_count[j] = Wss_slots[i].wep_count[j];
}
}
}
// restore ship/weapons loadout from the Player_loadout struct
void wss_maybe_restore_loadout()
{
int i,j;
wss_unit *slot;
Assert( (Ss_pool != NULL) && (Wl_pool != NULL) && (Wss_slots != NULL) );
if (Disable_internal_loadout_restoration_system) {
return;
}
// only restore if mission hasn't changed
if ( stricmp(Player_loadout.last_modified, The_mission.modified) != 0 ) {
return;
}
// first we generate a pool of ships and weapons used the last time this mission was played. We also generate a pool of what is
// available in this mission.
SCP_vector<int> last_loadout_ships(ship_info_size(), 0);
SCP_vector<int> this_loadout_ships(ship_info_size(), 0);
SCP_vector<int> last_loadout_weapons(weapon_info_size(), 0);
SCP_vector<int> this_loadout_weapons(weapon_info_size(), 0);
// record the ship classes / weapons used last time
for ( i = 0; i < MAX_WSS_SLOTS; i++ ) {
slot = &Player_loadout.unit_data[i];
if ((slot->ship_class >= 0) && (slot->ship_class < ship_info_size())) {
++last_loadout_ships[slot->ship_class];
for ( j = 0; j < MAX_SHIP_WEAPONS; j++ ) {
if (Weapon_info.in_bounds(slot->wep[j])) {
last_loadout_weapons[slot->wep[j]] += slot->wep_count[j];
}
}
}
}
// record the ships classes / weapons used by the player and wingmen. We don't include the amount in the pools yet
for ( i = 0; i < MAX_WSS_SLOTS; i++ ) {
if ((Wss_slots[i].ship_class >= 0) && (Wss_slots[i].ship_class < ship_info_size())) {
++this_loadout_ships[Wss_slots[i].ship_class];
for ( j = 0; j < MAX_SHIP_WEAPONS; j++ ) {
if (Weapon_info.in_bounds(Wss_slots[i].wep[j])) {
this_loadout_weapons[Wss_slots[i].wep[j]] += Wss_slots[i].wep_count[j];
}
}
}
}
// now compare the two, adding in what was left in the pools. If there are less of a ship or weapon class in the mission now
// than there were last time, we can't restore and must abort.
for (const auto &[ship_class, count] : *Ss_pool) {
if (count >= 1) {
this_loadout_ships[ship_class] += count;
}
}
for (i = 0; i < ship_info_size(); i++) {
if ( this_loadout_ships[i] < last_loadout_ships[i]) {
return;
}
}
for (const auto &[weapon_class, count] : *Wl_pool) {
if (count >= 1) {
this_loadout_weapons[weapon_class] += count;
}
}
for (i = 0; i < weapon_info_size(); i++) {
if ( this_loadout_weapons[i] < last_loadout_weapons[i]) {
return;
}
}
// go through the slots and restore the previous runthrough's loadout. Also remove that ship from total of ships in this mission
for ( i = 0; i < MAX_WSS_SLOTS; i++ ) {
slot = &Player_loadout.unit_data[i];
if (Ship_info.in_bounds(slot->ship_class)) {
--this_loadout_ships[slot->ship_class];
Assertion((this_loadout_ships[slot->ship_class] >= 0), "Attempting to restore the previous missions loadout has resulted in an invalid number of ships available");
}
// restore the ship class for each slot
Wss_slots[i].ship_class = slot->ship_class;
for ( j = 0; j < MAX_SHIP_WEAPONS; j++ ) {
if (Ship_info.in_bounds(slot->ship_class) && Weapon_info.in_bounds(slot->wep[j])) {
this_loadout_weapons[slot->wep[j]] -= slot->wep_count[j];
Assertion((this_loadout_weapons[slot->wep[j]] >= 0), "Attempting to restore the previous missions loadout has resulted in an invalid number of weapons available");
}
Wss_slots[i].wep[j]= slot->wep[j];
Wss_slots[i].wep_count[j] = slot->wep_count[j];
}
}
// restore the ship pool. Update counts for classes already in the loadout, then add any class the
// previous runthrough used that isn't in it (two passes so we don't iterate over mid-loop inserts).
for (auto &[ship_class, count] : *Ss_pool) {
count = this_loadout_ships[ship_class];
}
for ( i = 0; i < ship_info_size(); i++ ) {
if (this_loadout_ships[i] > 0 && !Ss_pool->contains(i)) {
(*Ss_pool)[i] = this_loadout_ships[i];
}
}
// restore the weapons pool
for (auto &[weapon_class, count] : *Wl_pool) {
count = this_loadout_weapons[weapon_class];
}
for ( i = 0; i < weapon_info_size(); i++ ) {
if (this_loadout_weapons[i] > 0 && !Wl_pool->contains(i)) {
(*Wl_pool)[i] = this_loadout_weapons[i];
}
}
}
// Do a direct restore of the Player_loadout ship/weapon data to the wings
void wss_direct_restore_loadout()
{
int i, j;
wing *wp;
wss_unit *slot;
// only restore if mission hasn't changed
if ( stricmp(Player_loadout.last_modified, The_mission.modified) != 0 ) {
return;
}
// niffiwan: if Starting_wings[] has missing wings, Player_loadout.unit_data
// skips the missing wings. Use a new variable to track the number of valid
// wings in Starting_wings[], otherwise mission can crash on restart
int valid_wing_index = -1;
for ( i = 0; i < MAX_WING_BLOCKS; i++ ) {
if ( Starting_wings[i] < 0 )
continue;
valid_wing_index++;
wp = &Wings[Starting_wings[i]];
// If this wing is still on the arrival list, then update the parse objects
if ( wp->ship_index[0] == -1 ) {
p_object *p_objp;
j=0;
for ( p_objp = GET_FIRST(&Ship_arrival_list); p_objp != END_OF_LIST(&Ship_arrival_list); p_objp = GET_NEXT(p_objp) ) {
if ( p_objp->wingnum == WING_INDEX(wp) ) {
// niffiwan: don't overrun the array
if (j >= MAX_WING_SLOTS) {
Warning(LOCATION, "Starting Wing '%s' has more than 'MAX_WING_SLOTS' ships\n", Starting_wing_names[i]);
break;
}
slot = &Player_loadout.unit_data[valid_wing_index*MAX_WING_SLOTS+j];
p_objp->ship_class = slot->ship_class;
wl_update_parse_object_weapons(p_objp, slot);
j++;
}
}
} else {
int k;
int cleanup_ship_index[MAX_WING_SLOTS];
ship *shipp;
for ( k = 0; k < MAX_WING_SLOTS; k++ ) {
cleanup_ship_index[k] = -1;
}
// This wing is already created, so directly update the ships
for ( j = 0; j < MAX_WING_SLOTS; j++ ) {
if ( wp->ship_index[j] == -1 ) { // if this is an invalid ship, move on
continue;
}
slot = &Player_loadout.unit_data[valid_wing_index*MAX_WING_SLOTS+j];
shipp = &Ships[wp->ship_index[j]];
if ( shipp->ship_info_index != slot->ship_class ) {
if ( slot->ship_class == -1 ) {
cleanup_ship_index[j] = wp->ship_index[j];
ship_add_exited_ship( shipp, Ship::Exit_Flags::Player_deleted );
obj_delete(shipp->objnum);
hud_set_wingman_status_none( shipp->wing_status_wing_index, shipp->wing_status_wing_pos);
continue;
} else {
change_ship_type(wp->ship_index[j], slot->ship_class);
}
}
wl_bash_ship_weapons(&Ships[wp->ship_index[j]].weapons, slot);
}
for ( k = 0; k < MAX_WING_SLOTS; k++ ) {
if ( cleanup_ship_index[k] != -1 ) {
ship_wing_cleanup( cleanup_ship_index[k], wp );
}
}
}
} // end for
}
int wss_slots_all_empty()
{
int i;
Assert( Wss_slots != NULL );
for ( i = 0; i < MAX_WSS_SLOTS; i++ ) {
if ( Wss_slots[i].ship_class >= 0 )
break;
}
if ( i == MAX_WSS_SLOTS )
return 1;
else
return 0;
}
// determine the mode (WSS_...) based on slot/list index values
int wss_get_mode(int from_slot, int from_list, int to_slot, int to_list, int wl_ship_slot)
{
int mode, to_slot_empty=0;
Assert( Wss_slots != NULL );
if ( wl_ship_slot >= 0 ) {
// weapons loadout
if ( to_slot >= 0 ) {
if ( Wss_slots[wl_ship_slot].wep_count[to_slot] == 0 ) {
to_slot_empty = 1;
}
}
} else {
// ship select
if ( to_slot >= 0 ) {
if ( Wss_slots[to_slot].ship_class == -1 ){
to_slot_empty = 1;
}
}
}
// determine mode
if ( from_slot >= 0 && to_slot >= 0 ) {
mode = WSS_SWAP_SLOT_SLOT;
} else if ( from_slot >= 0 && to_list >= 0 ) {
mode = WSS_DUMP_TO_LIST;
} else if ( (from_list >= 0) && (to_slot >= 0) && (to_slot_empty) ) {
mode = WSS_GRAB_FROM_LIST;
} else if ( (from_list >= 0) && (to_slot >= 0) && (!to_slot_empty) ) {
mode = WSS_SWAP_LIST_SLOT;
} else {
mode = -1; // no changes required
}
return mode;
}
// store all the unit data and pool data
int store_wss_data(ubyte *data, __UNUSED const unsigned int max_size, interface_snd_id sound, int player_index)
{
int j, i, packet_size = 0;
ubyte val;
short player_id;
ushort pool_size;
// this function assumes that the data is going to be used over the network
// so make a non-network version of this function if needed
Assert( Game_mode & GM_MULTIPLAYER );
Assert( (Ss_pool != NULL) && (Wl_pool != NULL) && (Wss_slots != NULL) );
if ( !(Game_mode & GM_MULTIPLAYER) )
return 0;
// write the ship pool (only positive counts; the pool map can also hold exhausted 0-count entries)
pool_size = 0;
for (const auto &[ship_class, count] : *Ss_pool) {
if (count > 0) {
++pool_size;
}
}
ADD_USHORT(pool_size);
Assertion((((sizeof(short)+sizeof(short)) * pool_size) + packet_size) < max_size, "Size of ship pool exceeds max data size!");
for (const auto &[ship_class, count] : *Ss_pool) {
if (count > 0) {
ADD_SHORT(static_cast<short>(ship_class));
ADD_SHORT(static_cast<short>(count));
}
}
// write the weapon pool
pool_size = 0;
for (const auto &[weapon_class, count] : *Wl_pool) {
if (count > 0) {
++pool_size;
}
}
ADD_USHORT(pool_size);
Assertion((((sizeof(short)+sizeof(short)) * pool_size) + packet_size) < max_size, "Size of weapon pool exceeds max data size!");
for (const auto &[weapon_class, count] : *Wl_pool) {
if (count > 0) {
ADD_SHORT(static_cast<short>(weapon_class));
ADD_SHORT(static_cast<short>(count));
}
}
// write the unit data
val = MAX_WSS_SLOTS;
ADD_DATA(val);
val = MAX_SHIP_WEAPONS;
ADD_DATA(val);
Assertion((((sizeof(short) + ((sizeof(short)+sizeof(short)) * MAX_SHIP_WEAPONS)) * MAX_WSS_SLOTS) + packet_size) < max_size, "Size of wss data exceeds max data size!");
for (i = 0; i < MAX_WSS_SLOTS; i++) {
ADD_SHORT(static_cast<short>(Wss_slots[i].ship_class));
for (j = 0; j < MAX_SHIP_WEAPONS; j++) {
ADD_SHORT(static_cast<short>(Wss_slots[i].wep[j]));
Assert(Wss_slots[i].wep_count[j] < SHRT_MAX);
ADD_SHORT(static_cast<short>(Wss_slots[i].wep_count[j]));
}
}
// any sound index
ADD_SHORT(static_cast<short>(sound.value()));
// add a netplayer address to identify who should play the sound
player_id = -1;
if(player_index != -1){
player_id = Net_players[player_index].player_id;
}
ADD_SHORT(player_id);
Assert( packet_size < static_cast<int>(max_size) );
return packet_size;
}
int restore_wss_data(ubyte *data)
{
int i, j, offset = 0;
ubyte num_slots, num_weapons;
short b1, b2;
short player_id, sound;
ushort pool_size;
// this function assumes that the data is going to be used over the network
// so make a non-network version of this function if needed
Assert( Game_mode & GM_MULTIPLAYER );
Assert( (Ss_pool != NULL) && (Wl_pool != NULL) && (Wss_slots != NULL) );
if ( !(Game_mode & GM_MULTIPLAYER) )
return 0;
// restore ship pool
Ss_pool->clear();
GET_USHORT(pool_size);
for (i = 0; i < pool_size; i++) {
GET_SHORT(b1);
GET_SHORT(b2);
if (Ship_info.in_bounds(b1)) {
(*Ss_pool)[b1] = b2;
}
}
// restore weapons pool
Wl_pool->clear();
GET_USHORT(pool_size);
for (i = 0; i < pool_size; i++) {
GET_SHORT(b1);
GET_SHORT(b2);
if (Weapon_info.in_bounds(b1)) {
(*Wl_pool)[b1] = b2;
}
}
// restore unit data
for (i = 0; i < MAX_WSS_SLOTS; i++) {
Wss_slots[i].ship_class = -1;
for (j = 0; j < MAX_SHIP_WEAPONS; j++) {
Wss_slots[i].wep[j] = -1;
Wss_slots[i].wep_count[j] = 0;
}
}
GET_DATA(num_slots);
GET_DATA(num_weapons);
for (i = 0; i < num_slots; i++) {
GET_SHORT(b1);
if (i < MAX_WSS_SLOTS) {
Wss_slots[i].ship_class = b1;
}
for (j = 0; j < num_weapons; j++) {
GET_SHORT(b1);
GET_SHORT(b2);
if ( (i < MAX_WSS_SLOTS) && (j < MAX_SHIP_WEAPONS) ) {
Wss_slots[i].wep[j] = b1;
Wss_slots[i].wep_count[j] = b2;
}
}
}
// read in the sound data
GET_SHORT(sound);
// read in the player address
GET_SHORT(player_id);
// determine if I'm the guy who should be playing the sound
if((Net_player != NULL) && (Net_player->player_id == player_id)){
// play the sound
if (sound >= 0) {
gamesnd_play_iface(static_cast<InterfaceSounds>(sound));
}
}
if(!(Game_mode & GM_MULTIPLAYER)){
ss_synch_interface();
}
return offset;
}
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;
if (sip == nullptr)
{
//Assume it's a weapon
rot_angles.h = -(PI_2);
}
else if(sip->model_icon_angles.p != 0.0f || sip->model_icon_angles.b != 0.0f || sip->model_icon_angles.h != 0.0f)
{
// If non-zero model_icon_angles exists, always use that
rot_angles = sip->model_icon_angles;
}
else if(sip->is_small_ship())
{
rot_angles.p = -(PI_2);
}
else if((sip->max_speed <= 0.0f) && !(sip->flags[Ship::Info_Flags::Cargo]))
{
//Probably an installation or Knossos
rot_angles.h = PI;
}
else
{
//Probably a capship
rot_angles.h = PI_2;
}
vm_angles_2_matrix(&object_orient, &rot_angles);
gr_set_clip(x, y, w, h, resize_mode);
g3_start_frame(1);
if (sip != nullptr)
{
const auto& closeup_pos = sip->icon_closeup_pos.value_or(sip->closeup_pos);
const auto closeup_zoom = sip->icon_closeup_zoom.value_or(sip->closeup_zoom);
const auto zoom = closeup_zoom * zoom_multiplier * 2.5f;
g3_set_view_matrix(&closeup_pos, &vmd_identity_matrix, zoom);
gr_set_proj_matrix(Proj_fov * 0.5f, gr_screen.clip_aspect, Min_draw_distance, Max_draw_distance);
}
else
{
Assertion(wip != nullptr, "Weapon is null, get a coder!");
polymodel *pm = model_get(model_id);
bsp_info *bs = NULL; //tehe
for(int i = 0; i < pm->n_models; i++)
{
if(!pm->submodel[i].flags[Model::Submodel_flags::Is_thruster])
{
bs = &pm->submodel[i];
break;
}
}
if(bs == NULL)
{
bs = &pm->submodel[0];
}
vec3d weap_closeup = wip->icon_closeup_pos.value_or(wip->closeup_pos);
float y_closeup;
float tm_zoom = wip->icon_closeup_zoom.value_or(wip->closeup_zoom) * zoom_multiplier;
if (!wip->icon_closeup_pos.has_value()) {
// Find the center of the submodel when no icon-specific position override is defined
weap_closeup.xyz.x = -(bs->min.xyz.z + (bs->max.xyz.z - bs->min.xyz.z) / 2.0f);
weap_closeup.xyz.y = -(bs->min.xyz.y + (bs->max.xyz.y - bs->min.xyz.y) / 2.0f);
weap_closeup.xyz.z = -(bs->rad / tanf(tm_zoom / 2.0f));
y_closeup = -(weap_closeup.xyz.y / tanf(tm_zoom / 2.0f));
if (y_closeup < weap_closeup.xyz.z) {
weap_closeup.xyz.z = y_closeup;
}
if (bs->min.xyz.x < weap_closeup.xyz.z) {
weap_closeup.xyz.z = bs->min.xyz.x;
}
}
g3_set_view_matrix( &weap_closeup, &vmd_identity_matrix, tm_zoom);
gr_set_proj_matrix(Proj_fov * 0.5f, gr_screen.clip_aspect, 0.05f, 1000.0f);
}
model_render_params render_info;
render_info.set_detail_level_lock(0);
gr_set_view_matrix(&Eye_position, &Eye_matrix);
if(!(flags & MR_NO_LIGHTING))
{
//setup lights
common_setup_room_lights();
}
if (sip != NULL && sip->replacement_textures.size() > 0)
{
render_info.set_replacement_textures(model_id, sip->replacement_textures);
}
Glowpoint_override = true;
model_clear_instance(model_id);
int model_instance = -1;
auto cache_result = model_get_cached_ui_render_instance(model_id, &model_instance);
// Only set up the instance when it was freshly created; the cached instance persists across frames.
if (sip != nullptr && cache_result == TriStateBool::TRUE_) {
model_set_up_techroom_instance(sip, model_instance);
}
render_info.set_flags(flags);
model_render_immediate(&render_info, model_id, model_instance, &object_orient, &vmd_zero_vector);
Glowpoint_override = false;
gr_end_view_matrix();
gr_end_proj_matrix();
g3_end_frame();
gr_reset_clip();
}
void draw_model_rotating(model_render_params *render_info, int ship_class, int model_id, int x1, int y1, int x2, int y2, float *rotation_buffer, const vec3d *closeup_pos, float closeup_zoom, float rev_rate, uint64_t flags, int resize_mode, select_effect_params effect_params)
{
//WMC - Can't draw a non-model
if (model_id < 0)
return;
int model_instance = -1;
auto cache_result = model_get_cached_ui_render_instance(model_id, &model_instance);
// Only set up the instance when it was freshly created; the cached instance persists across frames.
if (!(flags & MR_IS_MISSILE) && SCP_vector_inbounds(Ship_info, ship_class) && cache_result == TriStateBool::TRUE_) {
model_set_up_techroom_instance(&Ship_info[ship_class], model_instance);
}
lighting_profiles::set_non_mission_profile non_mission_lighting_profile;
float time = (timer_get_milliseconds()-anim_timer_start)/1000.0f;
angles rot_angles, view_angles;
matrix model_orient;
auto pm = model_get(model_id);
vec3d closeup_pos_default = { { { 0.0f, 0.0f, -(pm->rad * 1.5f) } } };
if (!closeup_pos || IS_VEC_NULL(closeup_pos))
closeup_pos = &closeup_pos_default;
const bool& shadow_disable_override = flags & MR_IS_MISSILE ? Shadow_disable_overrides.disable_mission_select_weapons : Shadow_disable_overrides.disable_mission_select_ships;
if (effect_params.effect == 2) { // FS2 Effect; Phase 0 Expand scanline, Phase 1 scan the grid and wireframe, Phase 2 scan up and reveal the ship, Phase 3 tilt the camera, Phase 4 start rotating the ship
// rotate the ship as much as required for this frame
if (time >= 3.6f) // Phase 4
*rotation_buffer += PI2 * flFrametime / rev_rate;
else
*rotation_buffer = PI; // No rotation before Phase 4
while (*rotation_buffer > PI2){
*rotation_buffer -= PI2;
}
view_angles.p = -PI_2;
if (time >= 3.0f) { // Phase 3
if (time >= 3.6f) { // done tilting
view_angles.p = -0.6f;
} else {
view_angles.p = (PI_2-0.6f)*(time-3.0f)*1.66667f - PI_2; // Phase 3 Tilt animation
}
}
view_angles.b = 0.0f;
view_angles.h = 0.0f;
vm_angles_2_matrix(&model_orient, &view_angles);
rot_angles.p = 0.0f;
rot_angles.b = 0.0f;
rot_angles.h = *rotation_buffer;
vm_rotate_matrix_by_angles(&model_orient, &rot_angles);
gr_set_clip(x1, y1, x2, y2, resize_mode);
vec3d wire_normal,ship_normal,plane_point;
// Clip the wireframe below the scanline
wire_normal.xyz.x = 0.0f;
wire_normal.xyz.y = 1.0f;
wire_normal.xyz.z = 0.0f;
// Clip the ship above the scanline
ship_normal.xyz.x = 0.0f;
ship_normal.xyz.y = -1.0f;
ship_normal.xyz.z = 0.0f;
//Make the clipping plane
float clip = -pm->rad*0.7f;
if (time < 1.5f && time >= 0.5f) // Phase 1 Move down
clip = pm->rad*(time-1.0f)*1.4f;
if (time >= 1.5f)
clip = pm->rad*(time-2.0f)*(-1.4f); // Phase 2 Move up
vm_vec_scale_sub(&plane_point,&vmd_zero_vector,&wire_normal,clip);
g3_start_frame(1);
g3_set_view_matrix(closeup_pos, &vmd_identity_matrix, closeup_zoom);
gr_set_proj_matrix(Proj_fov, gr_screen.clip_aspect, Min_draw_distance, Max_draw_distance);
gr_set_view_matrix(&Eye_position, &Eye_matrix);
vec3d start, stop;
float size = pm->rad*0.7f;
float start_scale = MIN(time,0.5f)*2.5f;
float offset = size*0.5f*MIN(MAX(time-3.0f,0.0f),0.6f)*1.66667f;
g3_start_instance_angles(&vmd_zero_vector,&view_angles);
if (time < 0.5f) { // Do the expanding scanline in phase 0
gr_set_color(effect_params.fs2_scanline_color.red, effect_params.fs2_scanline_color.green, effect_params.fs2_scanline_color.blue);
start.xyz.x = size*start_scale;
start.xyz.y = 0.0f;
start.xyz.z = -clip;
stop.xyz.x = -size*start_scale;
stop.xyz.y = 0.0f;
stop.xyz.z = -clip;
//g3_draw_htl_line(&start,&stop);
g3_render_line_3d(true, &start, &stop);
}
g3_done_instance(true);
gr_zbuffer_set(GR_ZBUFF_NONE); // Turn off Depthbuffer so we don't get gridlines over the ship or a disappearing scanline
Glowpoint_use_depth_buffer = false; // Since we don't have one
if (time >= 0.5f) { // Phase 1 onward draw the grid
start.xyz.y = -offset;
start.xyz.z = size+offset*0.5f;
stop.xyz.y = -offset;
stop.xyz.z = -size+offset*0.5f;
gr_set_color(effect_params.fs2_grid_color.red, effect_params.fs2_grid_color.green, effect_params.fs2_grid_color.blue);
g3_start_instance_angles(&vmd_zero_vector,&view_angles);
if (time < 1.5f) {
stop.xyz.z = -clip;
}
int num_lines = std::max(3, effect_params.fs2_grid_density);
float x_step = (size * 2.0f) / (num_lines - 1);
float x_start = -size;
for (int i = 0; i < num_lines; ++i) {
start.xyz.x = stop.xyz.x = x_start + i * x_step;
g3_render_line_3d(false, &start, &stop);
}
start.xyz.x = size;
stop.xyz.x = -size;
float z_step = (size * 2.0f) / (num_lines - 1);
float z_start = size + offset * 0.5f;
for (int i = 0; i < num_lines; ++i) {
float z = z_start - i * z_step;
if ((time < 1.5f) && (z <= -clip))
break;
start.xyz.z = stop.xyz.z = z;
g3_render_line_3d(false, &start, &stop);
}
g3_done_instance(true);
//setup lights
common_setup_room_lights();
// render the ships
model_clear_instance(model_id);
render_info->set_detail_level_lock(0);
gr_zbuffer_set(true);
if(shadow_maybe_start_frame(shadow_disable_override))
{
gr_end_view_matrix();
gr_end_proj_matrix();
gr_reset_clip();
model_render_params shadow_render_info;
shadow_render_info.set_detail_level_lock(0);
shadow_render_info.set_flags(flags | MR_NO_TEXTURING | MR_NO_LIGHTING);
if ( flags & MR_IS_MISSILE ) {
shadows_start_render(&Eye_matrix, &Eye_position, Proj_fov, Proj_fov, gr_screen.clip_aspect, SCP_vector {{-closeup_pos->xyz.z + pm->rad, -closeup_pos->xyz.z + pm->rad + 20.0f, -closeup_pos->xyz.z + pm->rad + 200.0f, -closeup_pos->xyz.z + pm->rad + 1000.0f}});
} else {
shadows_start_render(&Eye_matrix, &Eye_position, Proj_fov, Proj_fov, gr_screen.clip_aspect, SCP_vector {{-closeup_pos->xyz.z + pm->rad, -closeup_pos->xyz.z + pm->rad + 200.0f, -closeup_pos->xyz.z + pm->rad + 2000.0f, -closeup_pos->xyz.z + pm->rad + 10000.0f}});
}
model_render_immediate(&shadow_render_info, model_id, model_instance, &model_orient, &vmd_zero_vector);
shadows_end_render();
gr_set_clip(x1, y1, x2, y2, resize_mode);
gr_set_proj_matrix(Proj_fov, gr_screen.clip_aspect, Min_draw_distance, Max_draw_distance);
gr_set_view_matrix(&Eye_position, &Eye_matrix);
}
gr_zbuffer_set(false);
gr_set_color(effect_params.fs2_wireframe_color.red, effect_params.fs2_wireframe_color.green, effect_params.fs2_wireframe_color.blue);
render_info->set_color(effect_params.fs2_wireframe_color.red, effect_params.fs2_wireframe_color.green, effect_params.fs2_wireframe_color.blue);
render_info->set_animated_effect(ANIMATED_SHADER_LOADOUTSELECT_FS2, -clip);
if ( (time < 2.5f) && (time >= 0.5f) ) { // Phase 1 and 2 render the wireframe
render_info->set_clip_plane(plane_point,wire_normal);
render_info->set_flags(flags | MR_SHOW_OUTLINE_HTL | MR_NO_POLYS | MR_NO_TEXTURING | MR_NO_LIGHTING);
model_render_immediate(render_info, model_id, model_instance, &model_orient, &vmd_zero_vector);
}
if (time >= 1.5f) { // Render the ship in Phase 2 onwards
render_info->set_clip_plane(plane_point,ship_normal);
render_info->set_flags(flags);
model_render_immediate(render_info, model_id, model_instance, &model_orient, &vmd_zero_vector);
}
if (time < 2.5f) { // Render the scanline in Phase 1 and 2
gr_set_color(effect_params.fs2_scanline_color.red, effect_params.fs2_scanline_color.green, effect_params.fs2_scanline_color.blue);
start.xyz.x = size*1.25f;
start.xyz.y = 0.0f;
start.xyz.z = -clip;
stop.xyz.x = -size*1.25f;
stop.xyz.y = 0.0f;
stop.xyz.z = -clip;
g3_start_instance_angles(&vmd_zero_vector,&view_angles);
//g3_draw_htl_line(&start,&stop);
g3_render_line_3d(false, &start, &stop);
g3_done_instance(true);
}
}
gr_zbuffer_set(GR_ZBUFF_FULL); // Turn off depthbuffer again
batching_render_all();
Glowpoint_use_depth_buffer = true; // Back to normal
gr_end_view_matrix();
gr_end_proj_matrix();
g3_end_frame();
gr_reset_clip();
} else {
// rotate the ship as much as required for this frame
*rotation_buffer += PI2 * flFrametime / rev_rate;
while (*rotation_buffer > PI2){
*rotation_buffer -= PI2;
}
view_angles.p = -0.6f;
view_angles.b = 0.0f;
view_angles.h = 0.0f;
vm_angles_2_matrix(&model_orient, &view_angles);
rot_angles.p = 0.0f;
rot_angles.b = 0.0f;
rot_angles.h = *rotation_buffer;
vm_rotate_matrix_by_angles(&model_orient, &rot_angles);
g3_start_frame(1);
// render the wodel
g3_set_view_matrix(closeup_pos, &vmd_identity_matrix, closeup_zoom);
//setup lights
common_setup_room_lights();
model_clear_instance(model_id);
render_info->set_detail_level_lock(0);
if(shadow_maybe_start_frame(shadow_disable_override))
{
if ( flags & MR_IS_MISSILE ) {
shadows_start_render(&Eye_matrix, &Eye_position, Proj_fov, Proj_fov, gr_screen.clip_aspect, SCP_vector {{-closeup_pos->xyz.z + pm->rad, -closeup_pos->xyz.z + pm->rad + 20.0f, -closeup_pos->xyz.z + pm->rad + 200.0f, -closeup_pos->xyz.z + pm->rad + 1000.0f}});
} else {
shadows_start_render(&Eye_matrix, &Eye_position, Proj_fov, Proj_fov, gr_screen.clip_aspect, SCP_vector {{-closeup_pos->xyz.z + pm->rad, -closeup_pos->xyz.z + pm->rad + 200.0f, -closeup_pos->xyz.z + pm->rad + 2000.0f, -closeup_pos->xyz.z + pm->rad + 10000.0f}});
}
model_render_params shadow_render_info;
shadow_render_info.set_flags(flags | MR_NO_TEXTURING | MR_NO_LIGHTING);
shadow_render_info.set_detail_level_lock(0);
model_render_immediate(&shadow_render_info, model_id, model_instance, &model_orient, &vmd_zero_vector);
shadows_end_render();
}
gr_set_clip(x1, y1, x2, y2, resize_mode);
gr_set_proj_matrix(Proj_fov, gr_screen.clip_aspect, Min_draw_distance, Max_draw_distance);
gr_set_view_matrix(&Eye_position, &Eye_matrix);
gr_set_color(0,128,0);
if (effect_params.effect == 1) { // FS1 effect
render_info->set_animated_effect(ANIMATED_SHADER_LOADOUTSELECT_FS1, MIN(time*0.5f,2.0f));
render_info->set_flags(flags);
} else {
render_info->set_flags(flags);
}
model_render_immediate(render_info, model_id, model_instance, &model_orient, &vmd_zero_vector);
batching_render_all();
gr_end_view_matrix();
gr_end_proj_matrix();
g3_end_frame();
gr_reset_clip();
}
shadow_end_frame();
}
/**
* @brief add lights for all the non-gameplay ship rendering instances
*/
void common_setup_room_lights()
{
light_reset();
auto tempv = vm_vec_new(-1.0f,0.3f,-1.0f);
auto tempc = hdr_color(1.0f,0.95f,0.9f, 0.0f, 1.5f);
light_add_directional(&tempv,-1,false,&tempc);
tempv.xyz={-0.4f,0.4f,1.1f};
tempc = hdr_color(0.788f,0.886f,1.0f,0.0f,1.5f);
light_add_directional(&tempv,-1,false,&tempc);
tempv.xyz={0.4f,0.1f,0.4f};
tempc = hdr_color(1.0f,1.0f,1.0f,0.0f,0.4f);
light_add_directional(&tempv,-1,false,&tempc);
gr_set_ambient_light(53, 53, 53);
}
// NEWSTUFF END