Files
fs2open.github.com/code/hud/hudartillery.cpp
T
Goober5000andClaude Opus 4.8 bed228d264 avoid undefined behavior from UNREACHABLE on reachable code paths (#7555)
* avoid undefined behavior from UNREACHABLE on reachable code paths

UNREACHABLE expands to __assume(false) / __builtin_unreachable() in
release builds, which is undefined behavior if the code is ever actually
reached. Many UNREACHABLE sites only guard against unexpected-but-
possible runtime states (values from tables, mission/save files, network
packets, scripting, or user input), so reaching them should degrade
gracefully rather than invoke UB.

Replace UNREACHABLE with Assertion(false, ...) at all such sites. It
still pops an error in debug builds, but in release it is a harmless
no-op that simply falls through to the following code. Sites that are
genuinely unreachable by local invariants (e.g. a switch over a bounded
random number) are left as UNREACHABLE.

A few converted sites had no safe fall-through -- an uninitialized read,
null dereference, or null call would have followed -- so they also get a
minimal recovery: initialize the value, guard the dereference, return
early, or skip the offending item.

Where the converted site was a simple guard of the form
    if (x) { Assertion(false, ...); return; }
hoist the check onto the invariant instead:
    Assertion(!x, ...);
    if (x) { return; }
This states the expectation directly and shows the offending value in
the debug popup, while keeping the same graceful runtime degradation.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* addendum

After consultation with Lafiel, use UNREACHABLE in situations that are effectively impossible even if not provably impossible.  UNREACHABLE = "this shouldn't ever happen"; Assertion = "if this ever happens, it's a coding error".  Also fix a few other sites and change a couple to Warnings.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-09 11:20:25 +09:00

525 lines
14 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 "ai/ai.h"
#include "fireball/fireballs.h"
#include "gamesnd/gamesnd.h"
#include "globalincs/alphacolors.h"
#include "globalincs/linklist.h"
#include "hud/hudartillery.h"
#include "hud/hudmessage.h"
#include "io/timer.h"
#include "math/vecmat.h"
#include "network/multi.h"
#include "object/object.h"
#include "parse/parselo.h"
#include "ship/shipfx.h"
#include "sound/sound.h"
#include "utils/Random.h"
#include "weapon/beam.h"
#include "weapon/weapon.h"
// -----------------------------------------------------------------------------------------------------------------------
// ARTILLERY DEFINES/VARS
//
// Goober5000 - moved to hudartillery.h
// -----------------------------------------------------------------------------------------------------------------------
// ARTILLERY FUNCTIONS
//
// test code for subspace missile strike -------------------------------------------
// ssm_info, like ship_info etc.
SCP_vector<ssm_info> Ssm_info;
// list of active strikes
SCP_list<ssm_strike> Ssm_strikes;
// Goober5000
int ssm_info_lookup(const char *name)
{
if(name == NULL)
return -1;
for (auto it = Ssm_info.cbegin(); it != Ssm_info.cend(); ++it)
if (!stricmp(name, it->name))
return (int)std::distance(Ssm_info.cbegin(), it);
return -1;
}
void parse_ssm(const char *filename)
{
char ssm_name[NAME_LENGTH];
char weapon_name[NAME_LENGTH];
try
{
read_file_text(filename, CF_TYPE_TABLES);
reset_parse();
// parse the table
while(required_string_either("#end", "$SSM:"))
{
bool no_create = false;
int string_index;
ssm_info *s, new_ssm;
s = &new_ssm;
// name
required_string("$SSM:");
stuff_string(ssm_name, F_NAME, NAME_LENGTH);
if (*ssm_name == 0)
{
sprintf(ssm_name, "SSM " SIZE_T_ARG, Ssm_info.size());
mprintf(("Found an SSM entry without a name. Assigning \"%s\".\n", ssm_name));
}
if (optional_string("+nocreate"))
{
no_create = true;
int i = ssm_info_lookup(ssm_name);
if (i >= 0)
s = &Ssm_info[i];
}
else
{
strcpy_s(s->name, ssm_name);
}
// stuff data
if (optional_string("+Weapon:"))
{
stuff_string(weapon_name, F_NAME, NAME_LENGTH);
// see if we have a valid weapon
s->weapon_info_index = weapon_info_lookup(weapon_name);
if (s->weapon_info_index < 0)
error_display(0, "Unknown weapon [%s] for SSM strike [%s]; this SSM strike will be discarded.", weapon_name, s->name);
}
string_index = optional_string_either("+Count:", "+Min Count:");
if (string_index == 0)
{
stuff_int(&s->count);
s->max_count = -1;
}
else if (string_index == 1)
{
stuff_int(&s->count);
required_string("+Max Count:");
stuff_int(&s->max_count);
}
if (optional_string("+WarpEffect:"))
{
char unique_id[NAME_LENGTH];
stuff_string(unique_id, F_NAME, NAME_LENGTH);
if (can_construe_as_integer(unique_id))
{
int temp = atoi(unique_id);
if (!SCP_vector_inbounds(Fireball_info, temp))
error_display(0, "Fireball index [%d] out of range (should be 0-%d) for SSM strike [%s]", temp, static_cast<int>(Fireball_info.size()) - 1, s->name);
else
s->fireball_type = temp;
}
else
{
// We have a string to parse instead.
int fireball_type = fireball_info_lookup(unique_id);
if (fireball_type < 0)
error_display(0, "Unknown fireball [%s] to use as warp effect for SSM strike [%s]", unique_id, s->name);
else
s->fireball_type = fireball_type;
}
}
if (optional_string("+WarpRadius:"))
{
stuff_float(&s->warp_radius);
}
if (optional_string("+WarpTime:"))
{
stuff_float(&s->warp_time);
// According to fireballs.cpp, "Warp lifetime must be at least 4 seconds!"
if (s->warp_time < 4.0f)
{
// So let's warn them before they try to use it, shall we?
error_display(0, "Expected a '+WarpTime:' value equal or greater than 4.0, found '%f' in SSM strike [%s].\nSetting to 4.0, please check and set to a number 4.0 or greater!", s->warp_time, s->name);
// And then make the Assert obsolete -- Zacam
s->warp_time = 4.0f;
}
}
string_index = optional_string_either("+Radius:", "+Min Radius:");
if (string_index == 0)
{
stuff_float(&s->radius);
s->max_radius = -1.0f;
}
else if (string_index == 1)
{
stuff_float(&s->radius);
required_string("+Max Radius:");
stuff_float(&s->max_radius);
}
string_index = optional_string_either("+Offset:", "+Min Offset:");
if (string_index == 0)
{
stuff_float(&s->offset);
s->max_offset = -1.0f;
}
else if (string_index == 1)
{
stuff_float(&s->offset);
required_string("+Max Offset:");
stuff_float(&s->max_offset);
}
if (optional_string("+Shape:"))
{
switch (required_string_one_of(3, "Point", "Circle", "Sphere"))
{
case 0:
required_string("Point");
s->shape = SSM_SHAPE_POINT;
break;
case 1:
required_string("Circle");
FALLTHROUGH;
case -1: // If we're ignoring parse errors and can't identify the shape, go with a circle.
s->shape = SSM_SHAPE_CIRCLE;
break;
case 2:
required_string("Sphere");
s->shape = SSM_SHAPE_SPHERE;
break;
default:
UNREACHABLE("Impossible return value from required_string_one_of(); get a coder!");
}
}
if (optional_string("+HUD Message:"))
stuff_boolean(&s->send_message);
if (optional_string("+Custom Message:"))
{
stuff_string(s->custom_message, F_NAME, NAME_LENGTH);
s->use_custom_message = true;
}
parse_game_sound("+Alarm Sound:", &s->sound_index);
// don't add new entry if this is just a modified one
if (!no_create)
Ssm_info.push_back(new_ssm);
}
}
catch (const parse::ParseException& e)
{
mprintf(("TABLES: Unable to parse '%s'! Error message = %s.\n", filename, e.what()));
return;
}
}
// game init
void ssm_init()
{
if (cf_exists_full("ssm.tbl", CF_TYPE_TABLES)) {
mprintf(("TABLES => Starting parse of 'ssm.tbl'...\n"));
parse_ssm("ssm.tbl");
}
parse_modular_table(NOX("*-ssm.tbm"), parse_ssm);
// We already warned the modder that an SSM strike is invalid without a valid weapon, so now remove any that are in the list
Ssm_info.erase(
std::remove_if(Ssm_info.begin(), Ssm_info.end(), [](const ssm_info &ssm)
{
return ssm.weapon_info_index < 0;
}),
Ssm_info.end()
);
// Now that we've populated Ssm_info, let's validate weapon $SSM: entries.
validate_SSM_entries();
}
void ssm_get_random_start_pos(vec3d *out, const vec3d *start, const matrix *orient, size_t ssm_index)
{
vec3d temp;
ssm_info *s = &Ssm_info[ssm_index];
float radius, offset;
if (s->max_radius == -1.0f)
radius = s->radius;
else
radius = frand_range(s->radius, s->max_radius);
if (s->max_offset == -1.0f)
offset = s->offset;
else
offset = frand_range(s->offset, s->max_offset);
switch (s->shape) {
case SSM_SHAPE_SPHERE:
// get a random vector in a sphere around the target
vm_vec_random_in_sphere(&temp, start, radius, true);
break;
case SSM_SHAPE_CIRCLE:
// get a random vector in the circle of the firing plane
vm_vec_random_in_circle(&temp, start, orient, radius, true);
break;
case SSM_SHAPE_POINT:
// boooring
vm_vec_scale_add(&temp, start, &orient->vec.fvec, radius);
break;
default:
UNREACHABLE("Unknown shape '%d' in SSM type #" SIZE_T_ARG " ('%s'). This should not be possible; get a coder!\n", s->shape, ssm_index, s->name);
temp = *start;
break;
}
// offset it a bit
vm_vec_scale_add(out, &temp, &orient->vec.fvec, offset);
}
// level init
void ssm_level_init()
{
}
// start a subspace missile effect
// (it might be possible to make `target` const, but that would set off another const-cascade)
void ssm_create(object *target, const vec3d *start, size_t ssm_index, const ssm_firing_info *override, int team)
{
ssm_strike ssm;
matrix dir;
int idx, count;
// sanity
Assert(target != NULL);
if(target == NULL){
return;
}
Assert(start != NULL);
if(start == NULL){
return;
}
if (ssm_index >= Ssm_info.size()) {
return;
}
// Init the ssm data
count = Ssm_info[ssm_index].count;
if (Ssm_info[ssm_index].max_count != -1 && (Ssm_info[ssm_index].max_count - count) > 0) {
count = Random::next(count, Ssm_info[ssm_index].max_count);
}
// override in multiplayer
if(override != NULL){
ssm.sinfo = *override;
}
// single player or the server
else {
// forward orientation
vec3d temp;
vm_vec_normalized_dir(&temp, &target->pos, start);
vm_vector_2_matrix_norm(&dir, &temp, nullptr, nullptr);
// stuff info
ssm.sinfo.count = count;
ssm.sinfo.ssm_index = ssm_index;
ssm.sinfo.target = target;
ssm.sinfo.ssm_team = team;
// Instead of pushing them on one at a time, let's just grab all the memory we'll need at once
// (as a side effect, don't need to change the logic from the old array-based code)
ssm.sinfo.delay_stamp.resize(count);
ssm.sinfo.start_pos.resize(count);
for (idx = 0; idx < count; idx++) {
ssm.sinfo.delay_stamp[idx] = _timestamp(200 + Random::next(-199, 1000));
ssm_get_random_start_pos(&ssm.sinfo.start_pos[idx], start, &dir, ssm_index);
}
ssm_info *si = &Ssm_info[ssm_index];
weapon_info *wip = &Weapon_info[si->weapon_info_index];
if (wip->wi_flags[Weapon::Info_Flags::Beam]) {
ssm.sinfo.duration = ((si->warp_time - ((wip->b_info.beam_warmup / 1000.0f) + wip->b_info.beam_life + (wip->b_info.beam_warmdown / 1000.0f))) / 2.0f) / si->warp_time;
} else {
ssm.sinfo.duration = 0.5f;
}
// if we're the server, send a packet
if(MULTIPLAYER_MASTER){
//
}
}
ssm.done_flags.clear();
ssm.done_flags.resize(count);
ssm.fireballs.clear();
ssm.fireballs.resize(count, -1);
if(Ssm_info[ssm_index].send_message) {
if (!Ssm_info[ssm_index].use_custom_message)
HUD_printf("%s", XSTR("Firing artillery", 1570));
else
HUD_printf("%s", Ssm_info[ssm_index].custom_message);
}
if (Ssm_info[ssm_index].sound_index.isValid()) {
snd_play(gamesnd_get_game_sound(Ssm_info[ssm_index].sound_index));
}
Ssm_strikes.push_back(std::move(ssm));
}
// delete a finished ssm effect
void ssm_delete(SCP_list<ssm_strike>::iterator ssm)
{
Ssm_strikes.erase(ssm);
}
// process subspace missile stuff
void ssm_process()
{
int idx, finished;
SCP_list<ssm_strike>::iterator moveup, eraser;
ssm_info *si;
int weapon_objnum;
// process all strikes
moveup = Ssm_strikes.begin();
while ( moveup != Ssm_strikes.end() ) {
// get the type
Assertion(moveup->sinfo.ssm_index < Ssm_info.size(), "Invalid SSM index detected!");
si = &Ssm_info[moveup->sinfo.ssm_index];
// check all the individual missiles
finished = 1;
for(idx=0; idx<moveup->sinfo.count; idx++){
// if this guy is not marked as done
if(!moveup->done_flags[idx]){
finished = 0;
// if he already has the fireball effect
if(moveup->fireballs[idx] >= 0){
if ((1.0f - fireball_lifeleft_percent(&Objects[moveup->fireballs[idx]])) >= moveup->sinfo.duration) {
weapon_info *wip = &Weapon_info[si->weapon_info_index];
// get an orientation
vec3d temp;
matrix orient;
vm_vec_normalized_dir(&temp, &moveup->sinfo.target->pos, &moveup->sinfo.start_pos[idx]);
vm_vector_2_matrix_norm(&orient, &temp, nullptr, nullptr);
// are we a beam? -MageKing17
if (wip->wi_flags[Weapon::Info_Flags::Beam]) {
int num_beams = wip->b_info.beam_type == BeamType::OMNI && (int)wip->b_info.t5info.burst_rot_pattern.size() > 1 ?
(int)wip->b_info.t5info.burst_rot_pattern.size() : 1;
for (int i = 0; i < num_beams; i++) {
beam_fire_info fire_info;
memset(&fire_info, 0, sizeof(beam_fire_info));
fire_info.accuracy = 0.000001f; // this will guarantee a hit
fire_info.shooter = nullptr;
fire_info.turret = nullptr;
fire_info.target = moveup->sinfo.target;
fire_info.target_subsys = nullptr;
fire_info.bfi_flags |= BFIF_FLOATING_BEAM;
fire_info.starting_pos = moveup->sinfo.start_pos[idx];
fire_info.beam_info_index = si->weapon_info_index;
fire_info.team = static_cast<char>(moveup->sinfo.ssm_team);
fire_info.fire_method = BFM_SUBSPACE_STRIKE;
fire_info.burst_index = i;
// Fill target_pos but DONT turn on BFIF_TARGETING_COORDS
// It would mess up normal beams but type 5s can still make use of this
fire_info.target_pos1 = orient.vec.fvec + fire_info.starting_pos;
// fire the beam
beam_fire(&fire_info);
moveup->done_flags[idx] = true;
}
} else {
// fire the missile and flash the screen
weapon_objnum = weapon_create(&moveup->sinfo.start_pos[idx], &orient, si->weapon_info_index, -1, -1, true);
if (weapon_objnum >= 0) {
Weapons[Objects[weapon_objnum].instance].team = moveup->sinfo.ssm_team;
Weapons[Objects[weapon_objnum].instance].homing_object = moveup->sinfo.target;
Weapons[Objects[weapon_objnum].instance].target_sig = moveup->sinfo.target->signature;
}
// this makes this particular missile done
moveup->done_flags[idx] = true;
}
}
}
// maybe create his warpin effect
else if((moveup->sinfo.delay_stamp[idx].isValid()) && timestamp_elapsed(moveup->sinfo.delay_stamp[idx])){
// get an orientation
vec3d temp;
matrix orient;
vm_vec_normalized_dir(&temp, &moveup->sinfo.target->pos, &moveup->sinfo.start_pos[idx]);
vm_vector_2_matrix_norm(&orient, &temp, nullptr, nullptr);
moveup->fireballs[idx] = fireball_create(&moveup->sinfo.start_pos[idx], si->fireball_type, FIREBALL_WARP_EFFECT, -1, si->warp_radius, false, &vmd_zero_vector, si->warp_time, 0, &orient);
}
}
}
if(finished){
eraser = moveup;
++moveup;
ssm_delete(eraser);
continue;
}
++moveup;
}
}
// test code for subspace missile strike -------------------------------------------
// level init
void hud_init_artillery()
{
}
// update all hud artillery related stuff
void hud_artillery_update()
{
}
// render all hud artillery related stuff
void hud_artillery_render()
{
// render how long the player has been painting his target
if((Player_ai != NULL) && (Player_ai->artillery_objnum >= 0)){
gr_set_color_fast(&Color_bright_blue);
gr_printf_no_resize(gr_screen.center_offset_x + 10, gr_screen.center_offset_y + 50, "%f", Player_ai->artillery_lock_time);
}
}