/* * 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_scrollback_vec; SCP_vector 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 *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(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); } }