Descent3/editor/HFile.cpp
2024-06-15 20:12:48 +02:00

822 lines
23 KiB
C++

/*
* Descent 3
* Copyright (C) 2024 Parallax Software
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
--- HISTORICAL COMMENTS FOLLOW ---
* $Logfile: /DescentIII/Main/editor/HFile.cpp $
* $Revision: 1.1.1.1 $
* $Date: 2003-08-26 03:57:38 $
* $Author: kevinb $
*
* Handler funcs for File menu
*
* $Log: not supported by cvs2svn $
*
* 81 10/13/99 2:19p Chris
* Added the ability to disable wind for powerups on a level
*
* 80 10/13/99 10:40a Chris
* Added 'special forcefields' that have custom bounce factors
*
* 79 10/08/99 4:29p Chris
* Added the forcefield and glass breaking override options
*
* 78 5/18/99 11:10a Matt
* Added variable ceiling height.
*
* 77 4/27/99 3:24p Jeff
* showlevelstats shows information about goals and special flags
*
* 76 4/21/99 1:08p Chris
* Fixed new mines from having the always check ceiling flag
*
* 75 4/20/99 8:14p Chris
* Added support for object's that hit the ceiling and for making the
* level always check for the ceiling (inside and outside the mine)
*
* 74 4/18/99 5:42a Chris
* Added the FQ_IGNORE_RENDER_THROUGH_PORTALS flag
*
* 73 3/22/99 6:26p Matt
* Cleaned up error handling in cfile and editor level loads.
*
* 72 3/02/99 4:54p Matt
* Fixed another stupid bug
*
* 71 3/02/99 4:07p Matt
* Fixed stupid bug in duplicate room name check
*
* 70 3/01/99 10:39a Matt
* Strip leading and trailing spaces from object, trigger, and room names
* on level load and when the names are entered.
*
* 69 2/28/99 10:41p Matt
* After a level is loaded, check for duplicate trigger, room, & object
* names.
*
* 68 2/28/99 9:28p Matt
* Cleaned up load level error handing and version checking.
*
* 67 2/21/99 12:28p Matt
* Added terrain sound system
*
* 66 2/17/99 3:06p Matt
* Updated copyrights.
*
* 65 2/16/99 3:47p Jason
* added marker updates to multiplayer server stream
*
* 64 2/02/99 10:46a Matt
* Only reset the wireframe view radius when loading an actual new level,
* and not whenever a level is loading, which incudes going from the game
* to the editor.
*
* 63 1/29/99 12:48p Matt
* Rewrote the doorway system
*
* 62 1/21/99 11:15p Jeff
* pulled out some structs and defines from header files and moved them
* into seperate header files so that multiplayer dlls don't require major
* game headers, just those new headers. Side effect is a shorter build
* time. Also cleaned up some header file #includes that weren't needed.
* This affected polymodel.h, object.h, player.h, vecmat.h, room.h,
* manage.h and multi.h
*
* 61 1/08/99 2:56p Samir
* Ripped out OSIRIS1.
*
* 60 1/04/99 6:19p Jason
* counts number of specular faces in mine
*
* 59 12/18/98 5:40p Chris
* Ambient life is now all sequenced up
*
* 58 10/08/98 8:14p Matt
* Fixed stupid bug that's just started causing problems.
*
* 57 10/08/98 4:23p Kevin
* Changed code to comply with memory library usage. Always use mem_malloc
* , mem_free and mem_strdup
*
* 56 10/05/98 2:42a Chris
* Level goals are relatively functional
*
* 55 9/24/98 5:00p Matt
* Improved error checking for running out of rooms.
*
* 54 9/16/98 12:07p Chris
* Improved BOA AABB computation with room checksums
*
* 53 9/10/98 3:07p Chris
* Improved matcen sequencing code and test effect1
*
* 52 9/08/98 12:05p Jason
* moved doorway.h out of room.h
*
* 51 8/17/98 2:16p Jason
* fixed erroneous reporting of specular lightmap bytes
*
* 50 8/10/98 11:25a Jason
* added better specular lighting
*
* 49 8/03/98 5:47p Chris
* Improved the partial AABB computation
*
* 48 5/15/98 5:41p Jason
* implemented volume lighting system
*
* 47 5/13/98 3:06p Jason
* got rid of square-power-of-two lightmap limitations
*
* 46 5/13/98 12:06p Jason
* trimmed some memory usage
*
* 45 4/21/98 2:40p Matt
* Added option to show level stats
*
* 44 4/14/98 7:50p Matt
* Added system to keep info for each level
*
* 43 2/18/98 8:43p Chris
* Made BOA get saved with the level. It should only get built when it
* needs to be updated.
*
* 42 2/04/98 1:26p Matt
* Got rid of compile warning
*
* 41 2/03/98 5:01p Chris
* UPdated the path system
*
* 40 1/27/98 12:01p Chris
* Game path system now works with new, load, and save in the editor.
* Also, the game path system is further bug proved.
*
* 39 1/21/98 12:32p Matt
* Revamped viewer system
*
* 38 1/20/98 1:36p Matt
* Moved mine origin to the center of the terrain
*
* 37 1/19/98 10:04a Matt
* Added new object handle system
*
* 36 1/15/98 7:34p Matt
* Revamped error checking when computing face normals
*
* 35 1/02/98 12:00p Jason
* got rid of redundant rebuilding of min/max terrain
*
* 34 10/08/97 5:20p Jason
* clear object memory when "New" is selected
*
* 33 10/01/97 7:49p Matt
* Fixed bug w/ script memory freeing (as per Samir's instructions)
*
* 32 10/01/97 4:49p Samir
* Free level script after loading it, since the editor doesn't need the
* compiled program.
*
* 31 9/25/97 5:28p Samir
* Even newer script code due to more changes in ObjCScript.cpp
*
* 30 9/24/97 2:57p Samir
* Modified NewScript for level function.
*
* 29 9/22/97 5:59p Samir
* Changed ObjCScript system, so everything is broken, but it shouldn't
* crash the game.
*
* 28 9/17/97 1:24p Matt
* Ripped out segment code
*
* 27 9/17/97 12:53p Samir
* BIG SEGMENT RIPOUT
*
* 26 9/10/97 1:55p Samir
* Create a default level script for a new mine.
*
* 25 9/09/97 2:38p Samir
* Added include for string.h
*
* 24 9/09/97 1:04p Samir
* Added code to create new script and save level script name.
*
* 23 9/09/97 10:44a Matt
* Fixed a small bug, & ripped out some segment engine code
*
* 22 9/06/97 3:35p Matt
* Reset cur porral when room changes
* Ripped out some old segment engine code
*
* 21 8/29/97 5:21p Matt
* Made CreateNewMine() clear the room (not segment) selected list, and
* clear Placed_group
*
* 20 8/21/97 2:06p Matt
* Clear marked room when creating a new level
*
* 19 8/18/97 6:59p Matt
* Implemented Place Room/Attach room system
*
* 18 8/15/97 6:32p Samir
* Took out references to default/level scripts due to newer system.
*
* 17 8/12/97 3:22p Samir
* D3XReallocProgram now has two more args.
*
* 16 8/06/97 10:36a Samir
* Fixed D3XReallocProgram calls.
*
* 15 8/04/97 4:12p Samir
* Reset level script code every new mine.
*
* 14 7/24/97 2:58p Matt
* Reset editor viewer variables when creating a new mine
*
* 13 7/22/97 7:07p Matt
* Cleaned up D3EditState, moving some vars in and some out, and renaming
* and changing a few others
*
* 12 7/18/97 5:42p Matt
* Changed default level script to use CR/LF pair instead of Newline
*
* 11 7/17/97 7:22p Matt
* Initialize level script when create new level
*
* 26 6/30/97 1:30p Jason
* added netherspace stuff
*
* 25 6/24/97 3:50p Jason
* changes for y only terrain positions
*
* 24 5/13/97 10:29p Matt
* Deleted some unused code
*
* 23 5/13/97 12:01p Matt
* Added support for floating segments
*
* 22 5/08/97 7:29p Matt
* Made seperate viewers for mine & terrain, and cleaned up code the
* switched between modes
*
* 21 5/06/97 2:30p Jason
* added check for terrain viewer
*
* 20 4/25/97 2:31p Jason
* implemented game event system
*
* 19 4/04/97 2:57p Matt
* Added code to initialize all the type-specific data for an object from
* the page for that object type. This means that we need to pass less
* info to ObjCreate(), and that we save less info in the level save file.
* It also makes it easy to reset all the objects when an object page has
* changed.
*
* 18 4/01/97 11:00p Matt
* Changed editor to keep a viewer object (type camera) seperate from the
* player object. This camera, and not the player, is now moved by
* slewing, the C key, etc. When going into the game, the viewer position
* & orientation are copied to the player. When going back to the editor,
* the player position is copied to the viewer, and the player object is
* reset to its start location.
*
* 17 3/31/97 5:58p Matt
* Revamped mine update flags
*
* 16 3/31/97 3:47p Matt
* Added code to keep track of, render, and display current vertex.
*
* 15 3/21/97 5:01p Jason
* incremental terrain improvments
*
*
* 14 3/18/97 11:27a Samir
* Reset current trigger and doorway to 0 when loading a level in.
*
* 13 3/17/97 2:54p Samir
* Dont initialize initial doorways or triggers.
*
* 12 3/17/97 12:00p Matt
* Reset warning segments when creating new level and loading
* level.
*
* 11 3/12/97 1:14p Matt
* Clear selected list when creating a new mine
*
* 10 3/12/97 12:37p Samir
* Initialized doorways
*
* 9 3/05/97 3:33p Samir
* Define initial default Doorway for mine.
*
* 8 3/04/97 7:12p Samir
* Initialized a default trigger when new level
*
* 7 2/26/97 12:23p Matt
* Use real funcs to create segment sides & assign uvs.
* Deleted temp func that did the same.
*
* 6 2/24/97 6:00p Matt
* Added code to set marked segment.
* Changed code to not say that mine has changed when curside or curside
* has changed.
*
* 5 2/21/97 5:23p Matt
* Added new ResetVertices() to set Vertex_active array after level
* loaded. Renamed old ResetVertices() to InitVertices();
*
* 4 2/20/97 9:56a Matt
* Moved some general-purpose segment code from HFile.cpp to ESegments.cpp
*
* 3 2/11/97 6:44p Matt
* Reset player when do a new level
* Added new function: ResetFreeSegList()
*
* 2 2/10/97 5:40p Matt
* Code to handle several File menu options
*
* 1 2/10/97 12:16a Matt
*
* $NoKeywords: $
*/
#include <string.h>
#include "descent.h"
#include "HFile.h"
#include "d3edit.h"
#include "room.h"
#include "object.h"
#include "game.h"
#include "LoadLevel.h"
#include "MoveWorld.h"
#include "ERooms.h"
#include "trigger.h"
#include "door.h"
#include "selectedroom.h"
#include "terrain.h"
#include "player.h"
#include "gameevent.h"
#include "HView.h"
#include "gamepath.h"
#include "boa.h"
#include "mission.h"
#include "lighting.h"
#include "lightmap_info.h"
#include "lightmap.h"
#include "special_face.h"
#include "doorway.h"
#include "matcen.h"
#include "levelgoal.h"
#include "aiambient.h"
#include "polymodel.h"
#include "bnode.h"
#include "findintersection.h"
// Scripting stuff
// #include "d3x.h"
// #include "ObjScript.h"
// vertices for the default room created by CreateNewMine()
vector default_room_verts[] = {{-10, 8, 20}, {-5, 10, 20}, {5, 10, 20}, {10, 8, 20}, {10, -8, 20}, {5, -10, 20},
{-5, -10, 20}, {-10, -8, 20}, {-10, 8, -20}, {-5, 10, -20}, {5, 10, -20}, {10, 8, -20},
{10, -8, -20}, {5, -10, -20}, {-5, -10, -20}, {-10, -8, -20}};
extern void AssignDefaultUVsToRoom(room *rp);
// Where the center of the (mine's) world is
vector Mine_origin = {TERRAIN_WIDTH * (TERRAIN_SIZE / 2), -100, TERRAIN_DEPTH *(TERRAIN_SIZE / 2)};
// Create a default room for a new mine
room *CreateDefaultRoom() {
room *rp;
int i;
// Get a pointer to our room
rp = CreateNewRoom(16, 10, 0); // 16 verts, 10 faces, normal room
ASSERT(rp != NULL);
// Set the vertices for the room
for (i = 0; i < 16; i++)
rp->verts[i] = default_room_verts[i] + Mine_origin;
// Set the faces for the room
InitRoomFace(&rp->faces[0], 8);
for (i = 0; i < 8; i++)
rp->faces[0].face_verts[i] = i;
InitRoomFace(&rp->faces[1], 8);
for (i = 0; i < 8; i++)
rp->faces[1].face_verts[i] = 15 - i;
for (i = 0; i < 8; i++) {
InitRoomFace(&rp->faces[i + 2], 4);
rp->faces[i + 2].face_verts[0] = i;
rp->faces[i + 2].face_verts[1] = i + 8;
rp->faces[i + 2].face_verts[2] = ((i + 1) % 8) + 8;
rp->faces[i + 2].face_verts[3] = (i + 1) % 8;
}
// Set normals, textures and UVLs for face
for (i = 0; i < 10; i++) {
if (!ComputeFaceNormal(rp, i))
Int3(); // this is odd. Get Matt!
rp->faces[i].tmap = i + 1;
AssignDefaultUVsToRoomFace(rp, i);
}
return rp;
}
#define DEFAULT_SCRIPT "//Empty script\xd\xa"
// Create a new mine with one segment
void CreateNewMine() {
// Get rid of old mine
FreeAllRooms();
FreeAllObjects();
// Create a new room
Curroomp = CreateDefaultRoom();
ASSERT(Curroomp != NULL);
// Reset misc. vars
Curface = Curedge = Curvert = 0;
Curportal = -1;
// Say that this is a new mine
New_mine = 1;
// Reset the view position, etc. for the wireframe view
ResetWireframeView();
// Reintialize the objects
ResetObjectList();
// Create the player and put him in the center of the fa
CreatePlayerObject(ROOMNUM(Curroomp));
// Initialize editor viewer
Editor_view_mode = VM_MINE;
Editor_viewer_id = -1;
// Create a camera for this level
SetEditorViewer();
// Look for player objects & set player starts
FindPlayerStarts();
// Clear the marked room
Markedroomp = NULL;
// Clear the selected segments
ClearRoomSelectedList();
// Clear the placed room & group
Placed_room = -1;
Placed_group = NULL;
// Reset the triggers
Num_triggers = 0;
Current_trigger = -1;
// Reset the terrain
ResetTerrain(1);
// Reset terrain sound
ClearTerrainSound();
// Clear game events
ClearAllEvents();
int i;
BOA_AABB_checksum = BOA_mine_checksum = 0;
for (i = 0; i < MAX_ROOMS; i++) {
BOA_AABB_ROOM_checksum[i] = 0;
}
InitGamePaths();
DestroyAllMatcens();
// Resets the ambient life controller
a_life.ALReset();
Level_goals.CleanupAfterLevel();
BNode_ClearBNodeInfo();
FVI_always_check_ceiling = false;
Ceiling_height = MAX_TERRAIN_HEIGHT;
sound_override_force_field = -1;
sound_override_glass_breaking = -1;
for (i = 0; i < MAX_FORCE_FIELD_BOUNCE_TEXTURES; i++) {
force_field_bounce_texture[i] = -1;
force_field_bounce_multiplier[i] = 1.0f;
}
Level_powerups_ignore_wind = false;
// Init level info
strcpy(Level_info.name, "Unnamed");
strcpy(Level_info.designer, "Anonymous");
strcpy(Level_info.copyright, "Copyright (c) 1999 Outrage Entertainment, Inc.");
strcpy(Level_info.notes, "");
}
#include "osiris_predefs.h"
// Strips leading and trailing spaces from a string
// Returns true if spaces were stripped
bool StripLeadingTrailingSpaces(char *s) {
char *t;
bool stripped = 0;
// Look for leading spaces
t = s;
while (*t == ' ')
t++;
// Leading spaces found, so copy string over spaces
if (t != s) {
strcpy(s, t);
stripped = 1;
}
// Strip any trailing spaces
for (t = s + strlen(s) - 1; (t >= s) && (*t == ' '); t--) {
*t = 0;
stripped = 1;
}
return stripped;
}
// Check for duplicate names in the level
void CheckLevelNames() {
int i;
// Check objects
object *objp;
for (i = 0, objp = Objects; i <= Highest_object_index; i++, objp++) {
if ((objp->type != OBJ_NONE) && objp->name) {
int handle = osipf_FindObjectName(objp->name);
if (handle != objp->handle)
EditorMessageBox("Error: Objects %d and %d are both named \"%s\"", i, OBJNUM(ObjGet(handle)), objp->name);
if (StripLeadingTrailingSpaces(objp->name))
EditorMessageBox(
"Note: Object %d (\"%s\") had leading and/or trailing spaces in its name that have been removed.", i,
objp->name);
}
}
// Check triggers
trigger *tp;
for (i = 0, tp = Triggers; i < Num_triggers; i++, tp++) {
if (tp->name) {
int n = osipf_FindTriggerName(tp->name);
if (n != i)
EditorMessageBox("Error: Triggers %d and %d are both named \"%s\"", i, n, tp->name);
if (StripLeadingTrailingSpaces(tp->name))
EditorMessageBox(
"Note: Trigger %d (\"%s\") had leading and/or trailing spaces in its name that have been removed.", i,
tp->name);
}
}
// Check rooms
room *rp;
for (i = 0, rp = Rooms; i <= Highest_room_index; i++, rp++) {
if (rp->used && rp->name) {
int n = osipf_FindRoomName(rp->name);
if (n != i)
EditorMessageBox("Error: Rooms %d and %d are both named \"%s\"", i, n, rp->name);
if (StripLeadingTrailingSpaces(rp->name))
EditorMessageBox(
"Note: Room %d (\"%s\") had leading and/or trailing spaces in its name that have been removed.", i,
rp->name);
}
}
}
// Load a new level
bool EditorLoadLevel(char *filename) {
if (LoadLevel(filename)) {
// Check for duplicate names in the level
CheckLevelNames();
New_mine = 1; // say that this is a new mine
// Set viewer object
SetEditorViewer();
// Reset view radius
ResetWireframeViewRad();
// Done
return TRUE;
} else
return FALSE;
}
// Save the current level
// Returns 1 if level saved sucessfully
int EditorSaveLevel(char *filename) {
int sucess;
sucess = SaveLevel(filename);
if (sucess)
Mine_changed = 0;
return sucess;
}
// Returns a pointer to enough spaces to right-justify a number that's printed after the spaces
char *IntSpacing(int i) {
static char *spaces = " ";
i = abs(i);
for (int n = 1; i >= 10; n++)
i /= 10;
ASSERT(n <= 20);
return spaces + n * 2 + n / 2;
}
extern void dump_text_to_clipboard(char *text);
// Shows the stats for a level in a messagebox, and copies to the clipboard
void ShowLevelStats() {
int n_rooms, n_rooms_external, n_faces, n_verts, n_objects, n_portals, n_doors, n_objects_outside;
int n_object_faces, n_object_lightmap_faces;
int i;
room *rp;
int bytes_wasted = 0, spec_faces = 0, lm_bytes = 0;
int total_volume_bytes = 0;
int num_redgoals = 0, num_bluegoals = 0, num_greengoals = 0, num_yellowgoals = 0;
int num_sp1 = 0, num_sp2 = 0, num_sp3 = 0, num_sp4 = 0, num_sp5 = 0, num_sp6 = 0;
uint8_t lightmaps_used[MAX_LIGHTMAPS];
object *objp;
#define BUF_LEN 5000
char text_buf[BUF_LEN];
// Count the number of rooms, and the number of faces & points per room
n_rooms = n_rooms_external = n_faces = n_verts = n_doors = n_portals = 0;
for (i = 0, rp = Rooms; i <= Highest_room_index; i++, rp++)
if (rp->used) {
n_rooms++;
n_verts += rp->num_verts;
n_faces += rp->num_faces;
n_portals += rp->num_portals;
if (rp->flags & RF_EXTERNAL)
n_rooms_external++;
else {
// Get volume size of room
total_volume_bytes += GetVolumeSizeOfRoom(rp);
}
for (int t = 0; t < rp->num_faces; t++) {
face *fp = &rp->faces[t];
if (fp->special_handle != BAD_SPECIAL_FACE_INDEX && GameTextures[fp->tmap].flags & TF_SPECULAR &&
fp->lmi_handle != BAD_LMI_INDEX)
spec_faces++;
}
if (rp->flags & RF_DOOR)
n_doors++;
if (rp->flags & RF_SPECIAL1)
num_sp1++;
if (rp->flags & RF_SPECIAL2)
num_sp2++;
if (rp->flags & RF_SPECIAL3)
num_sp3++;
if (rp->flags & RF_SPECIAL4)
num_sp4++;
if (rp->flags & RF_SPECIAL5)
num_sp5++;
if (rp->flags & RF_SPECIAL6)
num_sp6++;
if (rp->flags & RF_GOAL1)
num_redgoals++;
if (rp->flags & RF_GOAL2)
num_bluegoals++;
if (rp->flags & RF_GOAL3)
num_greengoals++;
if (rp->flags & RF_GOAL4)
num_yellowgoals++;
}
// Count the number of objects
n_objects = n_object_faces = n_object_lightmap_faces = n_objects_outside = 0;
for (i = 0, objp = Objects; i <= Highest_object_index; i++, objp++)
if ((objp->type != OBJ_NONE) && (objp->type != OBJ_ROOM) && (objp->render_type == RT_POLYOBJ)) {
n_objects++;
if (OBJECT_OUTSIDE(objp))
n_objects_outside++;
// Count the number of faces in this object
poly_model *pm;
pm = GetPolymodelPointer(objp->rtype.pobj_info.model_num);
for (int m = 0; m < pm->n_models; m++) {
n_object_faces += pm->submodel[m].num_faces;
if (objp->lighting_render_type == LRT_LIGHTMAPS)
n_object_lightmap_faces += pm->submodel[m].num_faces;
}
}
memset(lightmaps_used, 0, MAX_LIGHTMAPS);
for (i = 0; i < MAX_LIGHTMAP_INFOS; i++) {
if (!LightmapInfo[i].used)
continue;
if (LightmapInfo[i].type == LMI_DYNAMIC || LightmapInfo[i].type == LMI_TERRAIN)
continue;
lightmaps_used[LightmapInfo[i].lm_handle] = 1;
}
for (i = 0; i < MAX_LIGHTMAPS; i++) {
if (lightmaps_used[i]) {
uint16_t *data = lm_data(i);
int w = lm_w(i);
int h = lm_h(i);
for (int j = 0; j < w * h; j++) {
if (!(data[j] & OPAQUE_FLAG)) {
bytes_wasted += 2;
} else {
lm_bytes += 2;
}
}
}
}
i = sprintf(text_buf,
"Level Stats:\n"
"\n"
"%s%d Rooms (%d external)\n"
"%s%d Faces\n"
"%s%d Vertices\n"
"\n"
"%s%d Portals\n"
"%s%d Doors\n"
"\n"
"%s%d Polygon Objects (%d inside, %d outside)\n"
"%s%d Object Faces (%d with lightmaps)\n"
"\n"
"%s%d Total lightmap faces\n"
"%d Total volume bytes\n"
"%d Total bytes in lightmaps\n"
"%d Total specular faces\n"
"%d Bytes wasted in lightmaps\n"
"\n"
"%d Red Goals\n"
"%d Blue Goals\n"
"%d Green Goals\n"
"%d Yellow Goals\n"
"%d Special 1 Rooms\n"
"%d Special 2 Rooms\n"
"%d Special 3 Rooms\n"
"%d Special 4 Rooms\n"
"%d Special 5 Rooms\n"
"%d Special 6 Rooms\n",
IntSpacing(n_rooms), n_rooms, n_rooms_external, IntSpacing(n_faces), n_faces, IntSpacing(n_verts),
n_verts, IntSpacing(n_portals / 2), n_portals / 2, IntSpacing(n_doors), n_doors, IntSpacing(n_objects),
n_objects, n_objects - n_objects_outside, n_objects_outside, IntSpacing(n_object_faces), n_object_faces,
n_object_lightmap_faces, IntSpacing(n_faces + n_object_lightmap_faces), n_faces + n_object_lightmap_faces,
total_volume_bytes, lm_bytes, spec_faces, bytes_wasted, num_redgoals, num_bluegoals, num_greengoals,
num_yellowgoals, num_sp1, num_sp2, num_sp3, num_sp4, num_sp5, num_sp6);
ASSERT(i < BUF_LEN);
dump_text_to_clipboard(text_buf);
OutrageMessageBox(
"%s\n\nThis info has also been posted to the clipboard.\n\nWhat other info would you like here? Let MattT know.",
text_buf);
}