Files
fs2open.github.com/code/mission/missionlog.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

843 lines
27 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 "globalincs/alphacolors.h"
#include "graphics/font.h"
#include "iff_defs/iff_defs.h"
#include "localization/localize.h"
#include "mission/missiongoals.h"
#include "mission/missionlog.h"
#include "mission/missionparse.h"
#include "network/multi.h"
#include "network/multimsgs.h"
#include "network/multiutil.h"
#include "parse/parselo.h"
#include "playerman/player.h"
#include "ship/ship.h"
#define EMPTY_LOG_NAME ""
// defines for X position offsets of different items for mission log
#define TIME_X 10
#define OBJECT_X 75
#define ACTION_X 250
#define NUM_LOG_COLORS 3
static int X, Line_offset, P_width;
SCP_vector<log_line_complete> Log_scrollback_vec;
SCP_vector<log_entry> Log_entries;
void mission_log_init()
{
// zero out all the memory so we don't get bogus information when playing across missions!
Log_entries.clear();
}
// following function adds an entry into the mission log.
// pass a type and a string which indicates the object
// that this event is for. Don't add entries with this function for multiplayer
void mission_log_add_entry(LogType type, const char *pname, const char *sname, int info_index, int flags)
{
// multiplayer clients don't use this function to add log entries -- they will get
// all their info from the host
if ( MULTIPLAYER_CLIENT ){
return;
}
Log_entries.emplace_back();
auto &entry = Log_entries.back();
entry.type = type;
if ( pname ) {
Assert (strlen(pname) < NAME_LENGTH);
strncpy_s(entry.pname, pname, NAME_LENGTH - 1);
} else
strcpy_s( entry.pname, EMPTY_LOG_NAME );
if ( sname ) {
Assert (strlen(sname) < NAME_LENGTH);
strncpy_s(entry.sname, sname, NAME_LENGTH - 1);
} else
strcpy_s( entry.sname, EMPTY_LOG_NAME );
entry.index = info_index;
entry.flags = flags;
entry.primary_team = -1;
entry.secondary_team = -1;
entry.pname_display = entry.pname;
entry.sname_display = entry.sname;
end_string_at_first_hash_symbol(entry.pname_display);
end_string_at_first_hash_symbol(entry.sname_display);
// determine the contents of the flags member based on the type of entry we added. We need to store things
// like team for the primary and (possibly) secondary object for this entry.
switch ( type ) {
int index;
case LOG_SHIP_DESTROYED:
case LOG_SHIP_ARRIVED:
case LOG_SHIP_DEPARTED:
case LOG_SHIP_DOCKED:
case LOG_SHIP_SUBSYS_DESTROYED:
case LOG_SHIP_UNDOCKED:
case LOG_SHIP_DISABLED:
case LOG_SHIP_DISARMED:
case LOG_SELF_DESTRUCTED:
// multiplayer. callsign is passed in for ship destroyed and self destruct
if((Game_mode & GM_MULTIPLAYER) && (multi_find_player_by_callsign(pname) >= 0)){
int np_index = multi_find_player_by_callsign(pname);
index = multi_get_player_ship( np_index );
} else {
index = ship_name_lookup( pname );
}
Assert (index >= 0);
entry.primary_team = Ships[index].team;
entry.pname_display = Ships[index].get_display_name();
if (Ships[index].flags[Ship::Ship_Flags::Hide_mission_log]) {
entry.flags |= MLF_HIDDEN;
}
// some of the entries have a secondary component. Figure out what is up with them.
if ( (type == LOG_SHIP_DOCKED) || (type == LOG_SHIP_UNDOCKED)) {
if ( sname ) {
index = ship_name_lookup( sname );
Assert (index >= 0);
entry.secondary_team = Ships[index].team;
entry.sname_display = Ships[index].get_display_name();
}
} else if ( type == LOG_SHIP_DESTROYED ) {
if ( sname ) {
// multiplayer, player name will possibly be sent in
if((Game_mode & GM_MULTIPLAYER) && (multi_find_player_by_callsign(sname) >= 0)) {
// get the player's ship
int np_index = multi_find_player_by_callsign(sname);
int np_ship = multi_get_player_ship(np_index);
if(np_ship < 0)
{
// argh. badness
Int3();
entry.secondary_team = Player_ship->team;
}
else
{
entry.secondary_team = Ships[Objects[Net_players[np_index].m_player->objnum].instance].team;
entry.sname_display = Ships[Objects[Net_players[np_index].m_player->objnum].instance].get_display_name();
}
}
else
{
index = ship_name_lookup( sname );
// no ship, then it probably exited -- check the exited
if ( index == -1 ) {
index = ship_find_exited_ship_by_name( sname );
if ( index == -1 ) {
break;
}
entry.secondary_team = Ships_exited[index].team;
entry.sname_display = Ships_exited[index].display_name;
} else {
entry.secondary_team = Ships[index].team;
entry.sname_display = Ships[index].get_display_name();
}
}
} else {
nprintf(("missionlog", "No secondary name for ship destroyed log entry!\n"));
}
} else if ( (type == LOG_SHIP_SUBSYS_DESTROYED) && (Ship_info[Ships[index].ship_info_index].is_small_ship()) ) {
// make subsystem destroyed entries for small ships hidden
entry.flags |= MLF_HIDDEN;
} else if ( (type == LOG_SHIP_ARRIVED) && (Ships[index].wingnum != -1 ) ) {
// arrival of ships in wings don't display
entry.flags |= MLF_HIDDEN;
}
break;
case LOG_WING_DESTROYED:
case LOG_WING_DEPARTED:
case LOG_WING_ARRIVED:
index = wing_name_lookup(pname, 1);
Assert(index != -1);
Assert(info_index != -1); // this is the team value
entry.pname_display = Wings[index].get_display_name();
// get the team value for this wing. Departed or destroyed wings will pass the team
// value in info_index parameter. For arriving wings, get the team value from the
// first ship in the list because the info_index contains the wave count
if ( type == LOG_WING_ARRIVED ) {
int i, si = -1;
// Goober5000 - get the team value from any ship in the list, because
// ships that arrive initially docked could be created in random order
for (i = 0; i < MAX_SHIPS_PER_WING; ++i) {
// get first valid ship
si = Wings[index].ship_index[i];
if (si >= 0) {
break;
}
}
Assert( si != -1 );
entry.primary_team = Ships[si].team;
} else {
entry.primary_team = info_index;
}
#ifndef NDEBUG
// MWA 2/25/98. debug code to try to find any ships in this wing that have departed.
// scan through all log entries and find at least one ship_depart entry for a ship
// that was in this wing.
if ( type == LOG_WING_DEPARTED ) {
// if all were destroyed, then don't do this debug code.
if ( (Wings[index].total_destroyed + Wings[index].total_vanished) == Wings[index].total_arrived_count ){
break;
}
if (std::find_if(Log_entries.begin(), Log_entries.end(), [index](const log_entry& le) {
return (le.type == LOG_SHIP_DEPARTED) && (le.index == index);
}) == Log_entries.end()) {
Assertion(false, "cannot find any departed ships from wing %s that supposedly departed.", Wings[index].name); // get Allender
}
}
#endif
break;
// don't display waypoint done entries
case LOG_WAYPOINTS_DONE:
entry.flags |= MLF_HIDDEN;
break;
default:
break;
}
entry.timestamp = Missiontime;
entry.timer_padding = The_mission.HUD_timer_padding;
// if in multiplayer and I am the master, send this log entry to everyone
if ( MULTIPLAYER_MASTER ){
send_mission_log_packet( &entry );
}
#ifndef NDEBUG
float mission_time = f2fl(Missiontime);
int minutes = (int)(mission_time / 60);
int seconds = (int)mission_time % 60;
// record the entry to the debug log too
switch (entry.type) {
case LOG_SHIP_DESTROYED:
case LOG_WING_DESTROYED:
nprintf(("missionlog", "MISSION LOG: %s destroyed at %02d:%02d\n", entry.pname, minutes, seconds));
break;
case LOG_SHIP_ARRIVED:
case LOG_WING_ARRIVED:
nprintf(("missionlog", "MISSION LOG: %s arrived at %02d:%02d\n", entry.pname, minutes, seconds));
break;
case LOG_SHIP_DEPARTED:
case LOG_WING_DEPARTED:
nprintf(("missionlog", "MISSION LOG: %s departed at %02d:%02d\n", entry.pname, minutes, seconds));
break;
case LOG_SHIP_DOCKED:
case LOG_SHIP_UNDOCKED:
nprintf(("missionlog", "MISSION LOG: %s %sdocked with %s at %02d:%02d\n", entry.pname, entry.type == LOG_SHIP_UNDOCKED ? "un" : "", entry.sname, minutes, seconds));
break;
case LOG_SHIP_SUBSYS_DESTROYED:
nprintf(("missionlog", "MISSION LOG: %s subsystem %s destroyed at %02d:%02d\n", entry.pname, entry.sname, minutes, seconds));
break;
case LOG_SHIP_DISABLED:
nprintf(("missionlog", "MISSION LOG: %s disabled at %02d:%02d\n", entry.pname, minutes, seconds));
break;
case LOG_SHIP_DISARMED:
nprintf(("missionlog", "MISSION LOG: %s disarmed at %02d:%02d\n", entry.pname, minutes, seconds));
break;
case LOG_PLAYER_CALLED_FOR_REARM:
nprintf(("missionlog", "MISSION LOG: Player %s called for rearm at %02d:%02d\n", entry.pname, minutes, seconds));
break;
case LOG_PLAYER_ABORTED_REARM:
nprintf(("missionlog", "MISSION LOG: Player %s aborted rearm at %02d:%02d\n", entry.pname, minutes, seconds));
break;
case LOG_PLAYER_CALLED_FOR_REINFORCEMENT:
nprintf(("missionlog", "MISSION LOG: A player called for reinforcement %s at %02d:%02d\n", entry.pname, minutes, seconds));
break;
case LOG_GOAL_SATISFIED:
nprintf(("missionlog", "MISSION LOG: Goal %s satisfied at %02d:%02d\n", entry.pname, minutes, seconds));
break;
case LOG_GOAL_FAILED:
nprintf(("missionlog", "MISSION LOG: Goal %s failed at %02d:%02d\n", entry.pname, minutes, seconds));
break;
case LOG_WAYPOINTS_DONE:
nprintf(("missionlog", "MISSION LOG: %s completed waypoint path %s at %02d:%02d\n", entry.pname, entry.sname, minutes, seconds));
break;
case LOG_CARGO_REVEALED:
nprintf(("missionlog", "MISSION LOG: %s cargo %s revealed at %02d:%02d\n", entry.pname, Cargo_names[entry.index], minutes, seconds));
break;
case LOG_CAP_SUBSYS_CARGO_REVEALED:
nprintf(("missionlog", "MISSION LOG: %s subsystem %s cargo %s revealed at %02d:%02d\n", entry.pname, entry.sname, Cargo_names[entry.index], minutes, seconds));
break;
case LOG_SELF_DESTRUCTED:
nprintf(("missionlog", "MISSION LOG: %s self-destructed at %02d:%02d\n", entry.pname, minutes, seconds));
break;
}
#endif
}
// function, used in multiplayer only, which adds an entry sent by the host of the game, into
// the mission log. The index of the log entry is passed as one of the parameters in addition to
// the normal parameters used for adding an entry to the log
void mission_log_add_entry_multi( LogType type, const char *pname, const char *sname, int index, fix timestamp, int flags )
{
// we'd better be in multiplayer and not the master of the game
Assert ( Game_mode & GM_MULTIPLAYER );
Assert ( !(Net_player->flags & NETINFO_FLAG_AM_MASTER) );
Log_entries.emplace_back();
auto &entry = Log_entries.back();
entry.type = type;
if ( pname ) {
Assert (strlen(pname) < NAME_LENGTH);
strcpy_s(entry.pname, pname);
}
if ( sname ) {
Assert (strlen(sname) < NAME_LENGTH);
strcpy_s(entry.sname, sname);
}
entry.index = index;
entry.flags = flags;
entry.timestamp = timestamp;
entry.timer_padding = The_mission.HUD_timer_padding;
entry.pname_display = entry.pname;
entry.sname_display = entry.sname;
}
// function to determine is the given event has taken place count number of times.
int mission_log_get_time_indexed( LogType type, const char *pname, const char *sname, int count, fix *time)
{
Assertion(count > 0, "The count parameter is %d; it should be greater than 0!", count);
for (const auto& entry: Log_entries) {
bool found = false;
if ( entry.type == type ) {
// if we are looking for a dock/undock entry, then we don't care about the order in which the names
// were passed into this function. Count the entry as found if either name matches both in the other
// set.
if ( (type == LOG_SHIP_DOCKED) || (type == LOG_SHIP_UNDOCKED) ) {
if (sname == NULL) {
Int3();
return 0;
}
if ( (!stricmp(entry.pname, pname) && !stricmp(entry.sname, sname)) || (!stricmp(entry.pname, sname) && !stricmp(entry.sname, pname)) ) {
found = true;
}
} else {
// for non dock/undock goals, then the names are important!
if (pname == NULL) {
Int3();
return 0;
}
if ( stricmp(entry.pname, pname) != 0 ) {
continue;
}
// if we are looking for a subsystem entry, the subsystem names must be compared
if ((type == LOG_SHIP_SUBSYS_DESTROYED || type == LOG_CAP_SUBSYS_CARGO_REVEALED)) {
if ( (sname == NULL) || !subsystem_stricmp(sname, entry.sname) ) {
found = true;
}
} else {
if ( (sname == NULL) || !stricmp(sname, entry.sname) ) {
found = true;
}
}
}
if ( found ) {
count--;
if ( !count ) {
if (time) {
*time = entry.timestamp;
}
return 1;
}
}
}
}
return 0;
}
// this function determines if the given type of event on the specified
// object has taken place yet. If not, it returns 0. If it has, the
// timestamp that the event happened is returned in the time parameter
int mission_log_get_time( LogType type, const char *pname, const char *sname, fix *time )
{
return mission_log_get_time_indexed( type, pname, sname, 1, time );
}
// determines the number of times the given type of event takes place
int mission_log_get_count( LogType type, const char *pname, const char *sname )
{
int count = 0;
for (const auto& entry : Log_entries) {
if ( entry.type == type ) {
// if we are looking for a dock/undock entry, then we don't care about the order in which the names
// were passed into this function. Count the entry as found if either name matches both in the other
// set.
if ( (type == LOG_SHIP_DOCKED) || (type == LOG_SHIP_UNDOCKED) ) {
if (sname == NULL) {
Int3();
return 0;
}
if ( (!stricmp(entry.pname, pname) && !stricmp(entry.sname, sname)) || (!stricmp(entry.pname, sname) && !stricmp(entry.sname, pname)) ) {
count++;
}
} else {
// for non dock/undock goals, then the names are important!
if (pname == NULL) {
Int3();
return 0;
}
if ( stricmp(entry.pname, pname) != 0 ) {
continue;
}
if ( (sname == NULL) || !stricmp(sname, entry.sname) ) {
count++;
}
}
}
}
return count;
}
void message_log_add_seg(log_text_seg *entry, int x, int line_offset, int msg_color, const char *text, int flags = 0)
{
// set the vector
entry->text.reset(vm_strdup(text));
entry->color = msg_color;
entry->x = x;
entry->line_offset = line_offset;
entry->flags = flags;
}
void message_log_add_seg(log_text_seg *entry, int x, int line_offset, int msg_color, const char *text, size_t len, int flags = 0)
{
// set the vector
entry->text.reset(vm_strndup(text, len));
entry->color = msg_color;
entry->x = x;
entry->line_offset = line_offset;
entry->flags = flags;
}
void message_log_add_segs(const char *source_string, int msg_color, int flags, SCP_vector<log_text_seg> *entry, bool split_string)
{
if (!source_string || !entry) {
mprintf(("Why are you passing a NULL pointer to message_log_add_segs?\n"));
return;
}
if (!*source_string) {
return;
}
// duplicate the string so that we can split it without modifying the source
char *dup_string = vm_strdup(source_string);
char *str = dup_string;
char *split = NULL;
if (split_string) {
int sanity_counter = 0;
while (true) {
if (X == ACTION_X) {
while (is_white_space(*str))
str++;
}
if (P_width - X > 0) {
split = split_str_once(str, P_width - X);
// if we couldn't actually split the string, try again on a new line
if (split == str) {
sanity_counter++;
if (sanity_counter > 5) {
Error(LOCATION, "Too many attempts to wrap a mission log line! Get a coder!\nLine = %s", str);
break;
}
} else {
log_text_seg new_seg;
message_log_add_seg(&new_seg, X, Line_offset, msg_color, str, flags);
entry->push_back(std::move(new_seg));
if (!split) {
int w;
gr_get_string_size(&w, nullptr, str);
X += w;
break;
}
}
}
X = ACTION_X;
Line_offset++;
str = split;
}
} else {
log_text_seg new_seg;
message_log_add_seg(&new_seg, X, Line_offset, msg_color, str, flags);
entry->push_back(std::move(new_seg));
}
// free the buffer
vm_free(dup_string);
}
int mission_log_color_get_team(int msg_color)
{
return msg_color - NUM_LOG_COLORS;
}
int mission_log_team_get_color(int team)
{
return NUM_LOG_COLORS + team;
}
// pw = total pixel width
void mission_log_init_scrollback(int pw, bool split_string)
{
P_width = pw;
Log_scrollback_vec.clear();
for (const auto& entry : Log_entries) {
if (entry.flags & MLF_HIDDEN)
continue;
// don't display failed bonus goals
if (entry.type == LOG_GOAL_FAILED) {
int type = Mission_goals[entry.index].type & GOAL_TYPE_MASK;
if (type == BONUS_GOAL) {
continue;
}
}
log_line_complete thisEntry;
// track time of event (normal Missiontime format)
thisEntry.timestamp = entry.timestamp;
thisEntry.timer_padding = entry.timer_padding;
// keep track of base color for the entry
int thisColor;
// Goober5000
if ((entry.type == LOG_GOAL_SATISFIED) || (entry.type == LOG_GOAL_FAILED))
thisColor = LOG_COLOR_BRIGHT;
else if (entry.primary_team >= 0)
thisColor = mission_log_team_get_color(entry.primary_team);
else
thisColor = LOG_COLOR_OTHER;
if ( (Lcl_gr) && ((entry.type == LOG_GOAL_FAILED) || (entry.type == LOG_GOAL_SATISFIED)) ) {
// in german goal events, just say "objective" instead of objective name
// this is cuz we can't translate objective names
message_log_add_seg(&thisEntry.objective, OBJECT_X, 0, thisColor, "Einsatzziel");
} else if ( (Lcl_pl) && ((entry.type == LOG_GOAL_FAILED) || (entry.type == LOG_GOAL_SATISFIED)) ) {
// same thing for polish
message_log_add_seg(&thisEntry.objective, OBJECT_X, 0, thisColor, "Cel misji");
} else {
message_log_add_seg(&thisEntry.objective, OBJECT_X, 0, thisColor, entry.pname_display.c_str());
}
//Set the flags for objectives
if (entry.type == LOG_GOAL_SATISFIED) {
thisEntry.objective.flags = LOG_FLAG_GOAL_TRUE;
} else if (entry.type == LOG_GOAL_FAILED) {
thisEntry.objective.flags = LOG_FLAG_GOAL_FAILED;
} else {
thisEntry.objective.flags = 0;
}
// now on to the actual message itself
X = ACTION_X;
Line_offset = 0;
// Goober5000
if (entry.secondary_team >= 0)
thisColor = mission_log_team_get_color(entry.secondary_team);
else
thisColor = LOG_COLOR_NORMAL;
char text[256];
switch (entry.type) {
case LOG_SHIP_DESTROYED:
message_log_add_segs(XSTR( "Destroyed", 404), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
if (!entry.sname_display.empty()) {
message_log_add_segs(XSTR(" Kill: ", 405), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
message_log_add_segs(entry.sname_display.c_str(), thisColor, 0, &thisEntry.segments, split_string);
if (entry.index >= 0) {
sprintf(text, NOX(" (%d%%)"), entry.index);
message_log_add_segs(text, LOG_COLOR_BRIGHT, 0, &thisEntry.segments, split_string);
}
}
break;
case LOG_SELF_DESTRUCTED:
message_log_add_segs(XSTR("Self destructed", 1476), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
break;
case LOG_WING_DESTROYED:
message_log_add_segs(XSTR("Destroyed", 404), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
break;
case LOG_SHIP_ARRIVED:
message_log_add_segs(XSTR("Arrived", 406), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
break;
case LOG_WING_ARRIVED:
if (entry.index > 1){
sprintf(text, XSTR( "Arrived (wave %d)", 407), entry.index);
} else {
strcpy_s(text, XSTR( "Arrived", 406));
}
message_log_add_segs(text, LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
break;
case LOG_SHIP_DEPARTED:
message_log_add_segs(XSTR("Departed", 408), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
break;
case LOG_WING_DEPARTED:
message_log_add_segs(XSTR("Departed", 408), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
break;
case LOG_SHIP_DOCKED:
message_log_add_segs(XSTR("Docked with ", 409), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
message_log_add_segs(entry.sname_display.c_str(), thisColor, 0, &thisEntry.segments, split_string);
break;
case LOG_SHIP_SUBSYS_DESTROYED: {
ship_info* sip = &Ship_info[(int)((entry.index >> 16) & 0xffff)];
int model_index = (int)(entry.index & 0xffff);
const char* subsys_name;
if (strlen(sip->subsystems[model_index].alt_sub_name)) {
subsys_name = sip->subsystems[model_index].alt_sub_name;
} else {
subsys_name = sip->subsystems[model_index].name;
}
message_log_add_segs(XSTR("Subsystem ", 410), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
message_log_add_segs(subsys_name, LOG_COLOR_BRIGHT, 0, &thisEntry.segments, split_string);
message_log_add_segs(XSTR(" destroyed", 411), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
break;
}
case LOG_SHIP_UNDOCKED:
message_log_add_segs(XSTR("Undocked with ", 412), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
message_log_add_segs(entry.sname_display.c_str(), thisColor, 0, &thisEntry.segments, split_string);
break;
case LOG_SHIP_DISABLED:
message_log_add_segs(XSTR("Disabled", 413), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
break;
case LOG_SHIP_DISARMED:
message_log_add_segs(XSTR("Disarmed", 414), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
break;
case LOG_PLAYER_CALLED_FOR_REARM:
message_log_add_segs(XSTR(" called for rearm", 415), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
break;
case LOG_PLAYER_ABORTED_REARM:
message_log_add_segs(XSTR(" aborted rearm", 416), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
break;
case LOG_PLAYER_CALLED_FOR_REINFORCEMENT:
message_log_add_segs(XSTR("Called in as reinforcement", 417), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
break;
case LOG_CARGO_REVEALED:
Assert( entry.index >= 0 );
Assert(!(entry.index & CARGO_NO_DEPLETE));
message_log_add_segs(XSTR("Cargo revealed: ", 418), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
strncpy(text, Cargo_names[entry.index], sizeof(text) - 1);
message_log_add_segs(text, LOG_COLOR_BRIGHT, 0, &thisEntry.segments, split_string);
break;
case LOG_CAP_SUBSYS_CARGO_REVEALED:
Assert( entry.index >= 0 );
Assert(!(entry.index & CARGO_NO_DEPLETE));
message_log_add_segs(entry.sname_display.c_str(), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
message_log_add_segs(XSTR( " subsystem cargo revealed: ", 1488), LOG_COLOR_NORMAL, 0, &thisEntry.segments, split_string);
strncpy(text, Cargo_names[entry.index], sizeof(text) - 1);
message_log_add_segs(text, LOG_COLOR_BRIGHT, 0, &thisEntry.segments, split_string);
break;
case LOG_GOAL_SATISFIED:
case LOG_GOAL_FAILED: {
int type = Mission_goals[entry.index].type & GOAL_TYPE_MASK;
sprintf( text, XSTR( "%s objective ", 419), Goal_type_text(type) );
if ( entry.type == LOG_GOAL_SATISFIED )
strcat_s(text, XSTR( "satisfied.", 420));
else
strcat_s(text, XSTR( "failed.", 421));
message_log_add_segs(text, LOG_COLOR_BRIGHT, 0, &thisEntry.segments, split_string);
break;
} // matches case statement!
default:
UNREACHABLE("Unhandled enum value %d!", entry.type);
break;
}
Log_scrollback_vec.push_back(std::move(thisEntry));
}
}
void mission_log_shutdown_scrollback()
{
Log_scrollback_vec.clear();
}
int mission_log_scrollback_num_lines()
{
return static_cast<int>(Log_scrollback_vec.size());
}
const log_line_complete* mission_log_scrollback_get_line(int index)
{
if (!SCP_vector_inbounds(Log_scrollback_vec, index))
return nullptr;
return &Log_scrollback_vec[index];
}
const color *log_line_get_color(int tag)
{
switch (tag) {
case LOG_COLOR_BRIGHT:
return &Color_bright;
case LOG_COLOR_OTHER:
return &Color_normal;
default: {
int team = mission_log_color_get_team(tag);
if (team < 0)
return &Color_text_normal;
else
return iff_get_color_by_team(team, -1, 1);
}
}
}
// message_log_scrollback displays the contents of the mesasge log currently much like the HUD
// message scrollback system. I'm sure this system will be overhauled.
void mission_log_scrollback(int line_offset, int list_x, int list_y, int list_w, int list_h)
{
int font_h = gr_get_font_height();
int y = 0;
for (int i = line_offset; i < mission_log_scrollback_num_lines(); i++) {
// if we're beyond the printable area stop printing!
if (y + font_h > list_h)
break;
bool printSymbols = false;
int symbolFlag = 0;
// print the timestamp
gr_set_color_fast(&Color_text_normal);
gr_print_timestamp(list_x + TIME_X, list_y + y, Log_scrollback_vec[i].timestamp, GR_RESIZE_MENU);
// print the objective
auto obj_color = log_line_get_color(Log_scrollback_vec[i].objective.color);
gr_set_color_fast(obj_color);
// check the flags
if ((Log_scrollback_vec[i].objective.flags & LOG_FLAG_GOAL_TRUE) || (Log_scrollback_vec[i].objective.flags & LOG_FLAG_GOAL_FAILED)) {
printSymbols = true;
symbolFlag = Log_scrollback_vec[i].objective.flags;
}
char buf[256];
strcpy_s(buf, Log_scrollback_vec[i].objective.text.get());
font::force_fit_string(buf, 255, ACTION_X - OBJECT_X - 8);
gr_string(list_x + Log_scrollback_vec[i].objective.x, list_y + y, buf, GR_RESIZE_MENU);
// print the segments
int max_seg_line_offset = 0;
for (int j = 0; j < (int)Log_scrollback_vec[i].segments.size(); j++) {
const auto& thisSeg = Log_scrollback_vec[i].segments[j];
if (thisSeg.line_offset > max_seg_line_offset)
max_seg_line_offset = thisSeg.line_offset;
int seg_y = y + (font_h * thisSeg.line_offset);
// extra printable area check, since we may have multiple lines here
if (seg_y + font_h > list_h)
break;
auto this_color = log_line_get_color(Log_scrollback_vec[i].segments[j].color);
gr_set_color_fast(this_color);
strcpy_s(buf, thisSeg.text.get());
font::force_fit_string(buf, 255, list_w - thisSeg.x);
gr_string(list_x + thisSeg.x, list_y + seg_y, buf, GR_RESIZE_MENU);
}
if (printSymbols) {
if (symbolFlag & LOG_FLAG_GOAL_FAILED)
gr_set_color_fast(&Color_bright_red);
else
gr_set_color_fast(&Color_bright_green);
int loc_y = list_y + y + font_h / 2 - 1;
gr_circle(list_x + TIME_X - 6, loc_y, 5, GR_RESIZE_MENU);
gr_set_color_fast(&Color_bright);
gr_line(list_x + TIME_X - 10, loc_y, list_x + TIME_X - 8, loc_y, GR_RESIZE_MENU);
gr_line(list_x + TIME_X - 6, loc_y - 4, list_x + TIME_X - 6, loc_y - 2, GR_RESIZE_MENU);
gr_line(list_x + TIME_X - 4, loc_y, list_x + TIME_X - 2, loc_y, GR_RESIZE_MENU);
gr_line(list_x + TIME_X - 6, loc_y + 2, list_x + TIME_X - 6, loc_y + 4, GR_RESIZE_MENU);
}
y += font_h * (max_seg_line_offset + 1);
}
}