/*
* 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/HRoom.cpp $
 * $Revision: 1.1.1.1 $
 * $Date: 2003-08-26 03:57:38 $
 * $Author: kevinb $
 *
 * Code to implement room functions
 *
 * $Log: not supported by cvs2svn $
 *
 * 106   9/15/99 1:56p Matt
 * Added the option to allow rooms or groups placed on the terrain to
 * either align with the terrain or with gravity.
 *
 * 105   9/07/99 12:10p Matt
 * Added a function to propagate a texture to all adjacent coplanar faces
 * in a room.
 *
 * 104   8/30/99 1:00p Gwar
 * use the "true" center of faces for determining attach points in room
 * connecting functions
 *
 * 103   8/19/99 6:57p Gwar
 * call ned_FindSharedEdge in CombineFaces to see if we can't combine
 * because of a flipped normal
 *
 * 102   8/17/99 7:11p Gwar
 *
 * 101   8/17/99 12:08p Gwar
 * don't set World_changed in functions that can be called from palette
 * rooms
 *
 * 100   8/12/99 12:08a Gwar
 * NEWEDITOR: texture, door and object memory management; and call SetPrim
 * when changing Curroomp, which now force unmarks everything in current
 * room view
 *
 * 99    7/04/99 4:55p Gwar
 * changes for texture management in NEWEDITOR
 *
 * 98    5/08/99 6:39p Matt
 * Added a function to delete a face and all faces connected to it.
 *
 * 97    5/04/99 12:32p Matt
 * Fixed stupid bug and stupider bug.
 *
 * 96    4/30/99 6:52p Matt
 * Added a function to merge an object's geometry into a room.
 *
 * 95    4/28/99 12:51a Matt
 * Added some funtions to edit face geometry.
 *
 * 94    4/25/99 2:38p Gwar
 * brought in HRoom.cpp and RoomUVs.cpp, and several misc game functions
 * were added to globals.cpp to make it possible
 *
 * 93    4/19/99 12:10a Matt
 * Added a menu item to delete a vertex from a face.
 *
 * 92    4/18/99 11:26p Matt
 * Restored point-to-plane check to FixCracks().  This check is necessary
 * because the point-to-edge check assumes that the points are all in the
 * same plane, and will not work if they are not.
 *
 * 91    4/07/99 4:39p Matt
 * In link to new external room, copy the textures from the old faces to
 * the new faces.
 *
 * 90    4/07/99 1:50p Matt
 * Took point-in-plane check out of fix level cracks, because it prevented
 * tjoints from being fixed for some non-planar faces.
 *
 * 89    4/06/99 3:48p Matt
 * Allow combining of rooms with rendered portals.
 *
 * 88    4/06/99 10:23a Matt
 *
 * 87    4/01/99 2:55p Matt
 * Fixed triangulate face code to deal with colinear points
 *
 * 86    4/01/99 1:25p Matt
 * Fixed bug in MatchPortalFaces when checking for match and a colinear
 * point was found.
 *
 * 85    3/31/99 12:58p Matt
 * Added snap-point-to-face
 *
 * 84    3/30/99 8:03p Matt
 * Deleted commented-out code
 *
 * 83    3/30/99 8:01p Matt
 * Took out normals-are-same check when combining adjacent faces.  It was
 * the wrong way to check the faces (the right way (in the D3 editor, that
 * is) is to check for point in the plane) and we were only using it as a
 * first-pass check anyway.
 *
 * 82    3/30/99 6:25p Matt
 * Use new error system for combine faces function.
 *
 * 81    3/29/99 6:46p Matt
 * When combining faces, use FaceIsPlanar() so the face will pass mine
 * verification.
 *
 * 80    3/27/99 5:01p Matt
 * Fixed stupid bug in build bridge code, that assigned UVs to all faces
 * instead of one face, causing an Int3 sometimes for faces that hadn't
 * yet had normals computed.
 *
 * 79    3/27/99 4:45p Matt
 * Fixed a surprising fundamental flaw in the polygon clip code.
 *
 * 78    3/23/99 5:12p Matt
 * Added function to combine rooms.
 *
 * 77    2/27/99 3:47p Mark
 * Place room on terrain was improperly clearing out the placed door.
 * (MattT on Mark's machine)
 *
 * 76    1/29/99 12:48p Matt
 * Rewrote the doorway system
 *
 * 75    1/21/99 11:34a Matt
 * Got rid of portal triggers.  Since we don't have multi-face portals, a
 * face trigger works fine for a portal.  Also fixed a few editor/trigger
 * bugs.
 *
 * 74    12/23/98 6:32p Matt
 * Changed, slightly, the process of creating a new face
 *
 * 73    12/23/98 10:53a Matt
 * Added functions to create a face
 *
 * 72    10/07/98 12:48p Matt
 * Added some extra error checking to prevent doubly-formed portals.
 *
 * 71    10/03/98 8:31p Matt
 * Added Join Rooms Exact function.
 *
 * 70    9/25/98 5:25p Matt
 * Don't try to fix cracks in unused rooms.
 *
 * 69    9/24/98 5:00p Matt
 * Improved error checking for running out of rooms.
 *
 * 68    9/16/98 10:02a Matt
 * Fixed serval bugs, in JoinAllOverlappingFaces() and MatchPortalFaces().
 *
 * 67    9/11/98 4:46p Matt
 * Don't allow combining of portal faces, pending code to do the combine
 * correctly.
 *
 * 66    9/10/98 6:38p Matt
 * Deal with error joining faces after smooth bridge.
 *
 * 65    9/09/98 4:07p Matt
 * Added Smooth Bridge function
 *
 * 64    9/08/98 3:25p Matt
 * Changed an error message.
 *
 * 63    9/08/98 12:05p Jason
 * moved doorway.h out of room.h
 *
 * 62    9/07/98 10:58p Matt
 * Added snap point to point.
 *
 * 61    9/07/98 8:14p Matt
 * Don't delete colinear points (& thus form t-joints) when combining
 * faces, and deal properly with combining faces that share multiple
 * edges.
 *
 * 60    9/04/98 3:34p Matt
 * Added groovy vertex snap code
 *
 * 59    9/04/98 12:29p Matt
 * Added marked edge & vertex in the editor
 *
 * 58    9/04/98 11:34a Matt
 * Fixed small bug in portal matching code, that caused one point on the
 * portals not to be set exactly the same.
 *
 * 57    9/03/98 5:29p Matt
 * Added code to fix cracks in levels.
 *
 * 56    9/02/98 5:57p Matt
 * Added code to make faces match exactly when forming a portal.  This
 * should get rid of all remaining cracking problems.
 *
 * 55    9/02/98 4:07p Matt
 * Fixed one kind of cracking:  after clip, make sure old points that
 * more-or-less line up are exactly the same.
 *
 * 54    9/02/98 2:29p Matt
 * Added code to prevent t-joints from being formed when joining rooms
 *
 * 53    9/01/98 12:04p Matt
 * Ripped out multi-face portal code
 *
 * 52    8/28/98 6:21p Matt
 * Added a function to flip a face.
 *
 * 51    8/24/98 3:02p Matt
 * Gave the user the option, when joining rooms, of extruding straight out
 * or toward the center of the base face.
 *
 * 50    7/01/98 7:53p Matt
 * Added code to allow forcing of join even when the two rooms stick
 * through each other.
 *
 * 49    6/25/98 7:15p Matt
 * Added a function to delete a pair of portals.
 *
 * 48    6/23/98 2:45p Matt
 * Added option when propagating to all faces in a room to only do so to
 * ones with the same texture.
 *
 * 47    6/08/98 12:28p Matt
 * Added a function to triangulate a face
 *
 * 46    5/22/98 4:47p Matt
 * Added menu item to propagate a texture to all the faces in a room.
 *
 * 45    4/27/98 6:41p Matt
 * Added code to join all adjacent faces between two rooms
 *
 * 44    4/17/98 6:25p Matt
 * In CheckFaceToPlane(), store the distance in a variable so it can be
 * looked at in the debugger.
 *
 * 43    4/17/98 6:23p Matt
 * Added code to make sure that the two faces of a portal have the same
 * number of vertices, but commented it out for now.  See the note in the
 * code.
 *
 * 42    4/16/98 11:50a Matt
 * Made snap code work for placed groups.
 *
 * 41    4/07/98 4:19p Matt
 * Fixed (I hope) a problem joining faces that matched perfectly.
 *
 * 40    4/02/98 12:23p Jason
 * trimmed some fat from our structures
 *
 * 39    4/01/98 5:55p Matt
 * Partially re-wrote JoinRooms() to allow creation of adjacent portals.
 *
 * 38    2/18/98 2:26a Matt
 * Fixed typo
 *
 * 37    2/16/98 2:53p Matt
 * Deal with small floating-point precision problem.
 *
 * 36    2/16/98 1:27p Matt
 * Added function to snap the placed room to a vertex/edge on the base
 * room/face
 *
 * 35    2/11/98 12:38p Matt
 * Added epsilon to point-to-plane check
 *
 * 34    2/08/98 6:02p Matt
 * Use groovy new vm_MatrixMulTMatrix() function
 *
 * 33    2/04/98 6:23p Matt
 * Changed object room number to indicate a terrain cell via a flag.  Got
 * rid of the object flag which used to indicate terrain.
 *
 * 32    1/30/98 1:27p Matt
 * Orthogonalize after computing rotation matrix for placed rooms (fixed
 * probem of bad matrix making rooms bigger).
 *
 * 31    1/21/98 12:40p Matt
 * When attaching a room to the ground, delete the room's attach face
 *
 * 30    1/21/98 12:32p Matt
 * Revamped viewer system
 *
 * 29    1/20/98 4:43p Matt
 * Fixed up the bridge and join functions
 *
 * 28    1/15/98 7:34p Matt
 * Revamped error checking when computing face normals
 *
 * 27    11/17/97 7:39p Sean
 * Changed join epsilon, and took out a return from a bogus error message
 * (Matt on Sean's machine)
 *
 * 26    11/06/97 6:47p Sean
 * FROM JASON: Changed <Highest_room_index to <=
 *
 * 25    11/05/97 7:13p Matt
 * Added join rooms function
 *
 * 24    10/03/97 3:38p Matt
 * Added place and attach code for rooms on the terrain
 *
 * 23    10/01/97 7:51p Matt
 * Added code for external rooms
 *
 * 22    9/29/97 12:09p Jason
 * added functionality to doorway system
 *
 * 21    9/26/97 6:01p Matt
 * Fixed delete room code so that Curroomp is never NULL
 *
 * 20    9/24/97 3:22p Matt
 * Added Drop Room function
 *
 * 19    9/15/97 5:56p Matt
 * Changed message (& fixed handling) when user tries to delete a room
 * attached to the terrain.
 *
 * 18    9/12/97 9:01p Craig
 * Fixed orientation of door object (Matt on Craig's machine)
 *
 * 17    9/12/97 5:38p Jason
 * got doors working
 *
 * 16    9/11/97 5:38p Jason
 * initial door coding for room engine
 *
 * 15    9/06/97 3:18p Matt
 * Use LinkRoomsSimple() when adding, attaching, & bridging
 * Reset Curportal when switch rooms
 *
 * 14    9/04/97 4:39p Matt
 * Added includes needed as a result of removing includes from d3edit.h
 *
 * 13    9/02/97 2:26p Matt
 * Made ComputePlacedRoomMatrix() deal w/ placed rooms or groups
 * Made polygon clipper deal with points on the the edge of the clip
 * polygon
 *
 * 12    8/29/97 5:43p Matt
 * Moved some funcs to erooms.cpp, and fixed some stuff for selected rooms
 *
 * 11    8/27/97 10:30a Matt
 * Fixed bogosity assigning UVs to faces in add room
 *
 * 10    8/22/97 9:30a Matt
 * When adding a room, make the new room the current room, and make the
 * face opposite the connection the current one.  That way you can add
 * several times in a row to create a long tunnel.
 *
 * 9     8/21/97 6:01p Matt
 * Added combine faces & delete room functions
 *
 * 8     8/19/97 1:01p Matt
 * Added error check for facing not overlapping when attaching rooms
 *
 * 7     8/18/97 6:59p Matt
 * Implemented Place Room/Attach room system
 *
 * 6     8/05/97 10:47a Matt
 * Added some error checking, and fixed a bug
 *
 * 5     8/04/97 7:38p Matt
 * Don't add a room if there's already a room at the face
 *
 * 4     8/04/97 12:46p Matt
 * Added BuildBridge() and SetMarkedRoom()
 *
 *
 * 3     8/01/97 6:15p Matt
 * Added code to attach rooms
 *
 * 2     7/22/97 10:33a Matt
 * Added AddRoom()
 *
 * 1     7/21/97 4:48p Matt
 *
 * $NoKeywords: $
 */

#include <stdlib.h>

#include "HRoom.h"

#ifndef NEWEDITOR
#include "d3edit.h"
#else
#include "..\neweditor\globals.h"
#include "..\neweditor\ned_geometry.h"
#endif

#include "erooms.h"
#include "RoomUVs.h"
#include "HView.h"
#include "SelectedRoom.h"
#include "group.h"
#include "door.h"
#include "doorway.h"
#include "terrain.h"
#include "HTexture.h"
#include "trigger.h"

// Make the Marked room/face the current room/face
void SetMarkedRoom() {
  Markedroomp = Curroomp;
  Markedface = Curface;
  Markededge = Curedge;
  Markedvert = Curvert;

  State_changed = 1;

  EditorStatus("Marked room:face set to %d:%d", ROOMNUM(Markedroomp), Markedface);
}

// Select next face on current room
void SelectNextFace() {
  if (++Curface >= Curroomp->num_faces)
    Curface = 0;
  State_changed = 1;
}

// Select previous face on current room
void SelectPrevFace() {
  if (--Curface < 0)
    Curface = Curroomp->num_faces - 1;
  State_changed = 1;
}

#define DEFAULT_ROOM_LENGTH 20.0

// Adds a room at the current room/face.  The room is created by extuding out from the current face
void AddRoom() {
  room *rp;
  face *cfp;
  int cnv, nfaces;
  vector room_delta;
  int i;

  // Get values from current room & face
  cfp = &Curroomp->faces[Curface]; // pointer to current face
  cnv = cfp->num_verts;            // number of verts in current face

  // Check for portal already here
  if (cfp->portal_num != -1) {
    OutrageMessageBox("Can't add room: There is already a connection at the current room:face.");
    return;
  }

  // Get values for new room
  nfaces = cnv + 2;

  // Get a pointer to our room
  rp = CreateNewRoom(cnv * 2, nfaces, 0);
  if (rp == NULL) {
    OutrageMessageBox("Cannot add room: No free rooms.");
    return;
  }

  // Compute delta vector
  room_delta = cfp->normal * -DEFAULT_ROOM_LENGTH;

  // Set the vertices for the room
  for (i = 0; i < cnv; i++) {
    rp->verts[i] = Curroomp->verts[cfp->face_verts[cnv - 1 - i]];
    rp->verts[cnv + i] = rp->verts[i] + room_delta;
  }

  // Set the faces for the room
  InitRoomFace(&rp->faces[0], cnv);
  for (i = 0; i < cnv; i++)
    rp->faces[0].face_verts[i] = i;

  InitRoomFace(&rp->faces[1], cnv);
  for (i = 0; i < cnv; i++)
    rp->faces[1].face_verts[i] = cnv * 2 - 1 - i;

  for (i = 0; i < nfaces - 2; i++) {
    InitRoomFace(&rp->faces[i + 2], 4);
    rp->faces[i + 2].face_verts[0] = i;
    rp->faces[i + 2].face_verts[1] = i + cnv;
    rp->faces[i + 2].face_verts[2] = ((i + 1) % cnv) + cnv;
    rp->faces[i + 2].face_verts[3] = (i + 1) % cnv;
  }

#ifndef NEWEDITOR
  // Set normals & textures for each face
  for (i = 0; i < nfaces; i++) {
    if (!ComputeFaceNormal(rp, i))
      Int3(); // Bad!  Get Matt!

    // Assign arbitrary texture map
    rp->faces[i].tmap = i + 1;
  }
#else
  int texnum = Editor_state.GetCurrentTexture();

  // Set normals & textures for each face
  for (i = 0; i < nfaces; i++) {
    if (!ComputeFaceNormal(rp, i))
      Int3(); // Bad!  Get Matt!

    // Apply texture
    HTextureApplyToRoomFace(rp, i, texnum);
  }
#endif

  // Set UVs for all the faces
  AssignDefaultUVsToRoom(rp);

  // Form the portals between the rooms
  LinkRoomsSimple(Rooms, ROOMNUM(Curroomp), Curface, ROOMNUM(rp), 0);

  // Make the new room the current room
#ifndef NEWEDITOR
  Curroomp = rp;
  Curface = 1;
  Curedge = Curvert = 0;
  Curportal = -1;
#else
  theApp.m_pLevelWnd->SetPrim(rp, 1, -1, 0, 0);
#endif

  // Set the flag
  World_changed = 1;
}

// Computes the orientation matrix for the placed room
void ComputePlacedRoomMatrix() {
  room *placedroomp;
  int placedface;
  matrix srcmat;
  vector t;

  if (Placed_room != -1) {
    placedroomp = &Rooms[Placed_room];
    placedface = Placed_room_face;
  } else {
    ASSERT(Placed_group != NULL);
    placedroomp = &Placed_group->rooms[Placed_group->attachroom];
    placedface = Placed_group->attachface;
  }

  // Compute source and destination matrices
  t = -placedroomp->faces[placedface].normal;
  vm_VectorToMatrix(&srcmat, &t, NULL, NULL);
  vm_VectorAngleToMatrix(&Placed_room_orient, &Placed_room_orient.fvec, Placed_room_angle);

  mprintf(0, "srcmat: %f %f %f\n", vm_GetMagnitude(&srcmat.fvec), vm_GetMagnitude(&srcmat.rvec),
          vm_GetMagnitude(&srcmat.uvec));
  mprintf(0, "orient: %f %f %f\n", vm_GetMagnitude(&Placed_room_orient.fvec), vm_GetMagnitude(&Placed_room_orient.rvec),
          vm_GetMagnitude(&Placed_room_orient.uvec));

  // Make sure the matrices are ok
  vm_Orthogonalize(&srcmat);
  vm_Orthogonalize(&Placed_room_orient);

  // Compute matrix to rotate src -> dest
  vm_MatrixMulTMatrix(&Placed_room_rotmat, &srcmat, &Placed_room_orient);

  mprintf(0, "rotmat: %f %f %f\n", vm_GetMagnitude(&Placed_room_rotmat.fvec), vm_GetMagnitude(&Placed_room_rotmat.rvec),
          vm_GetMagnitude(&Placed_room_rotmat.uvec));

  // Make sure the matrix is ok
  vm_Orthogonalize(&Placed_room_rotmat);
}

// Place a room for orientation before attachment
// Parameters:	baseroomp - pointer to the room in the mine to which the new room will be attached
//					baseface - the face on baseroomp to attach to
//					placed_room - the number of the room to be attached
//					placed_room_face the face on placed_room that's attached
void PlaceRoom(room *baseroomp, int baseface, int placed_room, int placed_room_face, int placed_room_door) {
  // Clear the placed group if one exists
  Placed_group = NULL;

  ASSERT(baseroomp->faces[baseface].portal_num == -1);

  room *placedroomp = &Rooms[placed_room];

  // Set globals
  Placed_room = placed_room;
  Placed_room_face = placed_room_face;
  Placed_room_orient.fvec = baseroomp->faces[baseface].normal;
  Placed_room_angle = 0;
  Placed_baseroomp = baseroomp;
  Placed_baseface = baseface;
  Placed_door = placed_room_door;

  // Compute attach points on each face
#ifndef NEWEDITOR
  ComputeCenterPointOnFace(&Placed_room_attachpoint, baseroomp, baseface);
  ComputeCenterPointOnFace(&Placed_room_origin, placedroomp, placed_room_face);
#else
  ComputeFaceBoundingCircle(&Placed_room_attachpoint, baseroomp, baseface);
  ComputeFaceBoundingCircle(&Placed_room_origin, placedroomp, placed_room_face);
#endif

  // Compute initial orientation matrix
  ComputePlacedRoomMatrix();

  // Set the flag
  State_changed = 1;
}

// Lined up a placed room.  Moves the placed room so the closest vert to basevert lines up exactly,
// and the edge from basevert to basevert+1 lines up with the corresponding edge on the placed room.
void SnapRoom(int basevert) {
  vector basecenter, attcenter;
  room *baseroomp, *attroomp;
  face *bfp, *afp;
  vector *bvp;
  int baseface, attface;
  vector baseedge, attedge;

  // Get values for the attach room
  if (Placed_room != -1) {
    attroomp = &Rooms[Placed_room];
    attface = Placed_room_face;
  } else {
    ASSERT(Placed_group != NULL);
    attroomp = &Placed_group->rooms[Placed_group->attachroom];
    attface = Placed_group->attachface;
  }

  // Set some vars
  baseroomp = Placed_baseroomp;
  baseface = Placed_baseface;
  attcenter = Placed_room_origin;
  basecenter = Placed_room_attachpoint;
  bfp = &baseroomp->faces[baseface];
  afp = &attroomp->faces[attface];
  bvp = &baseroomp->verts[bfp->face_verts[basevert]];

  // Find matching vert in attach face
  float closest_dist = FLT_MAX;
  int closest_v;
  for (int v = 0; v < afp->num_verts; v++) {
    vector rotvert;
    float dist;

    rotvert = ((attroomp->verts[afp->face_verts[v]] - attcenter) * Placed_room_rotmat) + basecenter;
    dist = vm_VectorDistance(bvp, &rotvert);

    if (dist < closest_dist) {
      closest_dist = dist;
      closest_v = v;
    }
  }

  // Compute edge vector in base room
  vm_GetNormalizedDir(&baseedge, &baseroomp->verts[bfp->face_verts[(basevert + 1) % bfp->num_verts]], bvp);

  // Compute edge vector in attach room
  vector v0 = ((attroomp->verts[afp->face_verts[closest_v]] - attcenter) * Placed_room_rotmat) + basecenter;
  vector v1 = ((attroomp->verts[afp->face_verts[(closest_v + afp->num_verts - 1) % afp->num_verts]] - attcenter) *
               Placed_room_rotmat) +
              basecenter;
  vm_GetNormalizedDir(&attedge, &v1, &v0);

  // Get angle between two edge vectors
  double dot = baseedge * attedge;
  if (dot > 1.0)
    dot = 1.0;
  double ac = acos(dot);
  float delta_ang = 32768.0 * ac / PI;

  // Get sign of angle
  vector checkv;
  vm_CrossProduct(&checkv, &baseedge, &attedge);
  if ((bfp->normal * checkv) > 0)
    delta_ang = -delta_ang;

  // Update placed room angle
  Placed_room_angle += delta_ang;
  while (Placed_room_angle < 0)
    Placed_room_angle += 65536;

  // Regenerate matrix
  ComputePlacedRoomMatrix();

  // Move the room to line up the points
  v0 = ((attroomp->verts[afp->face_verts[closest_v]] - attcenter) * Placed_room_rotmat) + basecenter;
  Placed_room_attachpoint += *bvp - v0;

  // Update view
  State_changed = 1;
}

// Attach an already-placed room
void AttachRoom() {
  vector basecenter, attcenter;
  room *baseroomp, *attroomp, *newroomp;
  int baseface, attface;

  ASSERT(Placed_room != -1);

  // Set some vars
  baseroomp = Placed_baseroomp;
  baseface = Placed_baseface;
  attroomp = &Rooms[Placed_room];
  attface = Placed_room_face;
  attcenter = Placed_room_origin;
  basecenter = Placed_room_attachpoint;

  // Get the new room
  newroomp = CreateNewRoom(attroomp->num_verts, attroomp->num_faces, 0);
  if (newroomp == NULL) {
    OutrageMessageBox("Cannot attach room: No free rooms.");
    return;
  }

  // Rotate verts, copying into new room
  for (int i = 0; i < attroomp->num_verts; i++)
    newroomp->verts[i] = ((attroomp->verts[i] - attcenter) * Placed_room_rotmat) + basecenter;

  // Copy faces to new room
  for (i = 0; i < attroomp->num_faces; i++) {
    CopyFace(&newroomp->faces[i], &attroomp->faces[i]);
#ifdef NEWEDITOR
    LevelTexIncrementTexture(attroomp->faces[i].tmap);
#endif
  }

  // Recompute normals for the faces
  if (!ResetRoomFaceNormals(newroomp))
    Int3(); // Get Matt

  // Copy other values for this room
  newroomp->flags = attroomp->flags;
  newroomp->num_portals = 0;

  // Check for terrain or mine room
  if (baseroomp == NULL) { // a terrain room

    // Flag this as an external room
    newroomp->flags |= RF_EXTERNAL;

    // Delete the attach face, which should now be facing the ground
    DeleteRoomFace(newroomp, attface);
  } else { // a mine room

    // Clip the connecting faces against each other
    if (!ClipFacePair(newroomp, attface, baseroomp, baseface)) {
      OutrageMessageBox("Error making portal -- faces probably don't overlap.");
      FreeRoom(newroomp);
      return;
    }

    // Make the two faces match exactly
    MatchPortalFaces(baseroomp, baseface, newroomp, attface);

    // Create the portals between the rooms
    LinkRoomsSimple(Rooms, ROOMNUM(baseroomp), baseface, ROOMNUM(newroomp), attface);

    // If there is a door, place it!
    if (Placed_door != -1) {
      vector room_center;
      matrix orient = ~Placed_room_rotmat;
      vector doorcenter = {0, 0, 0};

      FreeRoom(&Rooms[Placed_room]);

      room_center = ((doorcenter - attcenter) * Placed_room_rotmat) + basecenter;

      ObjCreate(OBJ_DOOR, Placed_door, newroomp - Rooms, &room_center, &orient);

      doorway *dp = DoorwayAdd(newroomp, Placed_door);

#ifdef NEWEDITOR
      LevelDoorIncrementDoorway(Placed_door);
#endif

      Placed_door = -1;
    }
  }

  // Un-place the now-attached room
  Placed_room = -1;

  // Flag the world as changed
  World_changed = 1;
}

// structure for keeping track of vertices inserted in edges (from clipping)
struct edge_insert {
  int v0, v1; // the edge that got the new vert
  int new_v;  // the new vertex
};

// List of vertices inserted in edges.  Used only during clipping.
edge_insert Edge_inserts[MAX_VERTS_PER_FACE];
int Num_edge_inserts;

// Add an insert to the edge list
void AddEdgeInsert(int v0, int v1, int new_v) {
  Edge_inserts[Num_edge_inserts].v0 = v0;
  Edge_inserts[Num_edge_inserts].v1 = v1;
  Edge_inserts[Num_edge_inserts].new_v = new_v;
  Num_edge_inserts++;
}

// Clip a polygon against one edge of another polygon
// Fills inbuf and maybe outbuf with new polygons, and writes any new verts to the vertices array
// Parameters:	nv - the number of verts in the polygon to be clipped
//					vertnums - pointer to list of vertex numbers in the polygon
//					vertices - list of vertices referred to in vertnums
//					v0,v1 - the edge we're clipping against
//					normal - the surface normal of the polygon
//					inbuf - the clipped polygon is written to this buffer
//					inv - the number of verts in inbuf is written here
//					outbuf - the new polygon created by the part of the input polygon that was
//clipped away 					onv - the number of verys in outbuf 					num_vertices - pointer to the number of verts in the vertices array
void ClipAgainstEdge(int nv, int16_t *vertnums, vertex *vertices, int *num_vertices, vector *v0, vector *v1,
                     vector *normal, int16_t *inbuf, int *inv, int16_t *outbuf, int *onv) {
  int i, prev, next, check;
  int16_t *ip = inbuf, *op = outbuf;
  vertex *curv, *prevv, *nextv;
  int inside_points = 0, outside_points = 0; // real inside/outside points, distinct from edge points

  for (i = 0, prev = nv - 1, next = 1; i < nv; i++) {

    curv = &vertices[vertnums[i]];

    // Find out where point lies
    check = CheckPointAgainstEdge(&curv->vec, v0, v1, normal);
    if (check == 0) { // Current vertex is on edge

      // Add to both inside & outside lists
      *op++ = vertnums[i];
      *ip++ = vertnums[i];
    } else if (check == -1) { // Current vertex is outside
      int check2;

      prevv = &vertices[vertnums[prev]];
      nextv = &vertices[vertnums[next]];

      // Clip edge w/ previous vertex
      check2 = CheckPointAgainstEdge(&prevv->vec, v0, v1, normal);
      if (check2 == 1) { // prev inside, so clip
        ClipEdge(normal, prevv, curv, v0, v1, &vertices[*num_vertices]);
        AddEdgeInsert(vertnums[prev], vertnums[i], *num_vertices);
        *op++ = *ip++ = (*num_vertices)++;
      }

      // Add current vertex to outside polygon
      *op++ = vertnums[i];
      outside_points++;

      // Clip edge w/ next vertex
      check2 = CheckPointAgainstEdge(&nextv->vec, v0, v1, normal);
      if (check2 == 1) { // next inside, so clip
        ClipEdge(normal, curv, nextv, v0, v1, &vertices[*num_vertices]);
        AddEdgeInsert(vertnums[i], vertnums[next], *num_vertices);
        *op++ = *ip++ = (*num_vertices)++;
      }
    } else { // Current vertex is inside
      ASSERT(check == 1);

      // Add current vertex to inside polygon
      *ip++ = vertnums[i];
      inside_points++;
    }

    prev = i;
    if (++next == nv)
      next = 0;
  }

  // Set number of verts for return.  If no real inside or outside points, then don't count edge points
  *inv = inside_points ? (ip - inbuf) : 0;
  *onv = outside_points ? (op - outbuf) : 0;
}

// Adds a new vertex to all instances of a given edge
void AddVertToAllEdges(room *rp, int v0, int v1, int new_v) {
  face *fp;
  int f, v;

  for (f = 0, fp = rp->faces; f < rp->num_faces; f++, fp++) {
    for (v = 0; v < fp->num_verts; v++) {
      if (((fp->face_verts[v] == v0) && (fp->face_verts[(v + 1) % fp->num_verts] == v1)) ||
          ((fp->face_verts[v] == v1) && (fp->face_verts[(v + 1) % fp->num_verts] == v0)))
        AddVertToFace(rp, f, new_v, v);
    }
  }
}

// Adds a new point to all instances of a given edge
// First adds the point to the room (or finds it there), then calls AddVertToAllEdges()
void AddPointToAllEdges(room *rp, int v0, int v1, vector *new_v) {
  int newvertnum = RoomAddVertices(rp, 1);

  rp->verts[newvertnum] = *new_v;

  AddVertToAllEdges(rp, v0, v1, newvertnum);
}

// Clips a face against another.  Produces one polygon that is the intersection of the two input
// faces, and zero or more extra polygons, which are parts of the input face outside the clipping face.
// The input face is replaced by the clipped face, and new faces (formed by the parts of the input
// face outside the clip-against face) are added to the end of the room's facelist.
// This routine assumes that part or all of the being-clipped face is inside the clip-against face
// Parameters:	arp - the room with the face that is being changed
//					afacenum - the face being changed
//					brp - the room with the face we're clipping against
//					bfacenum - the face we're clipping against
// Returns:		true if the clip was ok, false if there was an error
bool ClipFace(room *arp, int afacenum, room *brp, int bfacenum) {
  face *afp = &arp->faces[afacenum];
  face *bfp = &brp->faces[bfacenum];
  int edgenum;
  int16_t vbuf0[MAX_VERTS_PER_FACE], vbuf1[MAX_VERTS_PER_FACE];
  int16_t newface_verts[MAX_VERTS_PER_FACE][MAX_VERTS_PER_FACE];
  int newface_nvs[MAX_VERTS_PER_FACE];
  vertex newverts[MAX_VERTS_PER_FACE];
  int newvertnums[MAX_VERTS_PER_FACE];
  int num_newverts;
  int num_newfaces = 0;
  int16_t *src, *dest;
  int nv;
  int i;

  // Init some stuff
  nv = afp->num_verts;
  src = vbuf0;
  dest = vbuf1;

  Num_edge_inserts = 0;

  // copy our vertices into one buffer
  for (i = 0; i < nv; i++) {
    newverts[i].vec = arp->verts[afp->face_verts[i]];
    newverts[i].uvl = afp->face_uvls[i];
    newvertnums[i] = afp->face_verts[i];
    src[i] = i;
  }
  num_newverts = nv;

  // Clip our polygon against each edge
  for (edgenum = 0; edgenum < bfp->num_verts; edgenum++) {
    vector *v0, *v1;
    int16_t *outbuf = newface_verts[num_newfaces];
    int *onv = &newface_nvs[num_newfaces];

    v0 = &brp->verts[bfp->face_verts[(bfp->num_verts - edgenum) % bfp->num_verts]];
    v1 = &brp->verts[bfp->face_verts[bfp->num_verts - edgenum - 1]];

    ClipAgainstEdge(nv, src, newverts, &num_newverts, v0, v1, &afp->normal, dest, &nv, outbuf, onv);

    if (nv <= 2) // no new face -- faces must not overlap
      return 0;

    src = dest;
    dest = (src == vbuf0) ? vbuf1 : vbuf0;

    if (newface_nvs[num_newfaces]) // is there a new face?
      num_newfaces++;              //..yes, increment counter
  }

  // Now we have the clipped face and the other new faces
  // Replace the old face, and add the new faces
  int first_new_vert, first_new_face;
  face *fp;

  // Allocate space for the new verts
  first_new_vert = RoomAddVertices(arp, num_newverts - afp->num_verts);

  // Copy new vertices into room & get real vert numbers
  for (i = 0; i < num_newverts - afp->num_verts; i++) {
    arp->verts[first_new_vert + i] = newverts[afp->num_verts + i].vec;
    newvertnums[afp->num_verts + i] = first_new_vert + i;
  }

  // Replace the input face with the clipped face
  ReInitRoomFace(afp, nv);
  for (i = 0; i < nv; i++) {
    afp->face_verts[i] = newvertnums[src[i]];
    afp->face_uvls[i] = newverts[src[i]].uvl;
  }
  if (!ComputeFaceNormal(arp, afacenum))
    Int3(); // Get Matt

  // Allocate space for the new faces
  first_new_face = RoomAddFaces(arp, num_newfaces);

  // Copy data for new faces (the outside faces)
  for (i = 0; i < num_newfaces; i++) {
    fp = &arp->faces[first_new_face + i];
    InitRoomFace(fp, newface_nvs[i]);
    for (int j = 0; j < newface_nvs[i]; j++) {
      fp->face_verts[j] = newvertnums[newface_verts[i][j]];
      fp->face_uvls[j] = newverts[newface_verts[i][j]].uvl;
    }
    if (!ComputeFaceNormal(arp, first_new_face + i))
      Int3(); // Get Matt
#ifndef NEWEDITOR
    fp->tmap = arp->faces[afacenum].tmap;
#else
    // Apply texture
    HTextureApplyToRoomFace(arp, first_new_face + i, arp->faces[afacenum].tmap);
#endif
    CopyFaceFlags(fp, &arp->faces[afacenum]);
  }

  // Add new verts to edges
  for (i = 0; i < Num_edge_inserts; i++) {
    int v0, v1, new_v;
    v0 = newvertnums[Edge_inserts[i].v0];
    v1 = newvertnums[Edge_inserts[i].v1];
    new_v = newvertnums[Edge_inserts[i].new_v];

    AddVertToAllEdges(arp, v0, v1, new_v);
  }

  // Done!
  return 1;
}

// Adds a point to a face
// Parameters:	rp,facenum - the face we're adding the point to
//					vertindex - the position in the face's vertex list at which to add the point
//					vp - the point to add
void AddPointToFace(room *rp, int facenum, int vertindex, vector *vp) {
  face *fp = &rp->faces[facenum];
  int old_verts[MAX_VERTS_PER_FACE];
  roomUVL old_uvls[MAX_VERTS_PER_FACE];
  int t;

  // Add the new point to the room
  int newvertnum = RoomAddVertices(rp, 1);
  rp->verts[newvertnum] = *vp;

  // Make copy of old verts
  for (t = 0; t < fp->num_verts; t++) {
    old_verts[t] = fp->face_verts[t];
    old_uvls[t] = fp->face_uvls[t];
  }

  // Allocate for new verts
  ReInitRoomFace(fp, fp->num_verts + 1);

  // Copy old verts
  for (t = 0; t < vertindex; t++) {
    fp->face_verts[t] = old_verts[t];
    fp->face_uvls[t] = old_uvls[t];
  }
  fp->face_verts[t++] = newvertnum;
  // fp0->face_uvls[t] = ??
  for (; t < fp->num_verts; t++) {
    fp->face_verts[t] = old_verts[t - 1];
    fp->face_uvls[t] = old_uvls[t - 1];
  }
}

// Deletes a point from a face
// Parameters:	rp,facenum - the face we're deleting the point from
//					vertindex - the vertex to delete
void DeletePointFromFace(room *rp, int facenum, int vertindex) {
  face *fp = &rp->faces[facenum];
  int old_verts[MAX_VERTS_PER_FACE];
  roomUVL old_uvls[MAX_VERTS_PER_FACE];
  int t;

  // Make copy of old verts
  for (t = 0; t < fp->num_verts; t++) {
    old_verts[t] = fp->face_verts[t];
    old_uvls[t] = fp->face_uvls[t];
  }

  // Allocate for new verts
  ReInitRoomFace(fp, fp->num_verts - 1);

  // Copy old verts
  for (t = 0; t < vertindex; t++) {
    fp->face_verts[t] = old_verts[t];
    fp->face_uvls[t] = old_uvls[t];
  }
  for (; t < fp->num_verts; t++) {
    fp->face_verts[t] = old_verts[t + 1];
    fp->face_uvls[t] = old_uvls[t + 1];
  }

#ifndef NEWEDITOR
  World_changed = 1;
#endif
}

// Takes two faces which are going to be made into a portal and makes them match exactly
// Alternatively, checks if the two faces can be matched
// After this function the two faces will have exactly the same vertices
// Parameters:	rp0,facenum0 - one of the faces
//					rp1,facenum1 - the other face
//					check_only - if set, doesn't change anything; just checks the faces
// Returns the number of points added to the faces
// If just_checking set, returns true if faces match, else false
int MatchPortalFaces(room *rp0, int facenum0, room *rp1, int facenum1, bool check_only) {
  face *fp0 = &rp0->faces[facenum0];
  face *fp1 = &rp1->faces[facenum1];
  vector *v0, *v1, *prev_v0, *prev_v1;
  int n0, n1, i, j, prev_vn0, prev_vn1, max_nv;
  int points_added = 0;

check_faces:;

  // First, find one point in common
  for (i = 0; i < fp0->num_verts; i++) {
    for (j = 0; j < fp1->num_verts; j++)
      if (PointsAreSame(&rp0->verts[fp0->face_verts[i]], &rp1->verts[fp1->face_verts[j]]))
        break;
    if (j < fp1->num_verts)
      break;
  }
  if (i >= fp0->num_verts) {
    if (!check_only)
      Int3(); // Counldn't find common point!  This is very bad.  Get Matt.
    return 0; // no match
  }

  prev_vn0 = fp0->face_verts[i];
  prev_vn1 = fp1->face_verts[j];
  prev_v0 = &rp0->verts[prev_vn0];
  prev_v1 = &rp1->verts[prev_vn1];

  // Make starting points identical
  if (!check_only)
    *prev_v0 = *prev_v1;

  // Use the larger number of vert
  max_nv = __max(fp0->num_verts, fp1->num_verts);

  // Trace through faces, adding points where needed
  for (n0 = n1 = 1; n0 < max_nv && n1 < max_nv; n0++, n1++) {
    int vn0, vn1;

  recheck:;

    vn0 = fp0->face_verts[(i + n0) % fp0->num_verts];
    vn1 = fp1->face_verts[(j - n1 + fp1->num_verts) % fp1->num_verts];

    v0 = &rp0->verts[vn0];
    v1 = &rp1->verts[vn1];

    if (PointsAreSame(v0, v1)) { // Points are at least very close.
      if (!check_only)
        *v0 = *v1; // Make the points identical
    } else {       // The points are not the same, so we check for an extra (colinear) point

      float d0, d1;

      // One of these points should lie along the edge of the other. Find which is which
      d0 = vm_VectorDistance(v0, prev_v0);
      d1 = vm_VectorDistance(v1, prev_v1);

      if (d0 > d1) { // Point 1 is presumably on the edge prev_v0 -> v0

        // Make sure the point is actually on the edge
        if (CheckPointAgainstEdge(v1, prev_v0, v0, &fp0->normal)) {
          if (check_only)
            return 0;
          Int3(); // point isn't on edge!  Bad!  Get Matt!
        } else {
          if (!check_only) {

            // Add the point
            AddPointToAllEdges(rp0, prev_vn0, vn0, v1);
            points_added++;
          } else {
            n1++; // skip edge point & resume checking
            goto recheck;
          }
        }
      } else { // Point 0 is presumably on the edge prev_v1 -> v1

        // Make sure the point is actually on the edge
        if (CheckPointAgainstEdge(v0, prev_v1, v1, &fp1->normal)) {
          if (check_only)
            return 0;
          Int3(); // point isn't on edge!  Bad!  Get Matt!
        } else {

          if (!check_only) {
            // Add the point
            AddPointToAllEdges(rp1, prev_vn1, vn1, v0);
            points_added++;
          } else {
            n0++; // skip edge point & resume checking
            goto recheck;
          }
        }
      }

      // Start check again
      if (!check_only)
        goto check_faces;
    }

    prev_vn0 = vn0;
    prev_vn1 = vn1;

    prev_v0 = v0;
    prev_v1 = v1;
  }

  if (check_only)
    return 1; // no errors found, so faces ok

  ASSERT(fp0->num_verts == fp1->num_verts); // Get Matt!

  return points_added;
}

// Clips a pair of faces against each other.
// Produces one polygon in each face that is the intersection of the two input faces,
// and zero or more extra polygons, which are parts of the input faces outside the clipping faces.
// The input faces are replaced by the clipped faces, and new faces (formed by the parts of the input
// face outside the clip-against face) are added to the end of the room's facelist.
// This routine assumes that part or all of the being-clipped face is inside the clip-against face
// Parameters:	rp0,face0 - the first room:face
//					rp1,face1 - the second room:face
// Returns:		true if the clip was ok, false if there was an error
bool ClipFacePair(room *rp0, int face0, room *rp1, int face1) {
  // Clip each face aginst the other
  return (ClipFace(rp0, face0, rp1, face1) && ClipFace(rp1, face1, rp0, face0));
}

// Creates a new bridge room connecting two rooms
// Parameters:	attroomp,attface - one end of the new room
//					baseroom,baseface - the other end of the new room
// The new room is created by extruding from attroom/attface
void BuildBridge(room *attroomp, int attface, room *baseroomp, int baseface) {
  room *newroomp;
  face *afp, *bfp;
  int nv, nfaces;
  vector delta_vec, bc, ac;
  vector *basevert; // a vertex on the base face
  int i;
  float cos, dist;

  // Check that the two faces don't already have portals
  if (attroomp->faces[attface].portal_num != -1) {
    OutrageMessageBox("Cannot build bridge: There is already a portal at %d:%d", ROOMNUM(attroomp), attface);
    return;
  }
  if (baseroomp->faces[baseface].portal_num != -1) {
    OutrageMessageBox("Cannot build bridge: There is already a portal at %d:%d", ROOMNUM(baseroomp), baseface);
    return;
  }

  // Get values from current room & face
  afp = &attroomp->faces[attface];   // pointer to attach face
  bfp = &baseroomp->faces[baseface]; // pointer to base face
  nv = afp->num_verts;               // number of verts in attach face
  nfaces = nv + 2;                   // number of faces in new room

  // Make sure all points on each face are on the front of the other face
  vector tnorm = -afp->normal;
  int t = CheckFaceToPlane(baseroomp, baseface, &attroomp->verts[afp->face_verts[0]], &tnorm);
  if (t != -1) {
    OutrageMessageBox("Can't build bridge: Vertex %d on %d:%d is behind %d:%d", t, ROOMNUM(baseroomp), baseface,
                      ROOMNUM(attroomp), attface);
    return;
  }
  tnorm = -bfp->normal;
  t = CheckFaceToPlane(attroomp, attface, &baseroomp->verts[bfp->face_verts[0]], &tnorm);
  if (t != -1) {
    OutrageMessageBox("Can't build bridge: Vertex %d on %d:%d is behind %d:%d", t, ROOMNUM(attroomp), attface,
                      ROOMNUM(baseroomp), baseface);
    return;
  }

  // Compute normalized vector from attach face to base face
#ifndef NEWEDITOR
  ComputeCenterPointOnFace(&bc, baseroomp, baseface);
  ComputeCenterPointOnFace(&ac, attroomp, attface);
#else
  ComputeFaceBoundingCircle(&bc, baseroomp, baseface);
  ComputeFaceBoundingCircle(&ac, attroomp, attface);
#endif
  dist = vm_GetNormalizedDir(&delta_vec, &bc, &ac);

  // Compute the cosine of the angle between our vector and the normal of the base face
  cos = delta_vec * bfp->normal;

  // Get a pointer to a vertex on the base face
  basevert = &baseroomp->verts[bfp->face_verts[0]];

  // Get a pointer to our room
  newroomp = CreateNewRoom(nv * 2, nfaces, 0);
  if (newroomp == NULL) {
    OutrageMessageBox("Cannot build bridge: No free rooms.");
    return;
  }

  // Create new vertices in the plane of the base face
  for (i = 0; i < nv; i++) {
    float dist;

    // Copy from att face to new room
    newroomp->verts[i] = attroomp->verts[afp->face_verts[nv - 1 - i]];

    // Get distance from base face to this vertex
    dist = ((*basevert - newroomp->verts[i]) * bfp->normal) / cos;

    // Get the new vert
    newroomp->verts[nv + i] = newroomp->verts[i] + (delta_vec * dist);
  }

  // Set the faces for the room
  InitRoomFace(&newroomp->faces[0], nv);
  for (i = 0; i < nv; i++)
    newroomp->faces[0].face_verts[i] = i;

  InitRoomFace(&newroomp->faces[1], nv);
  for (i = 0; i < nv; i++)
    newroomp->faces[1].face_verts[i] = nv * 2 - 1 - i;

  for (i = 0; i < nfaces - 2; i++) {
    InitRoomFace(&newroomp->faces[i + 2], 4);
    newroomp->faces[i + 2].face_verts[0] = i;
    newroomp->faces[i + 2].face_verts[1] = i + nv;
    newroomp->faces[i + 2].face_verts[2] = ((i + 1) % nv) + nv;
    newroomp->faces[i + 2].face_verts[3] = (i + 1) % nv;
  }

  // Set normals
  if (!ResetRoomFaceNormals(newroomp))
    Int3(); // Get Matt

    // Set textures
#ifndef NEWEDITOR
  for (i = 0; i < nfaces; i++)
    newroomp->faces[i].tmap = i + 1;
#else
  int texnum = Editor_state.GetCurrentTexture();
  // Apply texture
  for (i = 0; i < nfaces; i++)
    HTextureApplyToRoomFace(newroomp, i, texnum);
#endif

  // Assign UVs
  AssignDefaultUVsToRoom(newroomp);

  // Connect the new room to the attach room
  LinkRoomsSimple(Rooms, ROOMNUM(attroomp), attface, ROOMNUM(newroomp), 0);

  // Clip the connecting faces against each other
  t = ClipFacePair(newroomp, 1, baseroomp, baseface);
  ASSERT(t != 0);

  // Make the two faces match exactly
  MatchPortalFaces(baseroomp, baseface, newroomp, 1);

  // Connect the new room to the base room
  LinkRoomsSimple(Rooms, ROOMNUM(baseroomp), baseface, ROOMNUM(newroomp), 1);

  // Set Changed flag
  World_changed = 1;
}

#define JOIN_EPSILON 0.01

// Connects two rooms by changing the shape of one room to attach to the other
// Pretty similar to BuildBridge(), but doesn't create a new room
// Parameters:	attroomp,attface - one end of the new room
//					baseroom,baseface - the other end of the new room
void JoinRooms(room *attroomp, int attface, room *baseroomp, int baseface) {
  face *afp, *bfp;
  int nv, nfaces, t;
  vector delta_vec;
  vector *basevert; // a vertex on the base face
  int i;
  float cos;
  vector save_verts[MAX_VERTS_PER_FACE]; // save verts for face in case we bail

  // Check that the two faces don't already have portals
  if (attroomp->faces[attface].portal_num != -1) {
    OutrageMessageBox("Cannot join: There is already a portal at %d:%d", ROOMNUM(attroomp), attface);
    return;
  }
  if (baseroomp->faces[baseface].portal_num != -1) {
    OutrageMessageBox("Cannot join: There is already a portal at %d:%d", ROOMNUM(baseroomp), baseface);
    return;
  }

  // Get values from current room & face
  afp = &attroomp->faces[attface];   // pointer to attach face
  bfp = &baseroomp->faces[baseface]; // pointer to base face
  nv = afp->num_verts;               // number of verts in attach face
  nfaces = nv + 2;                   // number of faces in new room

  // Get a pointer to a vertex on the base face
  basevert = &baseroomp->verts[bfp->face_verts[0]];

  // Make sure all points on the attach face are on the front of the base face
  // We could probably take this check out and catch the problem when we're moving the points
  vector tnorm = -bfp->normal;
  t = CheckFaceToPlane(attroomp, attface, basevert, &tnorm);
  if (t != -1) {
    if (OutrageMessageBox(
            MBOX_YESNO,
            "One or more points on the attach face are behind the base face.\nDo you still want to join?") != IDYES) {
      EditorStatus("Join aborted.");
      return;
    }
  }

  // Make a list of all verts in the attach room that are in a portal
  int portal_list[MAX_VERTS_PER_ROOM];
  int num_portal_verts;
  num_portal_verts = BuildListOfPortalVerts(attroomp, portal_list);

  // Save old verts
  for (i = 0; i < nv; i++)
    save_verts[i] = attroomp->verts[afp->face_verts[i]];

  int answer = OutrageMessageBox(MBOX_YESNO, "Do you want to connect to the center of the base face?\n\n"
                                             "Answer NO to extrude straight from the current face.");

  // Compute normalized vector from attach face to base face.
  // This is the direction in which we will move the attach face
  if (answer == IDYES) { // extrude toward the center of the base face
    vector bc, ac;
#ifndef NEWEDITOR
    ComputeCenterPointOnFace(&bc, baseroomp, baseface);
    ComputeCenterPointOnFace(&ac, attroomp, attface);
#else
    ComputeFaceBoundingCircle(&bc, baseroomp, baseface);
    ComputeFaceBoundingCircle(&ac, attroomp, attface);
#endif
    vm_GetNormalizedDir(&delta_vec, &bc, &ac);
  } else { // extrude straight from attach face
    delta_vec = afp->normal;
  }

  // Compute the cosine of the angle between our vector and the normal of the base face
  cos = delta_vec * bfp->normal;

  // If cos is zero, two center points are the same, so no move
  if (fabs(cos) < JOIN_EPSILON)
    goto no_move_points;

  // Move the verts on the attach face to lie in the plane as the base face
  for (i = 0; i < nv; i++) {
    vector *vp = &attroomp->verts[afp->face_verts[i]];
    float dist;

    // Get distance from base face to this vertex
    dist = ((*basevert - *vp) * bfp->normal) / cos;

    // If we're moving the point, make sure it's not part of a portal
    if (fabs(dist) > POINT_TO_PLANE_EPSILON) {

      // Look through list of portal verts for this vert
      for (t = 0; t < num_portal_verts; t++)
        if (portal_list[t] == afp->face_verts[i]) { // Vert is in a portal.  Abort!
          OutrageMessageBox("Can't form joint because vertex %d on %d:%d is shared with a portal.\n\nTry doing the "
                            "join the other way, or using the Bridge function.",
                            i, ROOMNUM(attroomp), attface);
          goto abort_join;
        }

      // Move the vert
      *vp += (delta_vec * dist);
    }
  }

  // Recompute the normals in the room
  if (!ResetRoomFaceNormals(attroomp))
    Int3(); // Get Matt

  // What about the uvs for the faces that changed?

no_move_points:;

  // Clip the connecting faces against each other
  if (!ClipFacePair(attroomp, attface, baseroomp, baseface)) {
    OutrageMessageBox("Error making portal -- faces probably don't overlap.");

  abort_join:;

    // Restore old verts
    for (i = 0; i < nv; i++)
      attroomp->verts[afp->face_verts[i]] = save_verts[i];

    // Recompute the normals in the room
    if (!ResetRoomFaceNormals(attroomp))
      Int3(); // Get Matt

    return;
  }

  // Make the two faces match exactly
  MatchPortalFaces(baseroomp, baseface, attroomp, attface);

  // Create the portals between the rooms
  LinkRoomsSimple(Rooms, ROOMNUM(baseroomp), baseface, ROOMNUM(attroomp), attface);

  // Set Changed flag
  World_changed = 1;
}

// Connects two rooms if they already match up exactly
// Parameters:	attroomp,attface - one end of the new room
//					baseroom,baseface - the other end of the new room
void JoinRoomsExact(room *attroomp, int attface, room *baseroomp, int baseface) {
  // Check that the two faces don't already have portals
  if (attroomp->faces[attface].portal_num != -1) {
    OutrageMessageBox("Cannot join: There is already a portal at %d:%d", ROOMNUM(attroomp), attface);
    return;
  }
  if (baseroomp->faces[baseface].portal_num != -1) {
    OutrageMessageBox("Cannot join: There is already a portal at %d:%d", ROOMNUM(baseroomp), baseface);
    return;
  }

  // Check if we can do an exact match
  if (!MatchPortalFaces(baseroomp, baseface, attroomp, attface, 1)) {
    OutrageMessageBox("Cannot do exact join: These faces do not match");
    return;
  }

  // Make the two faces match exactly
  MatchPortalFaces(baseroomp, baseface, attroomp, attface);

  // Create the portals between the rooms
  LinkRoomsSimple(Rooms, ROOMNUM(baseroomp), baseface, ROOMNUM(attroomp), attface);

  // Set Changed flag
  World_changed = 1;
}

bool FaceIsPlanar(int nv, int16_t *face_verts, vector *normal, vector *verts);

// Combine two faces, if they can be combined
// Parameters:	rp - the room the faces are in
//					face0,face1 - the two faces
// Returns:		true if the faces were combined, else false
// Note: The UV coordinates of the new face are derrived from face0
bool CombineFaces(room *rp, int face0, int face1) {
  face *fp0 = &rp->faces[face0], *fp1 = &rp->faces[face1];
  int nv0 = fp0->num_verts, nv1 = fp1->num_verts;
  int v0, v1;
  int16_t vertlist[MAX_VERTS_PER_FACE];
  roomUVL uvllist[MAX_VERTS_PER_FACE];
  int i, nv;
  int first0, first1, n0, n1;

  ASSERT(face0 != face1);

  // Check for portals
  if ((fp0->portal_num != -1) || (fp1->portal_num != -1)) {
#ifndef NEWEDITOR
    SetErrorMessage(
        "You cannot combine portal faces.\n\nMattT will add this functionality if you need it.  Bug him about it.");
#else
    SetErrorMessage("You cannot combine portal faces.");
#endif
    return 0;
  }

  // Make sure faces share an edge
  if (!FindSharedEdge(fp0, fp1, &v0, &v1)) {
#ifdef NEWEDITOR
    if (ned_FindSharedEdge(fp0, fp1, &v0, &v1))
      SetErrorMessage("The face normals point in opposite directions.");
    else
#endif
      SetErrorMessage("The faces do not share an edge.");
    return 0;
  }

  // Get indices for first & last points in each face.  One shared point is copied with each face.
  first0 = v0 + 1;
  n0 = nv0 - 1;
  first1 = v1 + 1;
  n1 = nv1 - 1;

  // See if faces share additional edges
  while (1) { // check forward on face0, backward on face1
    int check0 = (first0 + 1) % nv0, check1 = (first1 + n1 - 1) % nv1;

    if (fp0->face_verts[check0] != fp1->face_verts[check1])
      break;

    first0++;
    n0--;
    n1--;
  }
  while (1) { // check forward on face1, backward on face0
    int check1 = (first1 + 1) % nv1, check0 = (first0 + n0 - 1) % nv0;

    if (fp0->face_verts[check0] != fp1->face_verts[check1])
      break;

    first1++;
    n1--;
    n0--;
  }

  // Build list of verts for new face
  nv = 0;
  for (i = 0; i < n0; i++) { // Add points from first face
    vertlist[nv] = fp0->face_verts[(first0 + i) % nv0];
    uvllist[nv] = fp0->face_uvls[(first0 + i) % nv0];
    nv++;
  }
  for (i = 0; i < n1; i++) { // Add points from second face
    vertlist[nv] = fp1->face_verts[(first1 + i) % nv1];
    uvllist[nv] = fp1->face_uvls[(first1 + i) % nv1];
    nv++;
  }

  // Calculate new normal
  vector new_normal;
  ComputeNormal(&new_normal, nv, vertlist, rp->verts);

  // Check if new face is planar
  if (!FaceIsPlanar(nv, vertlist, &new_normal, rp->verts)) {
    SetErrorMessage("The new face would not be planar.");
    return 0;
  }

  // Check if new face is convex
  int t;
  if ((t = CheckFaceConcavity(nv, vertlist, &new_normal, rp->verts)) != -1) {
    SetErrorMessage("The new face would be concave (at vertex %d).", vertlist[t]);
    return 0;
  }

  // Reset face0 for new verts
  ReInitRoomFace(fp0, nv);

  // Copy into new face
  for (i = 0; i < nv; i++) {
    fp0->face_verts[i] = vertlist[i];
    fp0->face_uvls[i] = uvllist[i];
  }

  // Set new normal
  fp0->normal = new_normal;

  // Reassign uvls in face
  AssignUVsToFace(rp, face0, &fp0->face_uvls[0], &fp0->face_uvls[1], 0, 1);

  // Delete the old face
  DeleteRoomFace(rp, face1);

  // Set flag
  World_changed = 1;

  // Everything's ok
  return 1;
}

// Deletes the given room from the mine
void DeleteRoomFromMine(room *rp) {
  int i, p;

  // See if this room is linked to the terrain
  for (p = 0; p < rp->num_portals; p++) {
    portal *pp = &rp->portals[p];
    if (pp->croom == -1) {
      OutrageMessageBox("You can't delete a room that attaches to the terrain.  Detach the room before deleting.");
      return;
    }
  }

  // Check if player or viewer object in this room
  for (i = rp->objects; i != -1; i = Objects[i].next) {

    // Is this the player object?
    if (i == OBJNUM(Player_object)) {
      OutrageMessageBox("You can't delete the room with the player in it.");
      return;
    }

#ifndef NEWEDITOR
    // Is this the viewer object?
    if (i == OBJNUM(Viewer_object)) { // Try to find another viewer
      int objnum = FindNextViewerObject(Viewer_object->id + 1, -1);
      if (objnum == OBJNUM(Viewer_object)) {
        OutrageMessageBox("You can't delete the room with the only viewer in it.");
        return;
      } else
        SelectNextViewer();
    }
#endif
  }

  //	count the number of rooms used. if only one, don't delete it.
  for (i = 0; i <= Highest_room_index; i++)
    if (Rooms[i].used && (i != ROOMNUM(rp)))
      break;
  if (i > Highest_room_index) {
    OutrageMessageBox("You can't delete the only room in the mine.");
    return;
  }

  // If this is current room, set new current room
  if (rp == Curroomp) {
    room *newroomp = NULL;

    // Look for connected room
    for (p = 0; p < rp->num_portals; p++)
      if (rp->portals[p].croom != -1) {
        newroomp = &Rooms[rp->portals[0].croom];
        break;
      }

    // If didn't find connected room, look for any room
    if (newroomp == NULL) {
      for (i = 0; i <= Highest_room_index; i++) {
        if (Rooms[i].used && (i != ROOMNUM(rp))) {
          newroomp = &Rooms[i];
          break;
        }
      }
    }

    ASSERT(newroomp != NULL);

#ifndef NEWEDITOR
    Curroomp = newroomp;
    Curface = Curedge = Curvert = 0;
    Curportal = -1;
#else
    theApp.m_pLevelWnd->SetPrim(newroomp, 0, -1, 0, 0);
#endif

    EditorStatus("Current room set to %d.", ROOMNUM(Curroomp));
  }

  // Delete connections
  while (rp->num_portals) {
    portal *pp = &rp->portals[0];
    ASSERT(pp->croom != -1);
    DeleteRoomPortal(&Rooms[pp->croom], pp->cportal);
    DeleteRoomPortal(rp, 0);
  }

  // If this is marked room, clear marked room
  if (Markedroomp == rp)
    Markedroomp = NULL;

  // Free the objects in this room
  for (i = rp->objects; i != -1; i = Objects[i].next) {
#ifdef NEWEDITOR
    if (IS_GENERIC(Objects[i].type))
      LevelObjDecrementObject(Objects[i].id);
    else if (Objects[i].type == OBJ_DOOR) {
      ASSERT(rp->flags & RF_DOOR);
      LevelDoorDecrementDoorway(Objects[i].id);
    }
#endif
    ObjDelete(i);
  }

#ifdef NEWEDITOR
  // Unmark the textures in this room
  for (i = 0; i < rp->num_faces; i++)
    LevelTexDecrementTexture(rp->faces[i].tmap);
#endif

  // Delete any triggers in this room
  face *fp;
  for (i = 0, fp = rp->faces; i < rp->num_faces; fp++, i++)
    if (fp->flags & FF_HAS_TRIGGER)
      DeleteTrigger(ROOMNUM(rp), i);

  // Remove this room from the selected list (if there)
  RemoveRoomFromSelectedList(ROOMNUM(rp));

  // Free up the room
  FreeRoom(rp);
}

#define DROP_ROOM_OFFSET 20.0

// Places a room a short distance from the specified room & face
// The new room is not attached to anything
// Parameters:	baseroomp,baseface - the new room is dropped off of this face
//					droproom_num - the room to be dropped
void DropRoom(room *baseroomp, int baseface, int droproom_num) {
  int roomnum;
  room *newroomp;
  vector oldcenter, facecenter, newcenter, offset;
  float rad;

  // Create a new room
  roomnum = GetFreeRoom(0);
  if (roomnum == -1) {
    OutrageMessageBox("Cannot drop room: No free rooms.");
    return;
  }
  newroomp = &Rooms[roomnum];

  // Copy data to new room
  CopyRoom(newroomp, &Rooms[droproom_num]);

  // Compute offset
  rad = ComputeRoomBoundingSphere(&oldcenter, newroomp);
#ifndef NEWEDITOR
  ComputeCenterPointOnFace(&facecenter, baseroomp, baseface);
#else
  ComputeFaceBoundingCircle(&facecenter, baseroomp, baseface);
#endif
  newcenter = facecenter - (baseroomp->faces[baseface].normal * (rad + DROP_ROOM_OFFSET));
  offset = newcenter - oldcenter;

  // Add in offset to all the points
  for (int i = 0; i < newroomp->num_verts; i++)
    newroomp->verts[i] += offset;

    // Set current room to new room
#ifndef NEWEDITOR
  Curroomp = newroomp;
  Curface = Curedge = Curvert = 0;
  Curportal = -1;
#else
  theApp.m_pLevelWnd->SetPrim(newroomp, 0, -1, 0, 0);
#endif

  // We're done
  World_changed = 1;
}

// Place a room on the terrain for orientation before attachment
// Parameters:	cellnum - the cell where the room is being placed
//					placed_room - the number of the room to be attached
//					placed_room_face the face on placed_room that's attached
void PlaceExternalRoom(int cellnum, int placed_room, int placed_room_face, bool align_to_terrain) {
  room *placedroomp = &Rooms[placed_room];

  // Clear the placed group if one exists
  Placed_group = NULL;

  // Clear the placed door if one exists
  if (Placed_door != -1) {
    ASSERT(Placed_room != -1);
    FreeRoom(&Rooms[Placed_room]);
    Placed_door = Placed_door = -1;
  }

  // Set globals
  Placed_room = placed_room;
  Placed_room_face = placed_room_face;
  Placed_room_angle = 0;
  Placed_baseroomp = NULL;

  if (align_to_terrain)
    Placed_room_orient.fvec = TerrainNormals[MAX_TERRAIN_LOD - 1][cellnum].normal1;
  else
    Placed_room_orient.fvec = Identity_matrix.uvec;

  // Compute attach point on terrain
  ComputeTerrainSegmentCenter(&Placed_room_attachpoint, cellnum);

  // Compute attach points on each face
#ifndef NEWEDITOR
  ComputeCenterPointOnFace(&Placed_room_origin, placedroomp, placed_room_face);
#else
  ComputeFaceBoundingCircle(&Placed_room_origin, placedroomp, placed_room_face);
#endif

  // Compute initial orientation matrix
  ComputePlacedRoomMatrix();

  // Set the flag
  State_changed = 1;
}

// Find all the matching faces between two rooms and join them
// Returns the number of new portals created
int JoinAllMatchingFaces(room *rp0, room *rp1) {
  face *fp0, *fp1;
  int f0, f1;
  int join_count = 0;

  // Check each face in room 0 against each in room 1
  for (f0 = 0, fp0 = rp0->faces; f0 < rp0->num_faces; f0++, fp0++) {

    // Check if face 0 is already part of portal
    if (fp0->portal_num != -1)
      continue;

    // Check current face against all faces in room 1
    for (f1 = 0, fp1 = rp1->faces; f1 < rp1->num_faces; f1++, fp1++) {

      // Check if face 1 is already part of portal
      if (fp1->portal_num != -1)
        continue;

      // First check if faces have same number of verts
      // This used to check if the normals were the same, but I decided that was bad. -MT, 3/30/99
      if ((fp0->num_verts == fp1->num_verts)) {
        int i, j, n;

        // Find one point in common
        for (i = 0; i < fp0->num_verts; i++) {
          for (j = 0; j < fp1->num_verts; j++)
            if (PointsAreSame(&rp0->verts[fp0->face_verts[i]], &rp1->verts[fp1->face_verts[j]]))
              break;
          if (j < fp1->num_verts)
            break;
        }
        if (i >= fp0->num_verts) // Couldn't find a match
          continue;              //..so go on to next face

        // Trace through verts in faces, making sure they match
        for (n = 1; n < fp0->num_verts; n++) {
          vector *v0, *v1;

          v0 = &rp0->verts[fp0->face_verts[(i + n) % fp0->num_verts]];
          v1 = &rp1->verts[fp1->face_verts[(j - n + fp1->num_verts) % fp1->num_verts]];

          if (!PointsAreSame(v0, v1)) // Found mismatch
            break;

          *v0 = *v1; // make points *exactly* the same
        }
        if (n < fp0->num_verts) // Found a mismatch
          continue;             //..so go on to next face

        // Found extact match, so join
        LinkRoomsSimple(Rooms, ROOMNUM(rp0), f0, ROOMNUM(rp1), f1);

        // Increment count
        join_count++;
      }
    }
  }

  return join_count;
}

// Find all the overlapping faces between two rooms and join them
// Returns the number of new portals created
int JoinAllOverlappingFaces(room *rp0, room *rp1) {
  face *fp0, *fp1;
  int f0, f1;
  int join_count = 0;

  // Check each face in room 0 against each in room 1
  for (f0 = 0, fp0 = rp0->faces; f0 < rp0->num_faces; f0++, fp0++) {

    // Check if face 0 is already part of portal
    if (fp0->portal_num != -1)
      continue;

    // Check current face against all faces in room 1
    for (f1 = 0, fp1 = rp1->faces; f1 < rp1->num_faces; f1++, fp1++) {

      // Check if face 1 is already part of portal
      if (fp1->portal_num != -1)
        continue;

      // This used to check if normals the same, but I decided that was bad. -MT, 3/30/99
      {
        int i;
        vector *planepoint, *norm;

        // Normals are same, so check if points are all in the same plane
        planepoint = &rp1->verts[fp1->face_verts[0]];
        norm = &fp1->normal;
        for (i = 0; i < fp0->num_verts; i++)
          if (CheckPointToPlane(&rp0->verts[fp0->face_verts[i]], planepoint, norm))
            break;
        if (i < fp0->num_verts)
          continue;

        planepoint = &rp0->verts[fp0->face_verts[0]];
        norm = &fp0->normal;
        for (i = 0; i < fp1->num_verts; i++)
          if (CheckPointToPlane(&rp1->verts[fp1->face_verts[i]], planepoint, norm))
            break;
        if (i < fp1->num_verts)
          continue;

        // Clip the connecting faces against each other
        if (ClipFacePair(rp0, f0, rp1, f1)) {

          // Make the two faces match exactly
          MatchPortalFaces(rp0, f0, rp1, f1);

          // Create the portals between the rooms
          LinkRoomsSimple(Rooms, ROOMNUM(rp0), f0, ROOMNUM(rp1), f1);

          // Reset face pointer, which may have been changed by the clip
          fp0 = &rp0->faces[f0];

          // Increment count
          join_count++;

          // Stop checking this face, since we've formed a portal for it
          break;
        } else { // it's kind of bad if the clip fails, but we have to live with it

          // Reset face pointers, which may have been changed by the clip
          fp0 = &rp0->faces[f0];
          fp1 = &rp1->faces[f1];
        }
      }
    }
  }

  return join_count;
}

// Find all the adjacent faces between two rooms and join them
void JoinAllAdjacentFaces(room *rp0, room *rp1) {
  int join_count;
  int answer;

  answer = OutrageMessageBox(MBOX_YESNOCANCEL, "Do you want to include non-matching faces?\n\n"
                                               "Selecting YES means faces will be joined if they overlap.\n"
                                               "Selecting NO means faces much match exactly.");

  if (answer == IDCANCEL)
    return;

  if (answer == IDYES)
    join_count = JoinAllOverlappingFaces(rp0, rp1);
  else
    join_count = JoinAllMatchingFaces(rp0, rp1);

  World_changed = 1;

  OutrageMessageBox("%d new portals created.\n", join_count);
}

int FindNeighbor(room *rp, int facenum, int edgenum) {
  face *fp = &rp->faces[facenum];
  int v0, v1, f, v;

  v0 = fp->face_verts[edgenum];
  v1 = fp->face_verts[(edgenum + 1) % fp->num_verts];

  for (f = 0, fp = rp->faces; f < rp->num_faces; f++, fp++)
    for (v = 0; v < fp->num_verts; v++)
      if ((fp->face_verts[v] == v1) && (fp->face_verts[(v + 1) % fp->num_verts] == v0))
        return f;

  return -1;
}

void PropagateFromFace(room *rp, int facenum, uint8_t *face_flags, bool matching_faces_only) {
  face *fp = &rp->faces[facenum];

  face_flags[facenum] = 1;

  for (int v = 0; v < fp->num_verts; v++) {
    int t;

    t = FindNeighbor(rp, facenum, v);

    if ((t != -1) && !face_flags[t])
      if (!matching_faces_only || (rp->faces[t].tmap == fp->tmap)) {
        HTexturePropagateToFace(rp, t, rp, facenum);
        PropagateFromFace(rp, t, face_flags, matching_faces_only);
      }
  }
}

// Propagate a texture from one face to all the faces in the room
// If matching_faces_only is set, only propagate if the face has the same texture
void PropagateToAllFaces(room *rp, int facenum, bool matching_faces_only) {
  uint8_t face_flags[MAX_FACES_PER_ROOM];

  // Clear flags
  for (int i = 0; i < rp->num_faces; i++)
    face_flags[i] = 0;

  // Start recursive function
  PropagateFromFace(rp, facenum, face_flags, matching_faces_only);

  // Set flag
  World_changed = 1;
}

// Splits a face unto triangles, by fanning
// Parameters:	rp,facenum - the face to triangulate
//					vertnum - the vert that is the base of the fan
void TriangulateFace(room *rp, int facenum, int vertnum) {
  face *fp = &rp->faces[facenum], *nfp;
  int num_new_faces, first_new_face, i;

  if (fp->num_verts == 3) {
    OutrageMessageBox("Face %d has three verts and cannot be split.", facenum);
    return;
  }

  if (fp->portal_num != -1) {
    OutrageMessageBox("You cannot split a face that's part of a portal.");
    return;
  }

  if (fp->flags & (FF_HAS_TRIGGER + FF_FLOATING_TRIG)) {
    OutrageMessageBox("You cannot split a face that has a trigger.");
    return;
  }

  // Add new faces
  num_new_faces = fp->num_verts - 2;
  first_new_face = RoomAddFaces(rp, num_new_faces);

  // Reset pointer to our old face
  fp = &rp->faces[facenum];

  // Set up each face
  for (i = 0, nfp = &rp->faces[first_new_face]; i < num_new_faces; i++, nfp++) {
    InitRoomFace(nfp, 3);

    // Set verts & uvls for this face
    nfp->face_verts[0] = fp->face_verts[vertnum];
    nfp->face_uvls[0] = fp->face_uvls[vertnum];
    for (int v = 1; v < 3; v++) {
      nfp->face_verts[v] = fp->face_verts[(vertnum + i + v) % fp->num_verts];
      nfp->face_uvls[v] = fp->face_uvls[(vertnum + i + v) % fp->num_verts];
    }

    // Copy other data for this face
    CopyFaceFlags(nfp, fp);
#ifndef NEWEDITOR
    nfp->tmap = fp->tmap;
#else
    // Apply texture
    HTextureApplyToRoomFace(rp, first_new_face + i, nfp->tmap);
#endif
    nfp->light_multiple = fp->light_multiple;

    // Compute the normal for this face
    ComputeFaceNormal(rp, first_new_face + i);
  }

  // Delete original face
  DeleteRoomFace(rp, facenum);

  // Check for degenerate faces (caused by colinear points) and merge with adjacent faces
  first_new_face--; // original face deleted, causing shift
  bool ok;
  do { // Check first face
    ok = ComputeFaceNormal(rp, first_new_face);
    if (!ok) {
      face *fp = &rp->faces[first_new_face];
      ASSERT(first_new_face < rp->num_faces - 1);
      AddVertToFace(rp, first_new_face, rp->faces[first_new_face + 1].face_verts[2], fp->num_verts - 1);
      rp->faces[first_new_face].face_uvls[rp->faces[first_new_face].num_verts - 1] =
          rp->faces[first_new_face + 1].face_uvls[2];
      DeleteRoomFace(rp, first_new_face + 1);
      num_new_faces--;
    }
  } while (!ok);
  do { // Check last face
    ok = ComputeFaceNormal(rp, rp->num_faces - 1);
    if (!ok) {
      face *fp = &rp->faces[rp->num_faces - 2];
      ASSERT(first_new_face < rp->num_faces - 1);
      AddVertToFace(rp, rp->num_faces - 1, rp->faces[rp->num_faces - 2].face_verts[1], 0);
      rp->faces[rp->num_faces - 1].face_uvls[1] = rp->faces[rp->num_faces - 2].face_uvls[1];
      DeleteRoomFace(rp, rp->num_faces - 2);
      num_new_faces--;
    }
  } while (!ok);

  // We're done
  EditorStatus("Face %d replace by %d new faces", facenum, num_new_faces);
#ifndef NEWEDITOR
  World_changed = 1;
#endif
}

// Deletes the connection between two rooms.  Deletes both portals.
void DeletePortalPair(room *rp, int portalnum) {
  int roomnum = ROOMNUM(rp);
  portal *pp = &rp->portals[portalnum];
  int croom = pp->croom, cportal = pp->cportal;

  // Make sure the connecting portal points back to this portal
  if ((Rooms[croom].portals[cportal].croom != roomnum) || (Rooms[croom].portals[cportal].cportal != portalnum)) {
    OutrageMessageBox("Cannot delete this portal: the connecting portal does not point back at it.\n");
    return;
  }

  // Check all the faces in this portal to make sure they point back to the portal
  if (rp->faces[pp->portal_face].portal_num != portalnum) {
    OutrageMessageBox("Cannot delete this portal: its face does not point back at it.");
    return;
  }

  DeleteRoomPortal(rp, portalnum);
  DeleteRoomPortal(&Rooms[croom], cportal);

  EditorStatus("Deleted room %d portal %d and room %d portal %d", roomnum, portalnum, croom, cportal);

  World_changed = 1;
}

// Flips a face
void FlipFace(room *rp, int facenum) {
  face *fp = &rp->faces[facenum];

  ASSERT(fp->portal_num == -1);

  for (int i = 0; i < fp->num_verts / 2; i++) {
    int v = fp->face_verts[i];
    fp->face_verts[i] = fp->face_verts[fp->num_verts - 1 - i];
    fp->face_verts[fp->num_verts - 1 - i] = v;

    roomUVL uvl = fp->face_uvls[i];
    fp->face_uvls[i] = fp->face_uvls[fp->num_verts - 1 - i];
    fp->face_uvls[fp->num_verts - 1 - i] = uvl;
  }

  // Get new normal
  if (!ComputeFaceNormal(rp, facenum))
    Int3(); // Bad!  Get Matt!

#ifndef NEWEDITOR
  World_changed = 1;
  EditorStatus("Room %d face %d flipped.", ROOMNUM(rp), facenum);
#else
  if (ROOMNUM(rp) < MAX_ROOMS)
    EditorStatus("Room %d face %d flipped.", ROOMNUM(rp), facenum);
  else
    EditorStatus("Face %d flipped.", facenum);
#endif
}

// Attempt to fix the cracks in a level
void FixCracks() {
  int r, p, f, f2, v, v2;
  room *rp;
  face *fp, *fp2;
  int possible_tjoints = 0, tjoints_fixed = 0;
  int points_added = 0;
  int points_are_same_count = 0;

  if (OutrageMessageBox(MBOX_YESNO, "This function is somewhat dangerous.\n\n"
                                    "You should be sure to save your level beforehand, and you should check the "
                                    "results very carefully afterwards.\n\n"
                                    "Continue?") != IDYES)
    return;

  // First fix gaps at portals
  for (r = 0, rp = Rooms; r <= Highest_room_index; r++, rp++) {
    if (rp->used) {
      for (p = 0; p < rp->num_portals; p++) {
        portal *pp = &rp->portals[p];
        points_added +=
            MatchPortalFaces(rp, pp->portal_face, &Rooms[pp->croom], Rooms[pp->croom].portals[pp->cportal].portal_face);
      }
    }
  }

  // Now search for and fix t-joints
  for (r = 0, rp = Rooms; r <= Highest_room_index; r++, rp++) {
    if (rp->used)
      for (f = 0, fp = rp->faces; f < rp->num_faces; f++, fp++) {
        // if (fp->portal_num == -1)
        { // don't check portal faces
        recheck_face:;
          for (v = 0; v < fp->num_verts; v++) {
            int vv0 = fp->face_verts[v], vv1 = fp->face_verts[(v + 1) % fp->num_verts];
            for (f2 = 0, fp2 = rp->faces; f2 < rp->num_faces; f2++, fp2++) {
              if (f2 != f) {
                for (v2 = 0; v2 < fp2->num_verts; v2++) {
                  int tt0 = fp2->face_verts[v2], tt1 = fp2->face_verts[(v2 + 1) % fp2->num_verts];

                  if ((vv0 == tt1) && (vv1 == tt0))
                    break; // found one, so stop
                }
                if (v2 < fp2->num_verts)
                  break;
              }
            }
            if (f2 == rp->num_faces) { // didn't find a match
              // mprintf(0,"Couldn't find shared edge for %d:%d edge %d\n",r,f,v);
              possible_tjoints++;

              // Look for vert on this edge
              for (f2 = 0, fp2 = rp->faces; f2 < rp->num_faces; f2++, fp2++) {
                if (f2 != f) {
                  for (v2 = 0; v2 < fp2->num_verts; v2++) {
                    int tt0 = fp2->face_verts[v2], tt1 = fp2->face_verts[(v2 + 1) % fp2->num_verts];

                    if (vv0 == tt1) { // one point maches; check if next is on the edge
                      // if (PointsAreColinear(&rp->verts[vv0],&rp->verts[tt0],&rp->verts[vv1])) {

                      if (PointsAreSame(&rp->verts[tt0], &rp->verts[vv0]))
                        points_are_same_count++;
                      if (PointsAreSame(&rp->verts[tt0], &rp->verts[vv1]))
                        points_are_same_count++;

                      if (CheckPointToPlane(&rp->verts[tt0], &rp->verts[vv0], &fp->normal) == 0) {
                        if (CheckPointAgainstEdge(&rp->verts[tt0], &rp->verts[vv0], &rp->verts[vv1], &fp->normal) ==
                            0) {
                          // make sure the new point is actually between the two edge points
                          float edge_len = vm_VectorDistance(&rp->verts[vv1], &rp->verts[vv0]);
                          float d0 = vm_VectorDistance(&rp->verts[tt0], &rp->verts[vv0]);
                          float d1 = vm_VectorDistance(&rp->verts[tt0], &rp->verts[vv1]);
                          if ((d0 < edge_len) && (d1 < edge_len)) {
                            mprintf(0, "Adding %d to edge %d of %d:%d (from face %d)\n", tt0, v, r, f, f2);
                            AddVertToFace(rp, f, tt0, v);
                            tjoints_fixed++;
                            goto recheck_face;
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
  }

  // Fix portals again because points may have been added when fixing t-joints
  for (r = 0, rp = Rooms; r <= Highest_room_index; r++, rp++) {
    if (rp->used) {
      for (p = 0; p < rp->num_portals; p++) {
        portal *pp = &rp->portals[p];
        points_added +=
            MatchPortalFaces(rp, pp->portal_face, &Rooms[pp->croom], Rooms[pp->croom].portals[pp->cportal].portal_face);
      }
    }
  }

  mprintf(0, "%d possible t-joints\n", possible_tjoints);
  mprintf(0, "points_are_same_count = %d\n", points_are_same_count);

  OutrageMessageBox("Level fix results:\n\n"
                    "    %d points added to portals\n"
                    "    %d T-joints fixed",
                    points_added, tjoints_fixed);

  World_changed = 1;
}

// Variables for undoing a snap
int Snap_roomnum = -1;
int Snap_vertnum;
vector Snap_old_point, Snap_new_point;

// Moves a vertex to lie on a specified edge
void SnapPointToEdge(room *rp, int vertnum, vector *v0, vector *v1) {
  vector *vp = &rp->verts[vertnum];
  vector edge_vec, edge_normal;
  float d;

  // Save info for undo
  Snap_roomnum = ROOMNUM(rp);
  Snap_vertnum = vertnum;
  Snap_old_point = *vp;

  // Calculate new point
  edge_normal = edge_vec = *v1 - *v0;
  vm_NormalizeVector(&edge_normal);
  d = vm_DistToPlane(vp, &edge_normal, v0);
  *vp = *v0 + edge_normal * d;

  // Set the point
  Snap_new_point = *vp;

  // Done
  EditorStatus("Room %d point %d snapped.", Snap_roomnum, Snap_vertnum);
  World_changed = 1;
}

// Moves a vertex to be conincident with another vertex
void SnapPointToPoint(room *rp, int vertnum, room *snapto_rp, int snapto_vertnum) {
  vector *vp = &rp->verts[vertnum];

  // Save info for undo
  Snap_roomnum = ROOMNUM(rp);
  Snap_vertnum = vertnum;
  Snap_old_point = *vp;

  *vp = snapto_rp->verts[snapto_vertnum];

  // Set the point
  Snap_new_point = *vp;

  // Done
  EditorStatus("Room %d point %d snapped.", Snap_roomnum, Snap_vertnum);
  World_changed = 1;
}

// Moves a vertex to lie on a specified plane
void SnapPointToFace(room *rp, int vertnum, vector *v0, vector *normal) {
  vector *vp = &rp->verts[vertnum];
  float d;

  // Save info for undo
  Snap_roomnum = ROOMNUM(rp);
  Snap_vertnum = vertnum;
  Snap_old_point = *vp;

  // Calculate new point
  d = vm_DistToPlane(vp, normal, v0);
  *vp -= *normal * d;

  // Set the point
  Snap_new_point = *vp;

  // Done
  EditorStatus("Room %d point %d snapped.", Snap_roomnum, Snap_vertnum);
  World_changed = 1;
}

// Undoes the most recently-performed snap
void UndoSnap() {
  if (Snap_roomnum == -1)
    return;

  if ((Snap_roomnum <= Highest_room_index) && Rooms[Snap_roomnum].used) {
    vector *vp = &Rooms[Snap_roomnum].verts[Snap_vertnum];

    if ((vp->x == Snap_new_point.x) && (vp->y == Snap_new_point.y) && (vp->z == Snap_new_point.z)) {
      *vp = Snap_old_point;
      World_changed = 1;
      return;
    }
  }

  OutrageMessageBox("Cannot Undo: The snapped point has changed since the snap.");
  Snap_roomnum = -1;
}

// Gets the angle between two vectors in the range 0..1
// Parameters:	base - the start of both vectors
//					v0,v1 - the end points of the two vectors
//					normal - the surface normal of the face these verts lie in
// Returns the cosine of the angle between <base,v0> and <base,v1>
float GetAngle(vector *base, vector *v0, vector *v1, vector *normal) {
  vector edge0, edge1, cross;
  float dot;

  vm_GetNormalizedDir(&edge0, v0, base);
  vm_GetNormalizedDir(&edge1, v1, base);

  dot = edge0 * edge1;
  cross = edge0 ^ edge1;

  if (cross * *normal > 0)
    return 0.25 - dot / 4.0;
  else
    return 0.75 + dot / 4.0;
}

// Connects two rooms in a pleasing way
void BuildSmoothBridge(room *rp0, int facenum0, room *rp1, int facenum1) {
  room *rp;
  int nv0, nv1, nverts, nfaces;
  int i;
  vector tverts[MAX_VERTS_PER_FACE];

  // Get values from current faces
  face *fp0 = &rp0->faces[facenum0];
  face *fp1 = &rp1->faces[facenum1];
  nv0 = fp0->num_verts;
  nv1 = fp1->num_verts;

  // Check for portal already here
  if (fp0->portal_num != -1) {
    OutrageMessageBox("Cannot build bridge: There is already a connection at %d:%d.", ROOMNUM(rp0), facenum0);
    return;
  }
  if (fp1->portal_num != -1) {
    OutrageMessageBox("Cannot build bridge: There is already a connection at %d:%d.", ROOMNUM(rp1), facenum1);
    return;
  }

  // Get values for new room
  nverts = nv0 + nv1;
  nfaces = nverts + 2;

  // Get a pointer to our room
  rp = CreateNewRoom(nverts, nfaces);
  if (rp == NULL) {
    OutrageMessageBox("Cannot build bridge: No free rooms.");
    return;
  }

  // Set the vertices for the room
  for (i = 0; i < nv0; i++)
    rp->verts[i] = rp0->verts[fp0->face_verts[i]];
  for (i = 0; i < nv1; i++)
    rp->verts[nv0 + i] = rp1->verts[fp1->face_verts[nv1 - 1 - i]];

  // Create the connecting faces

  // Compute normalized vector from attach face to base face
  vector c0, c1, delta_vec;
#ifndef NEWEDITOR
  ComputeCenterPointOnFace(&c0, rp0, facenum0);
  ComputeCenterPointOnFace(&c1, rp1, facenum1);
#else
  ComputeFaceBoundingCircle(&c0, rp0, facenum0);
  ComputeFaceBoundingCircle(&c1, rp1, facenum1);
#endif
  vm_GetNormalizedDir(&delta_vec, &c1, &c0);

  // Compute the cosine of the angle between our vector and the normal of the base face
  float cos = delta_vec * fp1->normal;

  // Get a pointer to a vertex on face1
  vector *vp = &rp->verts[nv0];

  // Project the verts from end face0 into end face1
  for (i = 0; i < nv0; i++) {

    // Get distance from base face to this vertex
    float dist = ((*vp - rp->verts[i]) * fp1->normal) / cos;

    // Get the new vert
    tverts[i] = rp->verts[i] + (delta_vec * dist);
  }

  // Find closest face0 point to point 0 of face1
  int smallest_i = -1;
  float smallest_angle = 1.0;
  vector tnorm = -fp1->normal;
  for (i = 0; i < nv0; i++) {
    float angle = GetAngle(&c1, &rp->verts[nv0], &tverts[i], &tnorm);
    if (angle > 0.5)
      angle = 1.0 - angle; // we want closest in either direction
    if (angle < smallest_angle) {
      smallest_i = i;
      smallest_angle = angle;
    }
  }
  ASSERT(smallest_i != -1);

  // Generate new faces
  int prev1 = 0, // set up the previous edge
      prev0 = smallest_i;
  for (i = 0; i < nverts; i++) {
    float angle0, angle1;

    face *fp = &rp->faces[i];

    InitRoomFace(fp, 3);

    fp->face_verts[0] = nv0 + prev1;
    fp->face_verts[1] = prev0;

    if (i == nverts - 1) { // special case for last edge
      if ((rp->faces[0].face_verts[0] != fp->face_verts[0]) && (rp->faces[0].face_verts[0] != fp->face_verts[1]))
        fp->face_verts[2] = rp->faces[0].face_verts[0];
      else {
        ASSERT((rp->faces[0].face_verts[1] != fp->face_verts[0]) && (rp->faces[0].face_verts[0] != fp->face_verts[1]));
        fp->face_verts[2] = rp->faces[0].face_verts[1];
      }
      break;
    }

    // Pick point for third vert
    angle1 = GetAngle(&c1, &rp->verts[nv0 + prev1], &rp->verts[nv0 + (prev1 + 1) % nv1], &tnorm);
    angle0 = GetAngle(&c1, &rp->verts[nv0 + prev1], &tverts[(prev0 + 1) % nv0], &tnorm);

    if (angle1 < angle0) {
      prev1 = (prev1 + 1) % nv1;
      fp->face_verts[2] = nv0 + prev1;
    } else
      prev0 = fp->face_verts[2] = (prev0 + 1) % nv0;
  }

  // Set the two end faces for the room
  InitRoomFace(&rp->faces[nfaces - 2], fp0->num_verts);
  for (i = 0; i < fp0->num_verts; i++)
    rp->faces[nfaces - 2].face_verts[i] = nv0 - 1 - i;

  InitRoomFace(&rp->faces[nfaces - 1], fp1->num_verts);
  for (i = 0; i < fp1->num_verts; i++)
    rp->faces[nfaces - 1].face_verts[i] = fp0->num_verts + i;

  // Set normals & textures for each face
  for (i = 0; i < nfaces; i++) {
    if (!ComputeFaceNormal(rp, i))
      Int3(); // Bad!  Get Matt!
  }

  // Attempt to combine faces
  for (i = 0; i < nfaces - 2; i++) {
    int t = (i + 1) % (nfaces - 2);

    // Try compbining the faces
    if (CombineFaces(rp, i, t)) {
      i--; // check this face again
      nfaces--;
    }
  }

  // Set textures for each face
#ifndef NEWEDITOR
  for (i = 0; i < nfaces; i++)
    rp->faces[i].tmap = i + 1;
#else
  int texnum = Editor_state.GetCurrentTexture();
  for (i = 0; i < nfaces; i++) {
    // Apply texture
    HTextureApplyToRoomFace(rp, i, texnum);
  }
#endif

  // Set UVs for all the faces
  AssignDefaultUVsToRoom(rp);

  // Form the portals between the rooms
  LinkRoomsSimple(Rooms, ROOMNUM(rp0), facenum0, ROOMNUM(rp), nfaces - 2);
  LinkRoomsSimple(Rooms, ROOMNUM(rp1), facenum1, ROOMNUM(rp), nfaces - 1);

  // Make the new room the current room
#ifndef NEWEDITOR
  Curroomp = rp;
  Curface = nfaces - 2;
  Curedge = Curvert = 0;
  Curportal = -1;
#else
  theApp.m_pLevelWnd->SetPrim(rp, nfaces - 2, -1, 0, 0);
#endif

  // Set the flag
  World_changed = 1;
}

int16_t New_face_verts[MAX_VERTS_PER_FACE];
int New_face_num_verts = -1;
room *New_face_roomp;

void AddNewFaceVert() {
  if (Curroomp != New_face_roomp) {
    OutrageMessageBox("All points in the new face must be in the same room");
    return;
  }

  if (New_face_num_verts == -1) {
    OutrageMessageBox("You must start a new face before adding to it");
    return;
  }

  New_face_verts[New_face_num_verts++] = Curroomp->faces[Curface].face_verts[Curvert];

  EditorStatus("Added %d as vert #%d in new face", Curroomp->faces[Curface].face_verts[Curvert], New_face_num_verts);
}

void StartNewFace() {
  New_face_roomp = Curroomp;
  New_face_num_verts = 0;

  AddNewFaceVert();

  EditorStatus("Starting new face with vertex %d", Curroomp->faces[Curface].face_verts[Curvert]);
}

void EndNewFace() {
  AddNewFaceVert();

  if (New_face_num_verts == -1) {
    OutrageMessageBox("You must start a new face before ending it");
    return;
  }

  if (New_face_num_verts < 3) {
    OutrageMessageBox("A face must have at least three vertices");
    return;
  }

  int facenum;

  facenum = RoomAddFaces(Curroomp, 1);

  face *fp = &Curroomp->faces[facenum];

  InitRoomFace(fp, New_face_num_verts);

  for (int i = 0; i < New_face_num_verts; i++)
    fp->face_verts[i] = New_face_verts[i];

#ifndef NEWEDITOR
  fp->tmap = 0;
#else
  int texnum = Editor_state.GetCurrentTexture();
  // Apply texture
  HTextureApplyToRoomFace(Curroomp, facenum, texnum);
#endif

  ComputeFaceNormal(Curroomp, facenum);
  AssignDefaultUVsToRoomFace(Curroomp, facenum);

  EditorStatus("New face %d created", facenum);

  New_face_num_verts = -1;

  World_changed = 1;
}

void ObjRelink(int objnum, int newroomnum);
int RemoveDuplicatePoints(room *rp);
trigger *FindTrigger(int roomnum, int facenum);

// Combine the two rooms. base_rp stays, att_rp goes away
// Returns true if combine sucessful, false if can't join
bool CombineRooms(room *base_rp, room *att_rp) {
  int i;
  face *fp;
  bool connected = 0, rendered_portals = 0;

  // Make sure these rooms share a portal
  for (i = 0; i < base_rp->num_portals; i++) {
    if (base_rp->portals[i].croom == ROOMNUM(att_rp)) {
      connected = 1;
      if ((base_rp->faces[base_rp->portals[i].portal_face].flags & FF_HAS_TRIGGER) ||
          (att_rp->faces[att_rp->portals[base_rp->portals[i].cportal].portal_face].flags & FF_HAS_TRIGGER)) {
        OutrageMessageBox("Cannot combine rooms: a portal connecting them has a trigger.");
        return 0;
      }
      if ((base_rp->portals[i].flags & PF_RENDER_FACES) ||
          (att_rp->portals[base_rp->portals[i].cportal].flags & PF_RENDER_FACES)) {
        rendered_portals = 1;
      }
    }
  }

  if (!connected) {
    OutrageMessageBox("Cannot combine rooms: the rooms are not connected.");
    return 0;
  }

  if (rendered_portals) {
    if (OutrageMessageBox(
            MBOX_YESNO,
            "One or more of the portals connecting these rooms has the Render Faces flag set.  "
            "The rendered face will remain in the room after combine.\n\nDo you still want to combine?") != IDYES)
      return 0;
  }

  // Check for doors
  if (base_rp->doorway_data || att_rp->doorway_data) {
    OutrageMessageBox("Cannot combine rooms: one room is a door.");
    return 0;
  }

  // Allocate new verts & faces
  int first_new_vert = RoomAddVertices(base_rp, att_rp->num_verts);
  int first_new_face = RoomAddFaces(base_rp, att_rp->num_faces);

  // Copy verts to base room
  for (i = 0; i < att_rp->num_verts; i++)
    base_rp->verts[first_new_vert + i] = att_rp->verts[i];

  // Copy faces to base room
  for (i = 0, fp = &base_rp->faces[first_new_face]; i < att_rp->num_faces; fp++, i++) {

    // Copy the face
    CopyFace(fp, &att_rp->faces[i]);
    fp->flags = att_rp->faces[i].flags;

    // Update the vertices
    for (int v = 0; v < fp->num_verts; v++)
      fp->face_verts[v] += first_new_vert;

    // Move triggers from attach to base room
    if (att_rp->faces[i].flags & FF_HAS_TRIGGER) {
      trigger *tp = FindTrigger(ROOMNUM(att_rp), i);
      tp->roomnum = ROOMNUM(base_rp);
      tp->facenum += first_new_face;
    }
  }

  // Deal with attach room's portals
  for (i = 0; i < att_rp->num_portals; i++) {
    int facenum, croom, cface, flags, cflags;

    facenum = att_rp->portals[i].portal_face + first_new_face;
    flags = att_rp->portals[i].flags;
    croom = att_rp->portals[i].croom;
    cface = Rooms[croom].portals[att_rp->portals[i].cportal].portal_face;
    cflags = Rooms[croom].portals[att_rp->portals[i].cportal].flags;

    // Delete old portal from attach room
    DeletePortalPair(att_rp, i);

    // If this portal attaches to the base room, mark the portal faces for deletion
    if (croom == ROOMNUM(base_rp)) {
      if (!(flags & PF_RENDER_FACES))
        base_rp->faces[facenum].tmap = -1; //-1 means to delete the face
      if (!(cflags & PF_RENDER_FACES))
        base_rp->faces[cface].tmap = -1; //-1 means to delete the face
    } else {                             // Attaches to third room, so make attachment to base room
      LinkRooms(Rooms, ROOMNUM(base_rp), facenum, croom, cface);
      portal *pp = &base_rp->portals[base_rp->num_portals - 1];
      pp->flags |= (flags & (PF_RENDER_FACES + PF_RENDERED_FLYTHROUGH));
      Rooms[pp->croom].portals[pp->cportal].flags |= (cflags * (PF_RENDER_FACES + PF_RENDERED_FLYTHROUGH));
    }

    i--; // adjust for new portal numbering
  }

  // Delete obsolete faces
  for (i = 0; i < base_rp->num_faces; i++)
    if (base_rp->faces[i].tmap == -1) {
      DeleteRoomFace(base_rp, i);
      i--;
    }

  // Link objects to new room
  while (att_rp->objects != -1)
    ObjRelink(att_rp->objects, ROOMNUM(base_rp));

  // Delete the old room
  DeleteRoomFromMine(att_rp);

  // Get rid of duplicate verts
  RemoveDuplicatePoints(base_rp);

  // Done!
  World_changed = 1;
  return 1;
}

// Creates a an external room and links the specified faces to it
// Parameters:	rp - the room to connect to the new room
//					nfaces - how many faces connect to the new room (this becomes the number of
//portals) 					facenums - the list of faces to connect
void LinkToExternalRoom(room *rp, int nfaces, int *facenums) {
  int vert_xlate[MAX_VERTS_PER_ROOM];
  int v, i, nverts = 0;

  for (i = 0; i < rp->num_verts; i++)
    vert_xlate[i] = -1;

  for (i = 0; i < nfaces; i++) {
    face *fp = &rp->faces[facenums[i]];

    for (v = 0; v < fp->num_verts; v++) {
      int vertnum = fp->face_verts[v];
      if (vert_xlate[vertnum] == -1)
        vert_xlate[vertnum] = nverts++;
    }
  }

  room *newrp = CreateNewRoom(nverts, nfaces);

  newrp->flags |= RF_EXTERNAL;

  for (i = 0; i < rp->num_verts; i++) {
    if (vert_xlate[i] != -1)
      newrp->verts[vert_xlate[i]] = rp->verts[i];
  }

  for (i = 0; i < nfaces; i++) {
    face *newfp = &newrp->faces[i], *fp = &rp->faces[facenums[i]];

    InitRoomFace(newfp, fp->num_verts);

    for (v = 0; v < fp->num_verts; v++)
      newfp->face_verts[v] = vert_xlate[fp->face_verts[fp->num_verts - v - 1]];

#ifndef NEWEDITOR
    newfp->tmap = fp->tmap;
#else
    // Apply texture
    HTextureApplyToRoomFace(newrp, i, newfp->tmap);
#endif

    if (!ComputeFaceNormal(newrp, i))
      Int3();

    LinkRooms(Rooms, ROOMNUM(rp), facenums[i], ROOMNUM(newrp), i);
  }

  AssignDefaultUVsToRoom(newrp);

  World_changed = 1;
}

extern bool Disable_editor_rendering;

// Splits a face into two pieces
// Parameters:	rp,facenum - the face to split
//					v0,v1 - the two verts that will form the new edge
void SplitFace(room *rp, int facenum, int v0, int v1) {
  face *fp = &rp->faces[facenum], *nfp;
  int first_new_face, i;

  if (v0 == v1) {
    OutrageMessageBox("You cannot split a face using the same point twice.");
    return;
  }

  if (v1 < v0) {
    int t = v1;
    v1 = v0;
    v0 = t;
  }

  if (v1 == (v0 + 1) % fp->num_verts) {
    OutrageMessageBox("You cannot split a face using two adjacent points.");
    return;
  }

  if (fp->num_verts == 3) {
    OutrageMessageBox("Face %d has three verts and cannot be split.", facenum);
    return;
  }

  if (fp->portal_num != -1) {
    OutrageMessageBox("You cannot split a face that's part of a portal.");
    return;
  }

  if (fp->flags & (FF_HAS_TRIGGER + FF_FLOATING_TRIG)) {
    OutrageMessageBox("You cannot split a face that has a trigger.");
    return;
  }

  Disable_editor_rendering = 1;

  // Add new faces
  first_new_face = RoomAddFaces(rp, 2);

  // Reset pointer to our old face
  fp = &rp->faces[facenum];

  // Set up each face
  bool ok[2];
  for (i = 0, nfp = &rp->faces[first_new_face]; i < 2; i++, nfp++) {
    int nv, startv;

    if (i == 0) {
      nv = v1 - v0 + 1;
      startv = v0;
    } else {
      nv = fp->num_verts - (v1 - v0) + 1;
      startv = v1;
    }

    InitRoomFace(nfp, nv);

    // Set verts & uvls for this face
    for (int v = 0; v < nv; v++) {
      nfp->face_verts[v] = fp->face_verts[(startv + v) % fp->num_verts];
      nfp->face_uvls[v] = fp->face_uvls[(startv + v) % fp->num_verts];
    }

    // Copy other data for this face
    CopyFaceFlags(nfp, fp);
#ifndef NEWEDITOR
    nfp->tmap = fp->tmap;
#else
    // Apply texture
    HTextureApplyToRoomFace(rp, first_new_face + i, fp->tmap);
#endif
    nfp->light_multiple = fp->light_multiple;

    // Compute the normal for this face
    ok[i] = ComputeFaceNormal(rp, first_new_face + i);
  }

  // Bail if either face is degenerate
  if (!ok[0] || !ok[1]) {
    OutrageMessageBox("Cannot form face here: one new face is degenerate.");
    DeleteRoomFace(rp, first_new_face);
    DeleteRoomFace(rp, first_new_face + 1);
    Disable_editor_rendering = 0;
    return;
  }

  // Delete original face
  DeleteRoomFace(rp, facenum);

  // We're done
  EditorStatus("Face %d replaced by new faces %d & %d", facenum, first_new_face - 1, first_new_face);

  Disable_editor_rendering = 0;

#ifndef NEWEDITOR
  World_changed = 1;
#endif
}

// Adds a point to an edge
void SplitEdge(room *rp, int facenum, int edgenum, float position) {
  face *fp = &rp->faces[facenum];
  int vert1 = (edgenum + 1) % fp->num_verts;
  vector *v0, *v1, newv;

  v0 = &rp->verts[fp->face_verts[edgenum]];
  v1 = &rp->verts[fp->face_verts[vert1]];

  newv = *v0 + (*v1 - *v0) * position;

  AddPointToAllEdges(rp, fp->face_verts[edgenum], fp->face_verts[vert1], &newv);

  World_changed = 1;
}

#define MIN_NORMAL_MAG 0.035

int FindConnectedFace(room *rp, int facenum, int edgenum, int startface);

// Deletes a vertex from an face
// Only deletes if the two adjacent edges are colinear, and there isn't a T-joint at this point
void DeleteVert(room *rp, int facenum, int vertnum) {
  face *fp = &rp->faces[facenum];
  vector normal;
  float mag;
  int f0, f1;
  int prev_vertnum = (vertnum + fp->num_verts - 1) % fp->num_verts;

  mag = vm_GetNormal(&normal, &rp->verts[fp->face_verts[prev_vertnum]], &rp->verts[fp->face_verts[vertnum]],
                     &rp->verts[fp->face_verts[(vertnum + 1) % fp->num_verts]]);

  if (mag >= MIN_NORMAL_MAG) {
    OutrageMessageBox("Can't remove point: edges aren't colinear.");
    return;
  }

  // Make sure there isn't a t-joint at this face
  f0 = FindConnectedFace(rp, facenum, prev_vertnum, 0);
  f1 = FindConnectedFace(rp, facenum, vertnum, 0);
  if ((f0 != -1) && (f1 != -1) && (f0 != f1)) {
    OutrageMessageBox("Can't delete vertex: apparent T-joint with faces %d & %d\n", f0, f1);
    return;
  }

  // Delete the point on our passed face
  DeletePointFromFace(rp, facenum, vertnum);

  // Delete the point on the connected face
  face *fp2 = &rp->faces[f0];
  for (int v2 = 0; v2 < fp2->num_verts; v2++)
    if (fp2->face_verts[v2] == fp->face_verts[vertnum])
      break;
  ASSERT(v2 < fp2->num_verts);
  DeletePointFromFace(rp, f0, v2);

#ifndef NEWEDITOR
  World_changed = 1;
#endif
}

// Moves a vertex along its two adjacent edges
// Only moves if the two adjacent edges are colinear
void MoveVert(room *rp, int facenum, int vertnum, float new_position) {
  face *fp = &rp->faces[facenum];
  vector normal;
  float mag;
  int prev_vertnum = (vertnum + fp->num_verts - 1) % fp->num_verts;
  int next_vertnum = (vertnum + 1) % fp->num_verts;
  vector *v0, *v1, newv;

  mag = vm_GetNormal(&normal, &rp->verts[fp->face_verts[prev_vertnum]], &rp->verts[fp->face_verts[vertnum]],
                     &rp->verts[fp->face_verts[next_vertnum]]);

  if (mag >= MIN_NORMAL_MAG) {
    OutrageMessageBox("Can't move point: edges aren't colinear.");
    return;
  }

  v0 = &rp->verts[fp->face_verts[prev_vertnum]];
  v1 = &rp->verts[fp->face_verts[next_vertnum]];

  newv = *v0 + (*v1 - *v0) * new_position;

  rp->verts[fp->face_verts[vertnum]] = newv;

  World_changed = 1;
}

#include "polymodel.h"

// Incorporates the geometry from the specified object into the specified room.  Deletes the object.
// Returns true if worked, false if some error
bool MergeObjectIntoRoom(room *rp, int objnum) {
  object *objp = &Objects[objnum];
  ASSERT(objp->render_type == RT_POLYOBJ);

  poly_model *pm = GetPolymodelPointer(objp->rtype.pobj_info.model_num);

  if (pm->n_models > 1) {
#ifndef NEWEDITOR
    OutrageMessageBox("Cannot merge: Only supported for objects with one submodel.\n\nIf you really need this "
                      "functions for objects with multiple submodles, talk to MattT.");
#else
    OutrageMessageBox("Cannot merge: Only supported for objects with one submodel.");
#endif
    return 0;
  }
  if (!pm->n_ground) {
    OutrageMessageBox("Cannot merge: The object must have a ground plane to merge.");
    return 0;
  }
  if (OBJECT_OUTSIDE(objp)) {
    OutrageMessageBox("Cannot merge: The object is outside.");
    return 0;
  }
  if (objp->roomnum != ROOMNUM(rp)) {
    OutrageMessageBox("Cannot merge: The object is not in the specified room.");
    return 0;
  }

  // Get points to model data
  bsp_info *sm = &pm->submodel[0];

  // Add verts to room
  int first_new_vert = RoomAddVertices(rp, sm->nverts);

  // Rotate the points and copy them to the room
  matrix rotmat = ~objp->orient;
  for (int i = 0; i < sm->nverts; i++) {
    rp->verts[first_new_vert + i] = objp->pos + sm->verts[i] * rotmat;
  }

  // Add faces to the room
  int first_new_face = RoomAddFaces(rp, sm->num_faces);

  // Copy the faces
  for (i = 0; i < sm->num_faces; i++) {
    polyface *pfp = &sm->faces[i];
    face *fp = &rp->faces[first_new_face + i];

    InitRoomFace(fp, pfp->nverts);

    for (int v = 0; v < fp->num_verts; v++) {
      fp->face_verts[v] = first_new_vert + pfp->vertnums[v];
      fp->face_uvls[v].u = pfp->u[v];
      fp->face_uvls[v].v = pfp->v[v];
    }

#ifndef NEWEDITOR
    fp->tmap = pm->textures[pfp->texnum];
#else
    // Apply texture
    HTextureApplyToRoomFace(rp, first_new_face + i, pm->textures[pfp->texnum]);
#endif

    if (!ComputeFaceNormal(rp, first_new_face + i))
      Int3(); // Bad!  Get Matt!
  }

  // Kill the object
  ObjDelete(objnum);

  // Done!
  World_changed = 1;
  return 1;
}

void MarkFaceForDeletion(room *rp, int facenum) {
  face *fp = &rp->faces[facenum];

  // Mark this face
  fp->tmap = -1;

  // Find connect faces
  for (int e = 0; e < fp->num_verts; e++) {
    int t = -1;

    while ((t = FindConnectedFace(rp, facenum, e, t + 1)) != -1) {
      if (rp->faces[t].tmap != -1)
        MarkFaceForDeletion(rp, t);
    }
  }
}

// Delete all the faces connected to the specified face
void DeleteAllConnectedFaces(room *rp, int facenum) {
  // Make sure no faces using tmap of -1
  for (int f = 0; f < rp->num_faces; f++)
    ASSERT(rp->faces[f].tmap != -1);

  // Do recursive marking
  MarkFaceForDeletion(rp, facenum);

  // Now delete the faces
  for (f = 0; f < rp->num_faces;) {

    if (rp->faces[f].tmap == -1)
      DeleteRoomFace(rp, f);
    else
      f++;
  }

  // Done
  World_changed = 1;
}

// returns the number of faces changed
int PropagateToConnectedFacesSub(room *rp, int facenum, bool *face_checked) {
  int n_changed = 0;

  face_checked[facenum] = 1;

  face *fp = &rp->faces[facenum];

  for (int e = 0; e < fp->num_verts; e++) {
    int t = -1;

    do {

      t = FindConnectedFace(rp, facenum, e, t + 1);

      if ((t != -1) && !face_checked[t]) {

        if (NormalsAreSame(&fp->normal, &rp->faces[t].normal)) {

          HTexturePropagateToFace(rp, t, rp, facenum);

          n_changed++;

          n_changed += PropagateToConnectedFacesSub(rp, t, face_checked);
        }
      }

    } while (t != -1);
  }

  return n_changed;
}

void PropagateToConnectedFaces(room *rp, int facenum) {
  bool face_checked[MAX_FACES_PER_ROOM];
  int f, n_changed;

  for (f = 0; f < rp->num_faces; f++)
    face_checked[f] = 0;

  face_checked[facenum] = 1;

  n_changed = PropagateToConnectedFacesSub(rp, facenum, face_checked);

  EditorStatus("%d faces were retextured.", n_changed);

  if (n_changed)
    World_changed = 1;
}
