//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // //=============================================================================// #include "stdafx.h" #include "GameConfig.h" #include "GlobalFunctions.h" #include "History.h" #include "MainFrm.h" #include "MapCheckDlg.h" #include "MapDoc.h" #include "MapEntity.h" #include "MapSolid.h" #include "MapWorld.h" #include "Options.h" #include "ToolManager.h" #include "VisGroup.h" #include "hammer.h" #include "MapOverlay.h" #include "Selection.h" // memdbgon must be the last include file in a .cpp file!!! #include // ******** // NOTE: Make sure the order matches g_MapErrorStringIDs below! // ******** typedef enum { ErrorNoPlayerStart, ErrorMixedFace, ErrorDuplicatePlanes, ErrorMissingTarget, ErrorInvalidTexture, ErrorSolidStructure, ErrorUnusedKeyvalues, ErrorEmptyEntity, ErrorDuplicateKeys, ErrorSolidContents, ErrorInvalidTextureAxes, ErrorDuplicateFaceIDs, ErrorDuplicateNodeIDs, ErrorBadConnections, ErrorHiddenGroupHiddenChildren, ErrorHiddenGroupVisibleChildren, ErrorHiddenGroupMixedChildren, ErrorHiddenObjectNoVisGroup, ErrorHiddenChildOfEntity, ErrorIllegallyHiddenObject, ErrorKillInputRaceCondition, ErrorOverlayFaceList, } MapErrorType; // ******** // NOTE: Make sure the order matches MapErrorType above! // ******** struct { int m_StrResourceID; int m_DescriptionResourceID; } g_MapErrorStrings[] = { {IDS_NOPLAYERSTART, IDS_NOPLAYERSTART_DESC}, {IDS_MIXEDFACES, IDS_MIXEDFACES_DESC}, {IDS_DUPLICATEPLANES, IDS_DUPLICATEPLANES_DESC}, {IDS_UNMATCHEDTARGET, IDS_UNMATCHEDTARGET_DESC}, {IDS_INVALIDTEXTURE, IDS_INVALIDTEXTURE_DESC}, {IDS_SOLIDSTRUCTURE, IDS_SOLIDSTRUCTURE_DESC}, {IDS_UNUSEDKEYVALUES, IDS_UNUSEDKEYVALUES_DESC}, {IDS_EMPTYENTITY, IDS_EMPTYENTITY_DESC}, {IDS_DUPLICATEKEYS, IDS_DUPLICATEKEYS_DESC}, {IDS_SOLIDCONTENT, IDS_SOLIDCONTENT_DESC}, {IDS_INVALIDTEXTUREAXES, IDS_INVALIDTEXTUREAXES_DESC}, {IDS_DUPLICATEFACEID, IDS_DUPLICATEFACEID_DESC}, {IDS_DUPLICATE_NODE_ID, IDS_DUPLICATE_NODE_ID_DESC}, {IDS_BAD_CONNECTIONS, IDS_BAD_CONNECTIONS_DESC}, {IDS_HIDDEN_GROUP_HIDDEN_CHILDREN, IDS_HIDDEN_GROUP_HIDDEN_CHILDREN_DESC}, {IDS_HIDDEN_GROUP_VISIBLE_CHILDREN, IDS_HIDDEN_GROUP_VISIBLE_CHILDREN_DESC}, {IDS_HIDDEN_GROUP_MIXED_CHILDREN, IDS_HIDDEN_GROUP_MIXED_CHILDREN_DESC}, {IDS_HIDDEN_NO_VISGROUP, IDS_HIDDEN_NO_VISGROUP_DESC}, {IDS_HIDDEN_CHILD_OF_ENTITY, IDS_HIDDEN_CHILD_OF_ENTITY_DESC}, {IDS_HIDDEN_ILLEGALLY, IDS_HIDDEN_ILLEGALLY_DESC}, {IDS_KILL_INPUT_RACE_CONDITION, IDS_KILL_INPUT_RACE_CONDITION_DESC}, {IDS_BAD_OVERLAY, IDS_DAB_OVERLAY_DESC} }; typedef enum { CantFix, NeedsFix, Fixed, } FIXCODE; struct MapError { CMapClass *pObjects[3]; MapErrorType Type; DWORD dwExtra; FIXCODE Fix; }; // // Fix functions. // static void FixDuplicatePlanes(MapError *pError); static void FixSolidStructure(MapError *pError); static void FixInvalidTexture(MapError *pError); static void FixInvalidTextureAxes(MapError *pError); static void FixUnusedKeyvalues(MapError *pError); static void FixEmptyEntity(MapError *pError); static void FixBadConnections(MapError *pError); static void FixInvalidContents(MapError *pError); static void FixDuplicateFaceIDs(MapError *pError); static void FixDuplicateNodeIDs(MapError *pError); static void FixMissingTarget(MapError *pError); void FixHiddenObject(MapError *pError); static void FixKillInputRaceCondition(MapError *pError); static void FixOverlayFaceList(MapError *pError); CMapCheckDlg *s_pDlg = NULL; BEGIN_MESSAGE_MAP(CMapCheckDlg, CDialog) //{{AFX_MSG_MAP(CMapCheckDlg) ON_BN_CLICKED(IDC_GO, OnGo) ON_LBN_SELCHANGE(IDC_ERRORS, OnSelchangeErrors) ON_LBN_DBLCLK(IDC_ERRORS, OnDblClkErrors) ON_WM_PAINT() ON_BN_CLICKED(IDC_FIX, OnFix) ON_BN_CLICKED(IDC_FIXALL, OnFixall) ON_WM_DESTROY() ON_WM_CLOSE() ON_BN_CLICKED(IDC_CHECK_VISIBLE_ONLY, OnCheckVisibleOnly) //}}AFX_MSG_MAP END_MESSAGE_MAP() //----------------------------------------------------------------------------- // Visibility check //----------------------------------------------------------------------------- inline bool IsCheckVisible( CMapClass *pClass ) { return (Options.general.bCheckVisibleMapErrors == FALSE) || pClass->IsVisible(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CMapCheckDlg::CheckForProblems(CWnd *pwndParent) { if (!s_pDlg) { s_pDlg = new CMapCheckDlg; s_pDlg->Create(IDD, pwndParent); } if (!s_pDlg->DoCheck()) { // Found problems. s_pDlg->ShowWindow(SW_SHOW); } } //----------------------------------------------------------------------------- // Purpose: // Input : pParent - //----------------------------------------------------------------------------- CMapCheckDlg::CMapCheckDlg(CWnd *pParent) : CDialog(CMapCheckDlg::IDD, pParent) { //{{AFX_DATA_INIT(CMapCheckDlg) m_bCheckVisible = FALSE; //}}AFX_DATA_INIT m_bCheckVisible = Options.general.bCheckVisibleMapErrors; } //----------------------------------------------------------------------------- // Purpose: // Input : pDX - //----------------------------------------------------------------------------- void CMapCheckDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CMapCheckDlg) DDX_Control(pDX, IDC_FIXALL, m_cFixAll); DDX_Control(pDX, IDC_FIX, m_Fix); DDX_Control(pDX, IDC_GO, m_Go); DDX_Control(pDX, IDC_DESCRIPTION, m_Description); DDX_Control(pDX, IDC_ERRORS, m_Errors); DDX_Check(pDX, IDC_CHECK_VISIBLE_ONLY, m_bCheckVisible); //}}AFX_DATA_MAP if ( pDX->m_bSaveAndValidate ) { Options.general.bCheckVisibleMapErrors = m_bCheckVisible; } } //----------------------------------------------------------------------------- // Checkbox indicating whether we should check visible errors //----------------------------------------------------------------------------- void CMapCheckDlg::OnCheckVisibleOnly() { UpdateData( TRUE ); DoCheck(); } //----------------------------------------------------------------------------- // Purpose: Selects the current error objects and centers the views on it. //----------------------------------------------------------------------------- void CMapCheckDlg::OnGo() { GotoSelectedErrors(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CMapCheckDlg::GotoSelectedErrors() { int iSel = m_Errors.GetCurSel(); if (iSel == LB_ERR) { return; } ToolManager()->SetTool(TOOL_POINTER); CMapObjectList Objects; for (int i = 0; i < m_Errors.GetCount(); i++) { if (m_Errors.GetSel(i) > 0) { MapError *pError = (MapError *)m_Errors.GetItemDataPtr(i); if (pError) { Objects.AddToTail(pError->pObjects[0]); } } } CMapDoc *pDoc = CMapDoc::GetActiveMapDoc(); pDoc->SelectObjectList(&Objects); pDoc->CenterViewsOnSelection(); } //----------------------------------------------------------------------------- // Purpose: Fixes all the selected errors. //----------------------------------------------------------------------------- void CMapCheckDlg::OnFix() { int iSel = m_Errors.GetCurSel(); if (iSel == LB_ERR) { return; } UpdateBox ub; CMapObjectList Objects; ub.Objects = &Objects; for (int i = 0; i < m_Errors.GetCount(); i++) { if (m_Errors.GetSel(i) > 0) { MapError *pError = (MapError *)m_Errors.GetItemDataPtr(i); if (pError) { Fix(pError, ub); } } } OnSelchangeErrors(); CMapDoc::GetActiveMapDoc()->UpdateAllViews( MAPVIEW_UPDATE_OBJECTS ); } //----------------------------------------------------------------------------- // Purpose: // Input : pError - // ub - //----------------------------------------------------------------------------- void CMapCheckDlg::Fix(MapError *pError, UpdateBox &ub) { CMapDoc::GetActiveMapDoc()->SetModifiedFlag(); if (pError->Fix != NeedsFix) { // should never get here because this button is supposed // to be disabled if the error cannot be fixed return; } // // Expand the bounds of the update region to include the broken objects. // for (int i = 0; i < 2; i++) { if (!pError->pObjects[i]) { continue; } ub.Objects->AddToTail(pError->pObjects[i]); Vector mins; Vector maxs; pError->pObjects[i]->GetRender2DBox(mins, maxs); ub.Box.UpdateBounds(mins, maxs); } // // Perform the fix. // switch (pError->Type) { case ErrorDuplicatePlanes: { FixDuplicatePlanes(pError); break; } case ErrorDuplicateFaceIDs: { FixDuplicatePlanes(pError); break; } case ErrorDuplicateNodeIDs: { FixDuplicateNodeIDs(pError); break; } case ErrorMissingTarget: { FixMissingTarget(pError); break; } case ErrorSolidStructure: { FixSolidStructure(pError); break; } case ErrorSolidContents: { FixInvalidContents(pError); break; } case ErrorInvalidTexture: { FixInvalidTexture(pError); break; } case ErrorInvalidTextureAxes: { FixInvalidTextureAxes(pError); break; } case ErrorUnusedKeyvalues: { FixUnusedKeyvalues(pError); break; } case ErrorBadConnections: { FixBadConnections(pError); break; } case ErrorEmptyEntity: { FixEmptyEntity(pError); break; } case ErrorHiddenGroupVisibleChildren: case ErrorHiddenGroupMixedChildren: case ErrorHiddenGroupHiddenChildren: case ErrorHiddenObjectNoVisGroup: case ErrorHiddenChildOfEntity: case ErrorIllegallyHiddenObject: { FixHiddenObject(pError); break; } case ErrorKillInputRaceCondition: { FixKillInputRaceCondition(pError); break; } case ErrorOverlayFaceList: { FixOverlayFaceList( pError ); break; } } pError->Fix = Fixed; // // Expand the bounds of the update region to include the fixed objects. // for (int i = 0; i < 2; i++) { if (!pError->pObjects[i]) { continue; } Vector mins; Vector maxs; pError->pObjects[i]->GetRender2DBox(mins, maxs); ub.Box.UpdateBounds(mins, maxs); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CMapCheckDlg::OnFixall() { int iSel = m_Errors.GetCurSel(); if (iSel == LB_ERR) { return; } MapError *pError = (MapError *) m_Errors.GetItemDataPtr(iSel); if (pError->Fix == CantFix) { // should never get here because this button is supposed // to be disabled if the error cannot be fixed return; } UpdateBox ub; CMapObjectList Objects; ub.Objects = &Objects; // For every selected error... for (int i = 0; i < m_Errors.GetCount(); i++) { if (m_Errors.GetSel(i) > 0) { pError = (MapError *)m_Errors.GetItemDataPtr(i); if ((pError) && (pError->Fix == NeedsFix)) { // Find and fix every error of the same type. for (int j = 0; j < m_Errors.GetCount(); j++) { MapError *pError2 = (MapError *)m_Errors.GetItemDataPtr(j); if ((pError2->Type != pError->Type) || (pError2->Fix != NeedsFix)) { continue; } Fix(pError2, ub); } } } } OnSelchangeErrors(); CMapDoc::GetActiveMapDoc()->UpdateAllViews( MAPVIEW_UPDATE_OBJECTS ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CMapCheckDlg::OnSelchangeErrors() { // change description to match error int iSel = m_Errors.GetCurSel(); if(iSel == LB_ERR) { m_Fix.EnableWindow(FALSE); m_cFixAll.EnableWindow(FALSE); m_Go.EnableWindow(FALSE); } CString str; MapError *pError; pError = (MapError*) m_Errors.GetItemDataPtr(iSel); // Figure out which error string we're using. int iErrorStr = (int)pError->Type; iErrorStr = clamp( iErrorStr, 0, ARRAYSIZE( g_MapErrorStrings ) - 1 ); Assert( iErrorStr == (int)pError->Type ); str.LoadString(g_MapErrorStrings[iErrorStr].m_DescriptionResourceID); m_Description.SetWindowText(str); m_Go.EnableWindow(pError->pObjects[0] != NULL); // set state of fix button m_Fix.EnableWindow(pError->Fix == NeedsFix); m_cFixAll.EnableWindow(pError->Fix != CantFix); // set text of fix button switch (pError->Fix) { case NeedsFix: m_Fix.SetWindowText("&Fix"); break; case CantFix: m_Fix.SetWindowText("Can't fix"); break; case Fixed: m_Fix.SetWindowText("(fixed)"); break; } CMapDoc *pDoc = CMapDoc::GetActiveMapDoc(); pDoc->GetSelection()->SetMode(selectObjects); if (pError->pObjects[0]) { pDoc->SelectObject(pError->pObjects[0], scClear|scSelect|scSaveChanges ); } else { pDoc->SelectObject(NULL, scClear|scSaveChanges ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CMapCheckDlg::OnDblClkErrors() { GotoSelectedErrors(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CMapCheckDlg::OnPaint() { CPaintDC dc(this); // device context for painting } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CMapCheckDlg::KillErrorList() { // delete items in list.. their data ptrs are allocated objects int iSize = m_Errors.GetCount(); for(int i = 0; i < iSize; i++) { MapError *pError = (MapError*) m_Errors.GetItemDataPtr(i); delete pError; } m_Errors.ResetContent(); } //----------------------------------------------------------------------------- // Purpose: Builds the listbox string for the given error and adds it to the list. //----------------------------------------------------------------------------- static void AddErrorToListBox(CListBox *pList, MapError *pError) { CString str; // Figure out which error string we're using. int iErrorStr = (int)pError->Type; iErrorStr = clamp( iErrorStr, 0, ARRAYSIZE( g_MapErrorStrings ) - 1 ); Assert( iErrorStr == (int)pError->Type ); str.LoadString(g_MapErrorStrings[iErrorStr].m_StrResourceID); if (str.Find('%') != -1) { if (pError->Type == ErrorUnusedKeyvalues) { // dwExtra has the name of the string in it CString str2 = str; CMapEntity *pEntity = (CMapEntity *)pError->pObjects[0]; str.Format(str2, pEntity->GetClassName(), pError->dwExtra); } else { CString str2 = str; str.Format(str2, pError->dwExtra); } } int iIndex = pList->AddString(str); pList->SetItemDataPtr(iIndex, (PVOID)pError); } //----------------------------------------------------------------------------- // Purpose: // Input : pList - // Type - // dwExtra - // ... - //----------------------------------------------------------------------------- static void AddError(CListBox *pList, MapErrorType Type, DWORD dwExtra, ...) { MapError *pError = new MapError; memset(pError, 0, sizeof(MapError)); pError->Type = Type; pError->dwExtra = dwExtra; pError->Fix = CantFix; va_list vl; va_start(vl, dwExtra); // // Get the object pointer from the variable argument list. // switch (Type) { case ErrorNoPlayerStart: { // no objects. break; } case ErrorMixedFace: case ErrorMissingTarget: case ErrorDuplicatePlanes: case ErrorDuplicateFaceIDs: case ErrorDuplicateNodeIDs: case ErrorSolidStructure: case ErrorSolidContents: case ErrorInvalidTexture: case ErrorUnusedKeyvalues: case ErrorBadConnections: case ErrorEmptyEntity: case ErrorDuplicateKeys: case ErrorInvalidTextureAxes: case ErrorHiddenGroupHiddenChildren: case ErrorHiddenGroupVisibleChildren: case ErrorHiddenGroupMixedChildren: case ErrorHiddenObjectNoVisGroup: case ErrorHiddenChildOfEntity: case ErrorIllegallyHiddenObject: case ErrorOverlayFaceList: { pError->pObjects[0] = va_arg(vl, CMapClass *); break; } case ErrorKillInputRaceCondition: { pError->pObjects[0] = va_arg(vl, CMapClass *); pError->dwExtra = (DWORD)va_arg(vl, CEntityConnection *); break; } } // // Set the can fix flag. // switch (Type) { case ErrorSolidContents: case ErrorDuplicatePlanes: case ErrorDuplicateFaceIDs: case ErrorDuplicateNodeIDs: case ErrorSolidStructure: case ErrorInvalidTexture: case ErrorUnusedKeyvalues: case ErrorMissingTarget: case ErrorBadConnections: case ErrorEmptyEntity: case ErrorDuplicateKeys: case ErrorInvalidTextureAxes: case ErrorHiddenGroupHiddenChildren: case ErrorHiddenGroupVisibleChildren: case ErrorHiddenGroupMixedChildren: case ErrorHiddenObjectNoVisGroup: case ErrorHiddenChildOfEntity: case ErrorIllegallyHiddenObject: case ErrorKillInputRaceCondition: case ErrorOverlayFaceList: { pError->Fix = NeedsFix; break; } } va_end(vl); AddErrorToListBox(pList, pError); } //----------------------------------------------------------------------------- // Purpose: // Input : pObject - // DWORD - // Output : //----------------------------------------------------------------------------- static BOOL FindPlayer(CMapEntity *pObject, DWORD) { if ( !IsCheckVisible( pObject ) ) return TRUE; if (pObject->IsPlaceholder() && pObject->IsClass("info_player_start")) { return(FALSE); } return(TRUE); } //----------------------------------------------------------------------------- // Purpose: // Input : pList - // pWorld - //----------------------------------------------------------------------------- static void CheckRequirements(CListBox *pList, CMapWorld *pWorld) { // ensure there's a player start .. if (pWorld->EnumChildren((ENUMMAPCHILDRENPROC)FindPlayer, 0, MAPCLASS_TYPE(CMapEntity))) { // if rvl is !0, it was not stopped prematurely.. which means there is // NO player start. AddError(pList, ErrorNoPlayerStart, 0); } } //----------------------------------------------------------------------------- // Purpose: // Input : pSolid - // pList - // Output : //----------------------------------------------------------------------------- static BOOL _CheckMixedFaces(CMapSolid *pSolid, CListBox *pList) { if ( !IsCheckVisible( pSolid ) ) return TRUE; // run thru faces.. int iFaces = pSolid->GetFaceCount(); int iSolid = 2; // start off ambivalent int i; for(i = 0; i < iFaces; i++) { CMapFace *pFace = pSolid->GetFace(i); char ch = pFace->texture.texture[0]; if((ch == '*' && iSolid == 1) || (ch != '*' && iSolid == 0)) { break; } else iSolid = (ch == '*') ? 0 : 1; } if(i == iFaces) // all ok return TRUE; // NOT ok AddError(pList, ErrorMixedFace, 0, pSolid); return TRUE; } static void CheckMixedFaces(CListBox *pList, CMapWorld *pWorld) { pWorld->EnumChildren((ENUMMAPCHILDRENPROC)_CheckMixedFaces, (DWORD)pList, MAPCLASS_TYPE(CMapSolid)); } //----------------------------------------------------------------------------- // Purpose: Returns true if there is another node entity in the world with the // same node ID as the given entity. // Input : pNode - // pWorld - //----------------------------------------------------------------------------- bool FindDuplicateNodeID(CMapEntity *pNode, CMapWorld *pWorld) { if ( !IsCheckVisible( pNode ) ) return false; EnumChildrenPos_t pos; CMapClass *pChild = pWorld->GetFirstDescendent(pos); while (pChild != NULL) { CMapEntity *pEntity = dynamic_cast(pChild); if (pEntity && IsCheckVisible( pEntity ) && (pEntity != pNode) && pEntity->IsNodeClass()) { int nNodeID1 = pNode->GetNodeID(); int nNodeID2 = pEntity->GetNodeID(); if ((nNodeID1 != 0) && (nNodeID2 != 0) && (nNodeID1 == nNodeID2)) { return true; } } pChild = pWorld->GetNextDescendent(pos); } return false; } //----------------------------------------------------------------------------- // Purpose: Checks for node entities with the same node ID. //----------------------------------------------------------------------------- static void CheckDuplicateNodeIDs(CListBox *pList, CMapWorld *pWorld) { EnumChildrenPos_t pos; CMapClass *pChild = pWorld->GetFirstDescendent(pos); while (pChild != NULL) { CMapEntity *pEntity = dynamic_cast(pChild); if (pEntity && pEntity->IsNodeClass()) { if (FindDuplicateNodeID(pEntity, pWorld)) { AddError(pList, ErrorDuplicateNodeIDs, (DWORD)pWorld, pEntity); } } pChild = pWorld->GetNextDescendent(pos); } } //----------------------------------------------------------------------------- // Purpose: Checks for faces with identical face normals in this solid object. // Input : pSolid - Solid to check for duplicate faces. // Output : Returns TRUE if the face contains at least one duplicate face, // FALSE if the solid contains no duplicate faces. //----------------------------------------------------------------------------- BOOL DoesContainDuplicates(CMapSolid *pSolid) { int iFaces = pSolid->GetFaceCount(); for (int i = 0; i < iFaces; i++) { CMapFace *pFace = pSolid->GetFace(i); Vector& pts1 = pFace->plane.normal; for (int j = 0; j < iFaces; j++) { // Don't check self. if (j == i) { continue; } CMapFace *pFace2 = pSolid->GetFace(j); Vector& pts2 = pFace2->plane.normal; if (pts1 == pts2) { return(TRUE); } } } return(FALSE); } //----------------------------------------------------------------------------- // Purpose: // Input : pSolid - // pList - // Output : //----------------------------------------------------------------------------- static BOOL _CheckDuplicatePlanes(CMapSolid *pSolid, CListBox *pList) { if ( !IsCheckVisible( pSolid ) ) return TRUE; if (DoesContainDuplicates(pSolid)) { AddError(pList, ErrorDuplicatePlanes, 0, pSolid); } return(TRUE); } static void CheckDuplicatePlanes(CListBox *pList, CMapWorld *pWorld) { pWorld->EnumChildren((ENUMMAPCHILDRENPROC)_CheckDuplicatePlanes, (DWORD)pList, MAPCLASS_TYPE(CMapSolid)); } struct FindDuplicateFaceIDs_t { CMapFaceList All; // Collects all the face IDs in this map. CMapFaceList Duplicates; // Collects the duplicate face IDs in this map. }; //----------------------------------------------------------------------------- // Purpose: // Input : pSolid - // pData - // Output : Returns TRUE to continue enumerating. //----------------------------------------------------------------------------- static BOOL _CheckDuplicateFaceIDs(CMapSolid *pSolid, FindDuplicateFaceIDs_t *pData) { if ( !IsCheckVisible( pSolid ) ) return TRUE; int nFaceCount = pSolid->GetFaceCount(); for (int i = 0; i < nFaceCount; i++) { CMapFace *pFace = pSolid->GetFace(i); if (pData->All.FindFaceID(pFace->GetFaceID()) != -1) { if (pData->Duplicates.FindFaceID(pFace->GetFaceID()) != -1) { pData->Duplicates.AddToTail(pFace); } } else { pData->All.AddToTail(pFace); } } return(TRUE); } //----------------------------------------------------------------------------- // Purpose: Reports errors for all faces with duplicate face IDs. // Input : pList - // pWorld - //----------------------------------------------------------------------------- static void CheckDuplicateFaceIDs(CListBox *pList, CMapWorld *pWorld) { FindDuplicateFaceIDs_t Lists; Lists.All.SetGrowSize(128); Lists.Duplicates.SetGrowSize(128); pWorld->EnumChildren((ENUMMAPCHILDRENPROC)_CheckDuplicateFaceIDs, (DWORD)&Lists, MAPCLASS_TYPE(CMapSolid)); for (int i = 0; i < Lists.Duplicates.Count(); i++) { CMapFace *pFace = Lists.Duplicates.Element(i); AddError(pList, ErrorDuplicateFaceIDs, (DWORD)pFace, (CMapSolid *)pFace->GetParent()); } } //----------------------------------------------------------------------------- // Checks if a particular target is valid. //----------------------------------------------------------------------------- static void CheckValidTarget(CMapEntity *pEntity, const char *pFieldName, const char *pTargetName, CListBox *pList, bool bCheckClassNames) { if (!pTargetName) return; // These procedural names are always assumed to exist. if (!stricmp(pTargetName, "!activator") || !stricmp(pTargetName, "!caller") || !stricmp(pTargetName, "!player") || !stricmp(pTargetName, "!self")) return; CMapDoc *pDoc = CMapDoc::GetActiveMapDoc(); // Search by name first. CMapEntityList Found; bool bFound = pDoc->FindEntitiesByName(Found, pTargetName, (Options.general.bCheckVisibleMapErrors == TRUE)); if (!bFound && bCheckClassNames) { // Not found, search by classname. bFound = pDoc->FindEntitiesByClassName(Found, pTargetName, (Options.general.bCheckVisibleMapErrors == TRUE)); } if (!bFound) { // No dice, flag it as an error. AddError(pList, ErrorMissingTarget, (DWORD)pFieldName, pEntity); } } //----------------------------------------------------------------------------- // Purpose: Checks the given entity for references by name to nonexistent entities. // Input : pEntity - // pList - // Output : Returns TRUE to keep enumerating. //----------------------------------------------------------------------------- static BOOL _CheckMissingTargets(CMapEntity *pEntity, CListBox *pList) { if ( !IsCheckVisible( pEntity ) ) return TRUE; GDclass *pClass = pEntity->GetClass(); if (!pClass) { // Unknown class -- just check for target references. static char *pszTarget = "target"; const char *pszValue = pEntity->GetKeyValue(pszTarget); CheckValidTarget(pEntity, pszTarget, pszValue, pList, false); } else { // Known class -- check all target_destination and target_name_or_class keyvalues. for (int i = 0; i < pClass->GetVariableCount(); i++) { GDinputvariable *pVar = pClass->GetVariableAt(i); if ((pVar->GetType() != ivTargetDest) && (pVar->GetType() != ivTargetNameOrClass)) continue; const char *pszValue = pEntity->GetKeyValue(pVar->GetName()); CheckValidTarget(pEntity, pVar->GetName(), pszValue, pList, (pVar->GetType() == ivTargetNameOrClass)); } } return TRUE; } static void CheckMissingTargets(CListBox *pList, CMapWorld *pWorld) { pWorld->EnumChildren((ENUMMAPCHILDRENPROC)_CheckMissingTargets, (DWORD)pList, MAPCLASS_TYPE(CMapEntity)); } //----------------------------------------------------------------------------- // Purpose: Determines whether a solid is good or bad. // Input : pSolid - Solid to check. // pList - List box into which to place errors. // Output : Always returns TRUE to continue enumerating. //----------------------------------------------------------------------------- static BOOL _CheckSolidIntegrity(CMapSolid *pSolid, CListBox *pList) { if ( !IsCheckVisible( pSolid ) ) return TRUE; CCheckFaceInfo cfi; int nFaces = pSolid->GetFaceCount(); for (int i = 0; i < nFaces; i++) { CMapFace *pFace = pSolid->GetFace(i); // // Reset the iPoint member so results from previous faces don't carry over. // cfi.iPoint = -1; // // Check the face. // if (!pFace->CheckFace(&cfi)) { AddError(pList, ErrorSolidStructure, 0, pSolid); break; } } return(TRUE); } static void CheckSolidIntegrity(CListBox *pList, CMapWorld *pWorld) { pWorld->EnumChildren((ENUMMAPCHILDRENPROC)_CheckSolidIntegrity, (DWORD)pList, MAPCLASS_TYPE(CMapSolid)); } //----------------------------------------------------------------------------- // Purpose: // Input : pSolid - // pList - // Output : //----------------------------------------------------------------------------- static BOOL _CheckSolidContents(CMapSolid *pSolid, CListBox *pList) { if ( !IsCheckVisible( pSolid ) ) return TRUE; CCheckFaceInfo cfi; int nFaces = pSolid->GetFaceCount(); CMapFace *pFace = pSolid->GetFace(0); DWORD dwContents = pFace->texture.q2contents; for (int i = 1; i < nFaces; i++) { pFace = pSolid->GetFace(i); if (pFace->texture.q2contents == dwContents) { continue; } AddError(pList, ErrorSolidContents, 0, pSolid); break; } return TRUE; } static void CheckSolidContents(CListBox *pList, CMapWorld *pWorld) { if (CMapDoc::GetActiveMapDoc() && CMapDoc::GetActiveMapDoc()->GetGame() && CMapDoc::GetActiveMapDoc()->GetGame()->mapformat == mfQuake2) { pWorld->EnumChildren((ENUMMAPCHILDRENPROC)_CheckSolidContents, (DWORD)pList, MAPCLASS_TYPE(CMapSolid)); } } //----------------------------------------------------------------------------- // Purpose: Determines if there are any invalid textures or texture axes on any // face of this solid. Adds an error message to the list box for each // error found. // Input : pSolid - Solid to check. // pList - Pointer to the error list box. // Output : Returns TRUE. //----------------------------------------------------------------------------- static BOOL _CheckInvalidTextures(CMapSolid *pSolid, CListBox *pList) { if ( !IsCheckVisible( pSolid ) ) return TRUE; int nFaces = pSolid->GetFaceCount(); for(int i = 0; i < nFaces; i++) { const CMapFace *pFace = pSolid->GetFace(i); IEditorTexture *pTex = pFace->GetTexture(); if (pTex->IsDummy()) { AddError(pList, ErrorInvalidTexture, (DWORD)pFace->texture.texture, pSolid); return TRUE; } if (!pFace->IsTextureAxisValid()) { AddError(pList, ErrorInvalidTextureAxes, i, pSolid); return(TRUE); } } return(TRUE); } static void CheckInvalidTextures(CListBox *pList, CMapWorld *pWorld) { pWorld->EnumChildren((ENUMMAPCHILDRENPROC)_CheckInvalidTextures, (DWORD)pList, MAPCLASS_TYPE(CMapSolid)); } //----------------------------------------------------------------------------- // Purpose: // Input : pEntity - // pList - // Output : //----------------------------------------------------------------------------- static BOOL _CheckUnusedKeyvalues(CMapEntity *pEntity, CListBox *pList) { if ( !IsCheckVisible( pEntity ) ) return TRUE; if (!pEntity->IsClass() || pEntity->IsClass("multi_manager")) { return(TRUE); // can't check if no class associated } GDclass *pClass = pEntity->GetClass(); for (int i = pEntity->GetFirstKeyValue(); i != pEntity->GetInvalidKeyValue(); i=pEntity->GetNextKeyValue( i ) ) { if (pClass->VarForName(pEntity->GetKey(i)) == NULL) { AddError(pList, ErrorUnusedKeyvalues, (DWORD)pEntity->GetKey(i), pEntity); return(TRUE); } } return(TRUE); } static void CheckUnusedKeyvalues(CListBox *pList, CMapWorld *pWorld) { pWorld->EnumChildren((ENUMMAPCHILDRENPROC)_CheckUnusedKeyvalues, (DWORD)pList, MAPCLASS_TYPE(CMapEntity)); } //----------------------------------------------------------------------------- // Purpose: // Input : pEntity - // pList - // Output : //----------------------------------------------------------------------------- static BOOL _CheckEmptyEntities(CMapEntity *pEntity, CListBox *pList) { if ( !IsCheckVisible( pEntity ) ) return TRUE; if(!pEntity->IsPlaceholder() && !pEntity->GetChildCount()) { AddError(pList, ErrorEmptyEntity, (DWORD)pEntity->GetClassName(), pEntity); } return(TRUE); } static void CheckEmptyEntities(CListBox *pList, CMapWorld *pWorld) { pWorld->EnumChildren((ENUMMAPCHILDRENPROC)_CheckEmptyEntities, (DWORD)pList, MAPCLASS_TYPE(CMapEntity)); } //----------------------------------------------------------------------------- // Purpose: Checks the entity for bad I/O connections. // Input : pEntity - the entity to check // pList - list box that tracks the errors // Output : Returns TRUE to keep enumerating. //----------------------------------------------------------------------------- static BOOL _CheckBadConnections(CMapEntity *pEntity, CListBox *pList) { if ( !IsCheckVisible( pEntity ) ) return TRUE; if (CEntityConnection::ValidateOutputConnections(pEntity, (Options.general.bCheckVisibleMapErrors == TRUE)) == CONNECTION_BAD) { AddError(pList, ErrorBadConnections, (DWORD)pEntity->GetClassName(), pEntity); } // TODO: Check for a "Kill" input with the same output, target, and delay as another input. This // creates a race condition in the game where the order of arrival is not guaranteed //int nConnCount = pEntity->Connections_GetCount(); //for (int i = 0; i < nConnCount; i++) //{ // CEntityConnection *pConn = pEntity->Connections_Get(i); // if (!stricmp(pConn->GetInputName(), "kill")) // { // } //} return TRUE; } static void CheckBadConnections(CListBox *pList, CMapWorld *pWorld) { pWorld->EnumChildren((ENUMMAPCHILDRENPROC)_CheckBadConnections, (DWORD)pList, MAPCLASS_TYPE(CMapEntity)); } static bool HasVisGroupHiddenChildren(CMapClass *pObject) { const CMapObjectList *pChildren = pObject->GetChildren(); FOR_EACH_OBJ( *pChildren, pos ) { if (!pChildren->Element(pos)->IsVisGroupShown()) return true; } return false; } static bool HasVisGroupShownChildren(CMapClass *pObject) { const CMapObjectList *pChildren = pObject->GetChildren(); FOR_EACH_OBJ( *pChildren, pos ) { if (pChildren->Element(pos)->IsVisGroupShown()) return true; } return false; } //----------------------------------------------------------------------------- // Purpose: Makes sure that the visgroup assignments are valid. //----------------------------------------------------------------------------- static BOOL _CheckVisGroups(CMapClass *pObject, CListBox *pList) { CMapDoc *pDoc = CMapDoc::GetActiveMapDoc(); // dvs: FIXME: not working yet, revisit // Entities cannot have hidden children. //CMapEntity *pEntity = dynamic_cast(pObject); //if (pEntity && HasVisGroupHiddenChildren(pEntity)) //{ // AddError(pList, ErrorHiddenChildOfEntity, 0, pEntity); // return TRUE; //} // Check the validity of any object that claims to be hidden by visgroups. if (!pObject->IsVisGroupShown()) { // Groups cannot be hidden by visgroups. if (pObject->IsGroup()) { bool bHidden = HasVisGroupHiddenChildren(pObject); bool bVisible = HasVisGroupShownChildren(pObject); if (bHidden && !bVisible) { AddError(pList, ErrorHiddenGroupHiddenChildren, 0, pObject); } else if (!bHidden && bVisible) { AddError(pList, ErrorHiddenGroupVisibleChildren, 0, pObject); } else { AddError(pList, ErrorHiddenGroupMixedChildren, 0, pObject); } return TRUE; } // Check for unanticipated objects that are hidden but forbidden from visgroup membership. if (!pDoc->VisGroups_ObjectCanBelongToVisGroup(pObject)) { AddError(pList, ErrorIllegallyHiddenObject, 0, pObject); return TRUE; } // Hidden objects must belong to at least one visgroup. if (pObject->GetVisGroupCount() == 0) { AddError(pList, ErrorHiddenObjectNoVisGroup, 0, pObject); return TRUE; } } return TRUE; } static void CheckVisGroups(CListBox *pList, CMapWorld *pWorld) { pWorld->EnumChildrenRecurseGroupsOnly((ENUMMAPCHILDRENPROC)_CheckVisGroups, (DWORD)pList); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- static BOOL _CheckOverlayFaceList( CMapEntity *pEntity, CListBox *pList ) { if ( !IsCheckVisible( pEntity ) ) return TRUE; const CMapObjectList *pChildren = pEntity->GetChildren(); FOR_EACH_OBJ( *pChildren, pos ) { CMapOverlay *pOverlay = dynamic_cast( pChildren->Element(pos) ); if ( pOverlay ) { // Check to see if the overlay has assigned faces. if ( pOverlay->GetFaceCount() <= 0 ) { AddError( pList, ErrorOverlayFaceList, 0, pEntity ); return TRUE; } } } return TRUE; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- static void CheckOverlayFaceList( CListBox *pList, CMapWorld *pWorld ) { pWorld->EnumChildren( ( ENUMMAPCHILDRENPROC )_CheckOverlayFaceList, ( DWORD )pList, MAPCLASS_TYPE( CMapEntity )); } // // ** FIX FUNCTIONS // static void FixDuplicatePlanes(MapError *pError) { // duplicate planes in pObjects[0] // run thru faces.. CMapSolid *pSolid = (CMapSolid*) pError->pObjects[0]; ReStart: int iFaces = pSolid->GetFaceCount(); for(int i = 0; i < iFaces; i++) { CMapFace *pFace = pSolid->GetFace(i); Vector& pts1 = pFace->plane.normal; for (int j = 0; j < iFaces; j++) { // Don't check self if (j == i) { continue; } CMapFace *pFace2 = pSolid->GetFace(j); Vector& pts2 = pFace2->plane.normal; if (pts1 == pts2) { pSolid->DeleteFace(j); goto ReStart; } } } } //----------------------------------------------------------------------------- // Purpose: Repairs an invalid solid. // Input : pError - Contains information about the error. //----------------------------------------------------------------------------- static void FixSolidStructure(MapError *pError) { CMapSolid *pSolid = (CMapSolid *)pError->pObjects[0]; // // First make sure all the faces are good. // int nFaces = pSolid->GetFaceCount(); for (int i = nFaces - 1; i >= 0; i--) { CMapFace *pFace = pSolid->GetFace(i); if (!pFace->CheckFace(NULL)) { pFace->Fix(); } // // If the face has no points, just remove it from the solid. // if (pFace->GetPointCount() == 0) { pSolid->DeleteFace(i); } } // // Rebuild the solid from the planes. // pSolid->CreateFromPlanes(); pSolid->PostUpdate(Notify_Changed); } LPCTSTR GetDefaultTextureName(); // dvs: BAD! //----------------------------------------------------------------------------- // Purpose: Replaces any missing textures with the default texture. // Input : pError - //----------------------------------------------------------------------------- static void FixInvalidTexture(MapError *pError) { CMapSolid *pSolid = (CMapSolid *)pError->pObjects[0]; int nFaces = pSolid->GetFaceCount(); for (int i = 0; i < nFaces; i++) { CMapFace *pFace = pSolid->GetFace(i); if (pFace != NULL) { IEditorTexture *pTex = pFace->GetTexture(); if (pTex != NULL) { if (pTex->IsDummy()) { pFace->SetTexture(GetDefaultTextureName()); } } } } } static void FixInvalidTextureAxes(MapError *pError) { CMapSolid *pSolid = (CMapSolid *)pError->pObjects[0]; int nFaces = pSolid->GetFaceCount(); for (int i = 0; i < nFaces; i++) { CMapFace *pFace = pSolid->GetFace(i); if (!pFace->IsTextureAxisValid()) { pFace->InitializeTextureAxes(Options.GetTextureAlignment(), INIT_TEXTURE_FORCE | INIT_TEXTURE_AXES); } } } static void FixInvalidContents(MapError *pError) { CMapSolid *pSolid = (CMapSolid *)pError->pObjects[0]; CMapFace *pFace = pSolid->GetFace(0); DWORD dwContents = pFace->texture.q2contents; int nFaces = pSolid->GetFaceCount(); for (int i = 1; i < nFaces; i++) { pFace = pSolid->GetFace(i); pFace->texture.q2contents = dwContents; } } //----------------------------------------------------------------------------- // Purpose: Fixes duplicate face IDs by assigning the face a unique ID within // the world. // Input : pError - Holds the world and the face that is in error. //----------------------------------------------------------------------------- static void FixDuplicateFaceIDs(MapError *pError) { CMapWorld *pWorld = (CMapWorld *)pError->pObjects[0]; CMapFace *pFace = (CMapFace *)pError->dwExtra; pFace->SetFaceID(pWorld->FaceID_GetNext()); } //----------------------------------------------------------------------------- // Purpose: // Input : pError - //----------------------------------------------------------------------------- static void FixUnusedKeyvalues(MapError *pError) { CMapEntity *pEntity = (CMapEntity*) pError->pObjects[0]; GDclass *pClass = pEntity->GetClass(); if (!pClass) { return; } int iNext; for ( int i=pEntity->GetFirstKeyValue(); i != pEntity->GetInvalidKeyValue(); i = iNext ) { iNext = pEntity->GetNextKeyValue( i ); if (pClass->VarForName(pEntity->GetKey(i)) == NULL) { pEntity->DeleteKeyValue(pEntity->GetKey(i)); } } } //----------------------------------------------------------------------------- // Purpose: Removes any bad connections from the entity associated with the error. // Input : pError - //----------------------------------------------------------------------------- static void FixBadConnections(MapError *pError) { CMapEntity *pEntity = (CMapEntity *)pError->pObjects[0]; CEntityConnection::FixBadConnections(pEntity, (Options.general.bCheckVisibleMapErrors == TRUE)); } //----------------------------------------------------------------------------- // Purpose: Fixes a race condition caused by a Kill input being triggered at the // same instant as another input. // Input : pError - //----------------------------------------------------------------------------- static void FixKillInputRaceCondition(MapError *pError) { CEntityConnection *pConn = (CEntityConnection *)pError->pObjects[1]; // Delay the Kill command so that it arrives after the other command, // solving the race condition. pConn->SetDelay(pConn->GetDelay() + 0.01); } //----------------------------------------------------------------------------- // Purpose: // Input : pError - //----------------------------------------------------------------------------- static void FixOverlayFaceList( MapError *pError ) { CMapEntity *pEntity = static_cast( pError->pObjects[0] ); if ( !pEntity ) return; const CMapObjectList *pChildren = pEntity->GetChildren(); FOR_EACH_OBJ( *pChildren, pos ) { CMapOverlay *pOverlay = dynamic_cast( pChildren->Element(pos) ); if ( pOverlay ) { // Destroy itself. CMapDoc *pDoc = CMapDoc::GetActiveMapDoc(); pDoc->RemoveObjectFromWorld( pEntity, true ); GetHistory()->KeepForDestruction( pEntity ); return; } } } //----------------------------------------------------------------------------- // Purpose: // Input : pError - //----------------------------------------------------------------------------- static void FixEmptyEntity(MapError *pError) { CMapClass *pKillMe = pError->pObjects[0]; if (pKillMe->GetParent() != NULL) { GetHistory()->KeepForDestruction(pKillMe); pKillMe->GetParent()->RemoveChild(pKillMe); } } //----------------------------------------------------------------------------- // Purpose: Fixes duplicate node IDs by assigning the entity a unique node ID. // Input : pError - Holds the world and the entity that is in error. //----------------------------------------------------------------------------- static void FixDuplicateNodeIDs(MapError *pError) { CMapEntity *pEntity = (CMapEntity *)pError->pObjects[0]; pEntity->AssignNodeID(); } //----------------------------------------------------------------------------- // Purpose: Clears a bad target reference from the given entity. // Input : pError - //----------------------------------------------------------------------------- static void FixMissingTarget(MapError *pError) { CMapEntity *pEntity = (CMapEntity *)pError->pObjects[0]; const char *pszKey = (const char *)pError->dwExtra; pEntity->SetKeyValue(pszKey, NULL); } //----------------------------------------------------------------------------- // Purpose: Fix a an invalid visgroup state. This is either: // 1) A group that is hidden // 2) An object that is hidden but not in any visgroups //----------------------------------------------------------------------------- void FixHiddenObject(MapError *pError) { CMapClass *pObject = pError->pObjects[0]; // Tweak the object's visgroup state directly to avoid changing the // hidden/shown state of the object's children. pObject->m_bVisGroupShown = true; pObject->m_bVisGroupAutoShown = true; pObject->m_VisGroups.RemoveAll(); // Create a new visgroup to out the objects in (for hiding or inspection/deletion). CMapObjectList Objects; Objects.AddToTail(pObject); CMapDoc *pDoc = CMapDoc::GetActiveMapDoc(); if ((pError->Type == ErrorHiddenGroupHiddenChildren) || (pError->Type == ErrorHiddenObjectNoVisGroup)) { // The objects aren't in the compile, so just hide them. pDoc->VisGroups_CreateNamedVisGroup(Objects, "_hidden by Check for Problems", true, false); } else if (pError->Type == ErrorIllegallyHiddenObject) { // Do nothing, the object is now shown. } else { // ErrorHiddenGroupVisibleChildren // ErrorHiddenGroupMixedChildren // ErrorHiddenChildOfEntity // The objects either ARE in the compile, or they can't be hidden in a visgroup. // Don't hide them, just stick them in a visgroup for inspection pDoc->VisGroups_CreateNamedVisGroup(Objects, "found by Check for Problems", false, false); } } //----------------------------------------------------------------------------- // Purpose: Checks the map for problems. Returns true if the map is okay, // false if problems were found. //----------------------------------------------------------------------------- bool CMapCheckDlg::DoCheck(void) { CMapWorld *pWorld = GetActiveWorld(); // Clear error list KillErrorList(); // Map validation CheckRequirements(&m_Errors, pWorld); // Solid validation CheckMixedFaces(&m_Errors, pWorld); //CheckDuplicatePlanes(&m_Errors, pWorld); CheckDuplicateFaceIDs(&m_Errors, pWorld); CheckDuplicateNodeIDs(&m_Errors, pWorld); CheckSolidIntegrity(&m_Errors, pWorld); CheckSolidContents(&m_Errors, pWorld); CheckInvalidTextures(&m_Errors, pWorld); // Entity validation CheckUnusedKeyvalues(&m_Errors, pWorld); CheckEmptyEntities(&m_Errors, pWorld); CheckMissingTargets(&m_Errors, pWorld); CheckBadConnections(&m_Errors, pWorld); CheckVisGroups(&m_Errors, pWorld); CheckOverlayFaceList(&m_Errors, pWorld); if (!m_Errors.GetCount()) { AfxMessageBox("No errors were found."); EndDialog(IDOK); return true; } return false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CMapCheckDlg::OnOK() { DestroyWindow(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CMapCheckDlg::OnClose() { DestroyWindow(); } //----------------------------------------------------------------------------- // Purpose: Called when our window is being destroyed. //----------------------------------------------------------------------------- void CMapCheckDlg::OnDestroy() { delete this; s_pDlg = NULL; }