/* Copyright (C) 1999-2007 id Software, Inc. and contributors. For a list of contributors, see the accompanying CONTRIBUTORS file. This file is part of GtkRadiant. GtkRadiant 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 2 of the License, or (at your option) any later version. GtkRadiant 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 GtkRadiant; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ // // Linux stuff // // Leonardo Zide (leo@lokigames.com) // #include "stdafx.h" #include #include #include "gtkmisc.h" #include #if defined ( __linux__ ) || defined ( __APPLE__ ) #include #include #include #include #endif // for the logging part #include #include QEGlobals_t g_qeglobals; QEGlobals_GUI_t g_qeglobals_gui; // leo: Track memory allocations for debugging // NOTE TTimo this was never used and probably not relevant // there are tools to do that #ifdef MEM_DEBUG static GList *memblocks; void* debug_malloc( size_t size, const char* file, int line ){ void *buf = g_malloc( size + 8 ); *( (const char**)buf ) = file; buf = (char*)buf + 4; *( (int*)buf ) = line; buf = (char*)buf + 4; memblocks = g_list_append( memblocks, buf ); return buf; } void debug_free( void *buf, const char* file, int line ){ const char *f; int l; if ( g_list_find( memblocks, buf ) ) { memblocks = g_list_remove( memblocks, buf ); buf = (char*)buf - 4; l = *( (int*)buf ); buf = (char*)buf - 4; f = *( (const char**)buf ); Sys_FPrintf( SYS_DBG, "free: %s %d", file, line ); Sys_FPrintf( SYS_DBG, " allocated: %s %d\n", f, l ); g_free( buf ); } // else // free (buf); // from qmalloc, will leak unless we add this same hack to cmdlib } #endif vec_t Rad_rint( vec_t in ){ if ( g_PrefsDlg.m_bNoClamp ) { return in; } else{ return (float)floor( in + 0.5 ); } } void WINAPI QE_CheckOpenGLForErrors( void ){ char strMsg[1024]; int i = qglGetError(); if ( i != GL_NO_ERROR ) { if ( i == GL_OUT_OF_MEMORY ) { sprintf( strMsg, "OpenGL out of memory error %s\nDo you wish to save before exiting?", qgluErrorString( (GLenum)i ) ); if ( gtk_MessageBox( g_pParentWnd->m_pWidget, strMsg, "Radiant Error", MB_YESNO ) == IDYES ) { Map_SaveFile( NULL, false ); } _exit( 1 ); } else { Sys_Printf( "Warning: OpenGL Error %s\n", qgluErrorString( (GLenum)i ) ); } } } // NOTE: don't this function, use VFS instead char *ExpandReletivePath( char *p ){ static char temp[1024]; const char *base; if ( !p || !p[0] ) { return NULL; } if ( p[0] == '/' || p[0] == '\\' ) { return p; } base = ValueForKey( g_qeglobals.d_project_entity, "basepath" ); sprintf( temp, "%s/%s", base, p ); return temp; } char *copystring( char *s ){ char *b; b = (char*)malloc( strlen( s ) + 1 ); strcpy( b,s ); return b; } bool DoesFileExist( const char* pBuff, long& lSize ){ FileStream file; if ( file.Open( pBuff, "r" ) ) { lSize += file.GetLength(); file.Close(); return true; } return false; } void Map_Snapshot(){ CString strMsg; // I hope the modified flag is kept correctly up to date if ( !modified ) { return; } // we need to do the following // 1. make sure the snapshot directory exists (create it if it doesn't) // 2. find out what the lastest save is based on number // 3. inc that and save the map CString strOrgPath, strOrgFile; ExtractPath_and_Filename( currentmap, strOrgPath, strOrgFile ); AddSlash( strOrgPath ); strOrgPath += "snapshots"; bool bGo = true; struct stat Stat; if ( stat( strOrgPath, &Stat ) == -1 ) { #ifdef _WIN32 bGo = ( _mkdir( strOrgPath ) != -1 ); #endif #if defined ( __linux__ ) || defined ( __APPLE__ ) bGo = ( mkdir( strOrgPath,0755 ) != -1 ); #endif } AddSlash( strOrgPath ); if ( bGo ) { int nCount = 0; long lSize = 0; CString strNewPath; strNewPath = strOrgPath; strNewPath += strOrgFile; // QB - snapshots now follow the format: .. // **NOTE** atm snapshots must end with a .map (or .xmap) ext (this is why they were broken) CString strOldEXT = "map"; //default to .map const char* type = strrchr( strOrgFile.GetBuffer(),'.' ); if ( type != NULL ) { strOldEXT = ++type; }; // get the ext for later. StripExtension(strNewPath); // then strip it from the new path // CString strFile; while ( bGo ) { char buf[PATH_MAX]; //sprintf( buf, "%s.%i", strNewPath.GetBuffer(), nCount ); // snapshot will now end with a known ext. sprintf( buf, "%s.%i.%s", strNewPath.GetBuffer(), nCount, strOldEXT.GetBuffer() ); strFile = buf; bGo = DoesFileExist( strFile, lSize ); nCount++; } // strFile has the next available slot Map_SaveFile( strFile, false ); // it is still a modified map (we enter this only if this is a modified map) Sys_SetTitle( currentmap ); Sys_MarkMapModified(); if ( lSize > 12 * 1024 * 1024 ) { // total size of saves > 4 mb Sys_Printf( "The snapshot files in %s total more than 4 megabytes. You might consider cleaning up.", strOrgPath.GetBuffer() ); } } else { strMsg.Format( "Snapshot save failed.. unabled to create directory\n%s", strOrgPath.GetBuffer() ); gtk_MessageBox( g_pParentWnd->m_pWidget, strMsg ); } strOrgPath = ""; strOrgFile = ""; } /* =============== QE_CheckAutoSave If five minutes have passed since making a change and the map hasn't been saved, save it out. =============== */ void QE_CheckAutoSave( void ){ static time_t s_start; time_t now; time( &now ); if ( modified != 1 || !s_start ) { s_start = now; return; } if ( ( now - s_start ) > ( 60 * g_PrefsDlg.m_nAutoSave ) ) { if ( g_PrefsDlg.m_bAutoSave ) { CString strMsg; strMsg = g_PrefsDlg.m_bSnapShots ? "Autosaving snapshot..." : "Autosaving..."; Sys_Printf( strMsg ); Sys_Printf( "\n" ); Sys_Status( strMsg,0 ); // only snapshot if not working on a default map if ( g_PrefsDlg.m_bSnapShots && stricmp( currentmap, "unnamed.map" ) != 0 ) { Map_Snapshot(); } else { Map_SaveFile( ValueForKey( g_qeglobals.d_project_entity, "autosave" ), false ); } Sys_Status( "Autosaving...Saved.", 0 ); modified = 2; } else { Sys_Printf( "Autosave skipped...\n" ); Sys_Status( "Autosave skipped...", 0 ); } s_start = now; } } // NOTE TTimo we don't like that BuildShortPathName too much // the VFS provides a vfsCleanFileName which should perform the cleanup tasks // in the long run I'd like to completely get rid of this // used to be disabled, but caused problems // can't work with long win32 names until the BSP commands are not working differently #ifdef _WIN32 int BuildShortPathName( const char* pPath, char* pBuffer, int nBufferLen ){ char *pFile = NULL; int nResult = GetFullPathName( pPath, nBufferLen, pBuffer, &pFile ); nResult = GetShortPathName( pPath, pBuffer, nBufferLen ); if ( nResult == 0 ) { strcpy( pBuffer, pPath ); // Use long filename } return nResult; } #endif #if defined ( __linux__ ) || defined ( __APPLE__ ) int BuildShortPathName( const char* pPath, char* pBuffer, int nBufferLen ){ // remove /../ from directories const char *scr = pPath; char *dst = pBuffer; for ( int i = 0; ( i < nBufferLen ) && ( *scr != 0 ); i++ ) { if ( *scr == '/' && *( scr + 1 ) == '.' && *( scr + 2 ) == '.' ) { scr += 3; while ( dst != pBuffer && *( --dst ) != '/' ) { i--; } } *dst = *scr; scr++; dst++; } *dst = 0; return strlen( pBuffer ); } #endif /* const char *g_pPathFixups[]= { "basepath", "autosave", }; const int g_nPathFixupCount = sizeof(g_pPathFixups) / sizeof(const char*); void QE_CheckProjectEntity() { char *pFile; char pBuff[PATH_MAX]; char pNewPath[PATH_MAX]; for (int i = 0; i < g_nPathFixupCount; i++) { char *pPath = ValueForKey (g_qeglobals.d_project_entity, g_pPathFixups[i]); strcpy (pNewPath, pPath); if (pPath[0] != '\\' && pPath[0] != '/') if (GetFullPathName(pPath, PATH_MAX, pBuff, &pFile)) strcpy (pNewPath, pBuff); BuildShortPathName (pNewPath, pBuff, PATH_MAX); // check it's not ending with a filename seperator if (pBuff[strlen(pBuff)-1] == '/' || pBuff[strlen(pBuff)-1] == '\\') { Sys_FPrintf(SYS_WRN, "WARNING: \"%s\" path in the project file has an ending file seperator, fixing.\n", g_pPathFixups[i]); pBuff[strlen(pBuff)-1]=0; } SetKeyValue(g_qeglobals.d_project_entity, g_pPathFixups[i], pBuff); } } */ void HandleXMLError( void* ctxt, const char* text, ... ){ va_list argptr; static char buf[32768]; va_start( argptr,text ); vsprintf( buf, text, argptr ); Sys_FPrintf( SYS_ERR, "XML %s\n", buf ); va_end( argptr ); } #define DTD_BUFFER_LENGTH 1024 xmlDocPtr ParseXMLStream( IDataStream *stream, bool validate = false ){ xmlDocPtr doc = NULL; bool wellFormed = false, valid = false; int res, size = 1024; char chars[1024]; xmlParserCtxtPtr ctxt; //if(validate) // xmlDoValidityCheckingDefaultValue = 1; //else xmlDoValidityCheckingDefaultValue = 0; xmlSetGenericErrorFunc( NULL, HandleXMLError ); // SPoG // HACK: use AppPath to resolve DTD location // do a buffer-safe string copy and concatenate int i; char* w; const char* r; char buf[DTD_BUFFER_LENGTH]; w = buf; i = 0; // copy //assert(g_strAppPath.GetBuffer() != NULL); for ( r = g_strAppPath.GetBuffer(); i < DTD_BUFFER_LENGTH && *r != '\0'; i++, r++ ) w[i] = *r; // concatenate for ( r = "dtds/"; i < DTD_BUFFER_LENGTH && *r != '\0'; i++, r++ ) w[i] = *r; // terminate w[i] = '\0'; if ( i == DTD_BUFFER_LENGTH ) { HandleXMLError( NULL, "ERROR: buffer overflow: DTD path length too large\n" ); return NULL; } res = stream->Read( chars, 4 ); if ( res > 0 ) { ctxt = xmlCreatePushParserCtxt( NULL, NULL, chars, res, buf ); while ( ( res = stream->Read( chars, size ) ) > 0 ) { xmlParseChunk( ctxt, chars, res, 0 ); } xmlParseChunk( ctxt, chars, 0, 1 ); doc = ctxt->myDoc; wellFormed = ( ctxt->wellFormed == 1 ); valid = ( ctxt->valid == 1 ); xmlFreeParserCtxt( ctxt ); } if ( wellFormed && ( !validate || ( validate && valid ) ) ) { return doc; } if ( doc != NULL ) { xmlFreeDoc( doc ); } return NULL; } xmlDocPtr ParseXMLFile( const char* filename, bool validate = false ){ FileStream stream; if ( stream.Open( filename, "r" ) ) { return ParseXMLStream( &stream, validate ); } Sys_FPrintf( SYS_ERR, "Failed to open file: %s\n",filename ); return NULL; } // copy a string r to a buffer w // replace $string as appropriate void ReplaceTemplates( char* w, const char* r ){ const char *p; const char *__ENGINEPATH = "TEMPLATEenginepath"; const char *__USERHOMEPATH = "TEMPLATEuserhomepath"; const char *__TOOLSPATH = "TEMPLATEtoolspath"; const char *__BASEDIR = "TEMPLATEbasedir"; const char *__APPPATH = "TEMPLATEapppath"; const char *__Q3MAP2 = "TEMPLATEq3map2"; // iterate through string r while ( *r != '\0' ) { // check for special character if ( *r == '$' ) { if ( strncmp( r + 1, __ENGINEPATH, strlen( __ENGINEPATH ) ) == 0 ) { r += strlen( __ENGINEPATH ) + 1; p = g_pGameDescription->mEnginePath.GetBuffer(); } else if ( strncmp( r + 1, __USERHOMEPATH, strlen( __USERHOMEPATH ) ) == 0 ) { r += strlen( __USERHOMEPATH ) + 1; p = g_qeglobals.m_strHomeGame.GetBuffer(); } else if ( strncmp( r + 1, __BASEDIR, strlen( __BASEDIR ) ) == 0 ) { r += strlen( __BASEDIR ) + 1; p = g_pGameDescription->mBaseGame; } else if ( strncmp( r + 1, __TOOLSPATH, strlen( __TOOLSPATH ) ) == 0 ) { r += strlen( __TOOLSPATH ) + 1; p = g_strGameToolsPath.GetBuffer(); } else if ( strncmp( r + 1, __APPPATH, strlen( __APPPATH ) ) == 0 ) { r += strlen( __APPPATH ) + 1; p = g_strAppPath.GetBuffer(); } else if ( strncmp( r + 1, __Q3MAP2, strlen( __Q3MAP2 ) ) == 0 ) { r += strlen( __Q3MAP2 ) + 1; // see https://github.com/TTimo/GtkRadiant/issues/116 #ifdef _WIN32 if ( g_PrefsDlg.m_bx64q3map2 ) { p = "x64/q3map2"; } else #endif { p = "q3map2"; } } else { r++; p = "$"; } while ( *p != '\0' ) *w++ = *p++; } else{ *w++ = *r++; } } *w = '\0'; } /* Load up a project file to get the current version */ int QE_GetTemplateVersionForProject( const char * projectfile ) { xmlDocPtr doc; xmlNodePtr node, project; int ret; Sys_Printf( "Scanning template version in %s\n", projectfile ); doc = ParseXMLFile( projectfile, true ); if ( doc == NULL ) { Sys_FPrintf( SYS_ERR, "ERROR: XML parse failed %s\n", projectfile ); return 0; } node = doc->children; while ( node != NULL && node->type != XML_DTD_NODE ) { node = node->next; } if ( node == NULL || strcmp( (char*)node->name, "project" ) != 0 ) { Sys_FPrintf( SYS_ERR, "ERROR: invalid file type %s\n", projectfile ); xmlFree( doc ); return 0; } while ( node->type != XML_ELEMENT_NODE ) { node = node->next; } // project = node; for ( node = project->children; node != NULL; node = node->next ) { if ( node->type != XML_ELEMENT_NODE ) { continue; } if ( strcmp( (char*)node->properties->children->content, "template_version" ) == 0 ) { ret = atoi( (char*)node->properties->next->children->content ); xmlFreeDoc( doc ); return ret; } } Sys_FPrintf( SYS_WRN, "Version key not found in %s\n", projectfile ); xmlFreeDoc( doc ); return 0; } /* =========== QE_LoadProject NOTE: rather than bumping "version", consider bumping "template_version" (see above) NOTE: when QE_LoadProject is called, the prefs are updated with path to the latest project and saved on disk =========== */ bool QE_LoadProject( const char *projectfile ){ char buf[1024]; xmlDocPtr doc; xmlNodePtr node, project; Sys_Printf( "Loading project file: \"%s\"\n", projectfile ); doc = ParseXMLFile( projectfile, true ); if ( doc == NULL ) { return false; } node = doc->children; while ( node != NULL && node->type != XML_DTD_NODE ) node = node->next; if ( node == NULL || strcmp( (char*)node->name, "project" ) != 0 ) { Sys_FPrintf( SYS_ERR, "ERROR: invalid file type\n" ); return false; } while ( node->type != XML_ELEMENT_NODE ) node = node->next; // project = node; if ( g_qeglobals.d_project_entity != NULL ) { Entity_Free( g_qeglobals.d_project_entity ); } g_qeglobals.d_project_entity = Entity_Alloc(); for ( node = project->children; node != NULL; node = node->next ) { if ( node->type != XML_ELEMENT_NODE ) { continue; } // ReplaceTemplates( buf, (char*)node->properties->next->children->content ); SetKeyValue( g_qeglobals.d_project_entity, (char*)node->properties->children->content, buf ); } xmlFreeDoc( doc ); // project file version checking // add a version checking to avoid people loading later versions of the project file and bitching int ver = IntForKey( g_qeglobals.d_project_entity, "version" ); if ( ver > PROJECT_VERSION ) { char strMsg[1024]; sprintf( strMsg, "This is a version %d project file. This build only supports <=%d project files.\n" "Please choose another project file or upgrade your version of Radiant.", ver, PROJECT_VERSION ); gtk_MessageBox( g_pParentWnd->m_pWidget, strMsg, "Can't load project file", MB_ICONERROR | MB_OK ); // set the project file to nothing so we are sure we'll ask next time? g_PrefsDlg.m_strLastProject = ""; g_PrefsDlg.SavePrefs(); return false; } // set here some default project settings you need if ( strlen( ValueForKey( g_qeglobals.d_project_entity, "brush_primit" ) ) == 0 ) { SetKeyValue( g_qeglobals.d_project_entity, "brush_primit", "0" ); } g_qeglobals.m_bBrushPrimitMode = IntForKey( g_qeglobals.d_project_entity, "brush_primit" ); g_qeglobals.m_strHomeMaps = g_qeglobals.m_strHomeGame; const char* str = ValueForKey( g_qeglobals.d_project_entity, "gamename" ); if ( str[0] == '\0' ) { str = g_pGameDescription->mBaseGame.GetBuffer(); } g_qeglobals.m_strHomeMaps += str; g_qeglobals.m_strHomeMaps += '/'; // don't forget to create the dirs Q_mkdir( g_qeglobals.m_strHomeGame.GetBuffer(), 0775 ); Q_mkdir( g_qeglobals.m_strHomeMaps.GetBuffer(), 0775 ); // usefull for the log file and debuggin fucked up configurations from users: // output the basic information of the .qe4 project file // SPoG // all these paths should be unix format, with a trailing slash at the end // if not.. to debug, check that the project file paths are set up correctly Sys_Printf( "basepath : %s\n", ValueForKey( g_qeglobals.d_project_entity, "basepath" ) ); Sys_Printf( "entitypath : %s\n", ValueForKey( g_qeglobals.d_project_entity, "entitypath" ) ); // check whether user_project key exists.. // if not, save the current project under a new name if ( ValueForKey( g_qeglobals.d_project_entity, "user_project" )[0] == '\0' ) { Sys_Printf( "Loaded a template project file\n" ); // create the user_project key SetKeyValue( g_qeglobals.d_project_entity, "user_project", "1" ); if ( IntForKey( g_qeglobals.d_project_entity, "version" ) != PROJECT_VERSION ) { char strMsg[2048]; sprintf( strMsg, "The template project '%s' has version %d. The editor binary is configured for version %d.\n" "This indicates a problem in your setup.\n" "I will keep going with this project till you fix this", projectfile, IntForKey( g_qeglobals.d_project_entity, "version" ), PROJECT_VERSION ); gtk_MessageBox( g_pParentWnd->m_pWidget, strMsg, "Can't load project file", MB_ICONERROR | MB_OK ); } // create the writable project file path strcpy( buf, g_qeglobals.m_strHomeGame.GetBuffer() ); strcat( buf, g_pGameDescription->mBaseGame.GetBuffer() ); strcat( buf, "/scripts/" ); // while the filename is already in use, increment the number we add to the end int counter = 0; char pUser[PATH_MAX]; while ( 1 ) { sprintf( pUser, "%suser%d." PROJECT_FILETYPE, buf, counter ); counter++; if ( access( pUser, R_OK ) != 0 ) { // this is the one strcpy( buf, pUser ); break; } } // saving project will cause a save prefs g_PrefsDlg.m_strLastProject = buf; g_PrefsDlg.m_nLastProjectVer = IntForKey( g_qeglobals.d_project_entity, "version" ); QE_SaveProject( buf ); } else { // update preferences::LastProject with path of this successfully-loaded project // save preferences Sys_Printf( "Setting current project in prefs to \"%s\"\n", g_PrefsDlg.m_strLastProject.GetBuffer() ); g_PrefsDlg.m_strLastProject = projectfile; g_PrefsDlg.SavePrefs(); } return true; } /* =========== QE_SaveProject TTimo: whenever QE_SaveProject is called, prefs are updated and saved with the path to the project =========== */ qboolean QE_SaveProject( const char* filename ){ Sys_Printf( "Save project file '%s'\n", filename ); xmlNodePtr node; xmlDocPtr doc = xmlNewDoc( (xmlChar *)"1.0" ); // create DTD node xmlCreateIntSubset( doc, (xmlChar *)"project", NULL, (xmlChar *)"project.dtd" ); // create project node doc->children->next = xmlNewDocNode( doc, NULL, (xmlChar *)"project", NULL ); for ( epair_t* epair = g_qeglobals.d_project_entity->epairs; epair != NULL; epair = epair->next ) { node = xmlNewChild( doc->children->next, NULL, (xmlChar *)"key", NULL ); xmlSetProp( node, (xmlChar*)"name", (xmlChar*)epair->key ); xmlSetProp( node, (xmlChar*)"value", (xmlChar*)epair->value ); } CreateDirectoryPath( filename ); if ( xmlSaveFormatFile( filename, doc, 1 ) != -1 ) { xmlFreeDoc( doc ); Sys_Printf( "Setting current project in prefs to \"%s\"\n", filename ); g_PrefsDlg.m_strLastProject = filename; g_PrefsDlg.SavePrefs(); return TRUE; } else { xmlFreeDoc( doc ); Sys_FPrintf( SYS_ERR, "failed to save project file: \"%s\"\n", filename ); return FALSE; } } /* =========== QE_KeyDown =========== */ #define SPEED_MOVE 32 #define SPEED_TURN 22.5 /* =============== ConnectEntities Sets target / targetname on the two entities selected from the first selected to the secon =============== */ void ConnectEntities( void ){ entity_t *e1, *e2; const char *target; char *newtarg = NULL; if ( g_qeglobals.d_select_count != 2 ) { Sys_Status( "Must have two brushes selected", 0 ); Sys_Beep(); return; } e1 = g_qeglobals.d_select_order[0]->owner; e2 = g_qeglobals.d_select_order[1]->owner; if ( e1 == world_entity || e2 == world_entity ) { Sys_Status( "Can't connect to the world", 0 ); Sys_Beep(); return; } if ( e1 == e2 ) { Sys_Status( "Brushes are from same entity", 0 ); Sys_Beep(); return; } target = ValueForKey( e1, "target" ); if ( target && target[0] ) { newtarg = g_strdup( target ); } else { target = ValueForKey( e2, "targetname" ); if ( target && target[0] ) { newtarg = g_strdup( target ); } else{ Entity_Connect( e1, e2 ); } } if ( newtarg != NULL ) { SetKeyValue( e1, "target", newtarg ); SetKeyValue( e2, "targetname", newtarg ); g_free( newtarg ); } Sys_UpdateWindows( W_XY | W_CAMERA ); Select_Deselect(); Select_Brush( g_qeglobals.d_select_order[1] ); } qboolean QE_SingleBrush( bool bQuiet ){ if ( ( selected_brushes.next == &selected_brushes ) || ( selected_brushes.next->next != &selected_brushes ) ) { if ( !bQuiet ) { Sys_Printf( "Error: you must have a single brush selected\n" ); } return false; } if ( selected_brushes.next->owner->eclass->fixedsize ) { if ( !bQuiet ) { Sys_Printf( "Error: you cannot manipulate fixed size entities\n" ); } return false; } return true; } void QE_InitVFS( void ){ // VFS initialization ----------------------- // we will call vfsInitDirectory, giving the directories to look in (for files in pk3's and for standalone files) // we need to call in order, the mod ones first, then the base ones .. they will be searched in this order // *nix systems have a dual filesystem in ~/.q3a, which is searched first .. so we need to add that too Str directory,prefabs; // TTimo: let's leave this to HL mode for now if ( g_pGameDescription->mGameFile == "hl.game" ) { // Hydra: we search the "gametools" path first so that we can provide editor // specific pk3's wads and misc files for use by the editor. // the relevant map compiler tools will NOT use this directory, so this helps // to ensure that editor files are not used/required in release versions of maps // it also helps keep your editor files all in once place, with the editor modules, // plugins, scripts and config files. // it also helps when testing maps, as you'll know your files won't/can't be used // by the game engine itself. // directory = g_pGameDescription->mGameToolsPath; vfsInitDirectory( directory.GetBuffer() ); } // NOTE TTimo about the mymkdir calls .. this is a bit dirty, but a safe thing on *nix // if we have a mod dir if ( *ValueForKey( g_qeglobals.d_project_entity, "gamename" ) != '\0' ) { #if defined ( __linux__ ) || defined ( __APPLE__ ) // ~/./ directory = g_qeglobals.m_strHomeGame.GetBuffer(); Q_mkdir( directory.GetBuffer(), 0775 ); directory += ValueForKey( g_qeglobals.d_project_entity, "gamename" ); Q_mkdir( directory.GetBuffer(), 0775 ); vfsInitDirectory( directory.GetBuffer() ); AddSlash( directory ); prefabs = directory; // also create the maps dir, it will be used as prompt for load/save directory += "/maps"; Q_mkdir( directory, 0775 ); // and the prefabs dir prefabs += "/prefabs"; Q_mkdir( prefabs, 0775 ); #endif // / directory = g_pGameDescription->mEnginePath; directory += ValueForKey( g_qeglobals.d_project_entity, "gamename" ); Q_mkdir( directory.GetBuffer(), 0775 ); vfsInitDirectory( directory.GetBuffer() ); AddSlash( directory ); prefabs = directory; // also create the maps dir, it will be used as prompt for load/save directory += "/maps"; Q_mkdir( directory.GetBuffer(), 0775 ); // and the prefabs dir prefabs += "/prefabs"; Q_mkdir( prefabs, 0775 ); } #if defined ( __linux__ ) || defined ( __APPLE__ ) // ~/./ directory = g_qeglobals.m_strHomeGame.GetBuffer(); directory += g_pGameDescription->mBaseGame; vfsInitDirectory( directory.GetBuffer() ); #endif // / directory = g_pGameDescription->mEnginePath; directory += g_pGameDescription->mBaseGame; vfsInitDirectory( directory.GetBuffer() ); } void QE_Init( void ){ /* ** initialize variables */ g_qeglobals.d_gridsize = 8; g_qeglobals.d_showgrid = true; QE_InitVFS(); Eclass_Init(); FillClassList(); // list in entity window Map_Init(); FillTextureMenu(); FillBSPMenu(); /* ** other stuff */ Z_Init(); } void WINAPI QE_ConvertDOSToUnixName( char *dst, const char *src ){ while ( *src ) { if ( *src == '\\' ) { *dst = '/'; } else{ *dst = *src; } dst++; src++; } *dst = 0; } int g_numbrushes, g_numentities; void QE_CountBrushesAndUpdateStatusBar( void ){ static int s_lastbrushcount, s_lastentitycount; static qboolean s_didonce; //entity_t *e; brush_t *b, *next; g_numbrushes = 0; g_numentities = 0; if ( active_brushes.next != NULL ) { for ( b = active_brushes.next ; b != NULL && b != &active_brushes ; b = next ) { next = b->next; if ( b->brush_faces ) { if ( !b->owner->eclass->fixedsize ) { g_numbrushes++; } else{ g_numentities++; } } } } /* if ( entities.next != NULL ) { for ( e = entities.next ; e != &entities && g_numentities != MAX_MAP_ENTITIES ; e = e->next) { g_numentities++; } } */ if ( ( ( g_numbrushes != s_lastbrushcount ) || ( g_numentities != s_lastentitycount ) ) || ( !s_didonce ) ) { Sys_UpdateStatusBar(); s_lastbrushcount = g_numbrushes; s_lastentitycount = g_numentities; s_didonce = true; } } char com_token[1024]; qboolean com_eof; /* ================ I_FloatTime ================ */ double I_FloatTime( void ){ time_t t; time( &t ); return t; #if 0 // more precise, less portable struct timeval tp; struct timezone tzp; static int secbase; gettimeofday( &tp, &tzp ); if ( !secbase ) { secbase = tp.tv_sec; return tp.tv_usec / 1000000.0; } return ( tp.tv_sec - secbase ) + tp.tv_usec / 1000000.0; #endif } /* ============== COM_Parse Parse a token out of a string ============== */ char *COM_Parse( char *data ){ int c; int len; len = 0; com_token[0] = 0; if ( !data ) { return NULL; } // skip whitespace skipwhite: while ( ( c = *data ) <= ' ' ) { if ( c == 0 ) { com_eof = true; return NULL; // end of file; } data++; } // skip // comments if ( c == '/' && data[1] == '/' ) { while ( *data && *data != '\n' ) data++; goto skipwhite; } // handle quoted strings specially if ( c == '\"' ) { data++; do { c = *data++; if ( c == '\"' ) { com_token[len] = 0; return data; } com_token[len] = c; len++; } while ( 1 ); } // parse single characters if ( c == '{' || c == '}' || c == ')' || c == '(' || c == '\'' || c == ':' ) { com_token[len] = c; len++; com_token[len] = 0; return data + 1; } // parse a regular word do { com_token[len] = c; data++; len++; c = *data; if ( c == '{' || c == '}' || c == ')' || c == '(' || c == '\'' || c == ':' ) { break; } } while ( c > 32 ); com_token[len] = 0; return data; } char* Get_COM_Token(){ return com_token; } /* ============================================================================= MISC FUNCTIONS ============================================================================= */ int argc; char *argv[MAX_NUM_ARGVS]; /* ============ ParseCommandLine ============ */ void ParseCommandLine( char *lpCmdLine ){ argc = 1; argv[0] = const_cast("programname"); while ( *lpCmdLine && ( argc < MAX_NUM_ARGVS ) ) { while ( *lpCmdLine && ( ( *lpCmdLine <= 32 ) || ( *lpCmdLine > 126 ) ) ) lpCmdLine++; if ( *lpCmdLine ) { argv[argc] = lpCmdLine; argc++; while ( *lpCmdLine && ( ( *lpCmdLine > 32 ) && ( *lpCmdLine <= 126 ) ) ) lpCmdLine++; if ( *lpCmdLine ) { *lpCmdLine = 0; lpCmdLine++; } } } } /* ================= CheckParm Checks for the given parameter in the program's command line arguments Returns the argument number (1 to argc-1) or 0 if not present ================= */ int CheckParm( const char *check ){ int i; for ( i = 1; i < argc; i++ ) { if ( stricmp( check, argv[i] ) ) { return i; } } return 0; } /* ============== ParseNum / ParseHex ============== */ int ParseHex( const char *hex ){ const char *str; int num; num = 0; str = hex; while ( *str ) { num <<= 4; if ( *str >= '0' && *str <= '9' ) { num += *str - '0'; } else if ( *str >= 'a' && *str <= 'f' ) { num += 10 + *str - 'a'; } else if ( *str >= 'A' && *str <= 'F' ) { num += 10 + *str - 'A'; } else{ Error( "Bad hex number: %s",hex ); } str++; } return num; } int ParseNum( const char *str ){ if ( str[0] == '$' ) { return ParseHex( str + 1 ); } if ( str[0] == '0' && str[1] == 'x' ) { return ParseHex( str + 2 ); } return atol( str ); } // BSP frontend plugin // global flag for BSP frontend plugin is g_qeglobals.bBSPFrontendPlugin _QERPlugBSPFrontendTable g_BSPFrontendTable; // ============================================================================= // Sys_ functions bool Sys_AltDown(){ #ifdef _WIN32 return ( GetKeyState( VK_MENU ) & 0x8000 ) != 0; #endif #if defined ( __linux__ ) || defined ( __APPLE__ ) char keys[32]; int x; XQueryKeymap( GDK_DISPLAY(), keys ); x = XKeysymToKeycode( GDK_DISPLAY(), XK_Alt_L ); if ( keys[x / 8] & ( 1 << ( x % 8 ) ) ) { return true; } x = XKeysymToKeycode( GDK_DISPLAY(), XK_Alt_R ); if ( keys[x / 8] & ( 1 << ( x % 8 ) ) ) { return true; } return false; #endif } bool Sys_ShiftDown(){ #ifdef _WIN32 return ( GetKeyState( VK_SHIFT ) & 0x8000 ) != 0; #endif #if defined ( __linux__ ) || defined ( __APPLE__ ) char keys[32]; int x; XQueryKeymap( GDK_DISPLAY(), keys ); x = XKeysymToKeycode( GDK_DISPLAY(), XK_Shift_L ); if ( keys[x / 8] & ( 1 << ( x % 8 ) ) ) { return true; } x = XKeysymToKeycode( GDK_DISPLAY(), XK_Shift_R ); if ( keys[x / 8] & ( 1 << ( x % 8 ) ) ) { return true; } return false; #endif } void Sys_MarkMapModified( void ){ char title[PATH_MAX]; if ( modified != 1 ) { modified = true; // mark the map as changed sprintf( title, "%s *", currentmap ); QE_ConvertDOSToUnixName( title, title ); Sys_SetTitle( title ); } } void Sys_SetTitle( const char *text ){ gtk_window_set_title( GTK_WINDOW( g_qeglobals_gui.d_main_window ), text ); } bool g_bWaitCursor = false; void WINAPI Sys_BeginWait( void ){ GdkCursor *cursor = gdk_cursor_new( GDK_WATCH ); gdk_window_set_cursor( g_pParentWnd->m_pWidget->window, cursor ); gdk_cursor_unref( cursor ); g_bWaitCursor = true; } void WINAPI Sys_EndWait( void ){ GdkCursor *cursor = gdk_cursor_new( GDK_LEFT_PTR ); gdk_window_set_cursor( g_pParentWnd->m_pWidget->window, cursor ); gdk_cursor_unref( cursor ); g_bWaitCursor = false; } void Sys_GetCursorPos( int *x, int *y ){ // FIXME: not multihead safe gdk_window_get_pointer( NULL, x, y, NULL ); } void Sys_SetCursorPos( int x, int y ){ // NOTE: coordinates are in GDK space, not OS space #ifdef _WIN32 int sys_x = x - g_pParentWnd->GetGDKOffsetX(); int sys_y = y - g_pParentWnd->GetGDKOffsetY(); SetCursorPos( sys_x, sys_y ); #endif #if defined ( __linux__ ) || defined ( __APPLE__ ) XWarpPointer( GDK_DISPLAY(), None, GDK_ROOT_WINDOW(), 0, 0, 0, 0, x, y ); #endif } void Sys_Beep( void ){ #if defined ( __linux__ ) || defined ( __APPLE__ ) gdk_beep(); #else MessageBeep( MB_ICONASTERISK ); #endif } double Sys_DoubleTime( void ){ return clock() / 1000.0; } /* =============================================================== STATUS WINDOW =============================================================== */ void Sys_UpdateStatusBar( void ){ extern int g_numbrushes, g_numentities; char numbrushbuffer[100] = ""; sprintf( numbrushbuffer, "Brushes: %d Entities: %d", g_numbrushes, g_numentities ); g_pParentWnd->SetStatusText( 2, numbrushbuffer ); //Sys_Status( numbrushbuffer, 2 ); } void Sys_Status( const char *psz, int part ){ g_pParentWnd->SetStatusText( part, psz ); } // ============================================================================= // MRU #define MRU_MAX 4 static GtkWidget *MRU_items[MRU_MAX]; static int MRU_used; typedef char MRU_filename_t[PATH_MAX]; MRU_filename_t MRU_filenames[MRU_MAX]; static char* MRU_GetText( int index ){ return MRU_filenames[index]; } void buffer_write_escaped_mnemonic( char* buffer, const char* string ){ while ( *string != '\0' ) { if ( *string == '_' ) { *buffer++ = '_'; } *buffer++ = *string++; } *buffer = '\0'; } static void MRU_SetText( int index, const char *filename ){ strcpy( MRU_filenames[index], filename ); char mnemonic[PATH_MAX * 2 + 4]; mnemonic[0] = '_'; sprintf( mnemonic + 1, "%d", index + 1 ); mnemonic[2] = '-'; mnemonic[3] = ' '; buffer_write_escaped_mnemonic( mnemonic + 4, filename ); gtk_label_set_text_with_mnemonic( GTK_LABEL( GTK_BIN( MRU_items[index] )->child ), mnemonic ); } void MRU_Load(){ int i = g_PrefsDlg.m_nMRUCount; if ( i > 4 ) { i = 4; //FIXME: make this a define } for (; i > 0; i-- ) MRU_AddFile( g_PrefsDlg.m_strMRUFiles[i - 1].GetBuffer() ); } void MRU_Save(){ g_PrefsDlg.m_nMRUCount = MRU_used; for ( int i = 0; i < MRU_used; i++ ) g_PrefsDlg.m_strMRUFiles[i] = MRU_GetText( i ); } void MRU_AddWidget( GtkWidget *widget, int pos ){ if ( pos < MRU_MAX ) { MRU_items[pos] = widget; } } void MRU_AddFile( const char *str ){ int i; char* text; // check if file is already in our list for ( i = 0; i < MRU_used; i++ ) { text = MRU_GetText( i ); if ( strcmp( text, str ) == 0 ) { // reorder menu for (; i > 0; i-- ) MRU_SetText( i, MRU_GetText( i - 1 ) ); MRU_SetText( 0, str ); return; } } if ( MRU_used < MRU_MAX ) { MRU_used++; } // move items down for ( i = MRU_used - 1; i > 0; i-- ) MRU_SetText( i, MRU_GetText( i - 1 ) ); MRU_SetText( 0, str ); gtk_widget_set_sensitive( MRU_items[0], TRUE ); gtk_widget_show( MRU_items[MRU_used - 1] ); } void MRU_Activate( int index ){ char *text = MRU_GetText( index ); if ( access( text, R_OK ) == 0 ) { text = strdup( text ); MRU_AddFile( text ); Map_LoadFile( text ); free( text ); } else { MRU_used--; for ( int i = index; i < MRU_used; i++ ) MRU_SetText( i, MRU_GetText( i + 1 ) ); if ( MRU_used == 0 ) { gtk_label_set_text( GTK_LABEL( GTK_BIN( MRU_items[0] )->child ), "Recent Files" ); gtk_widget_set_sensitive( MRU_items[0], FALSE ); } else { gtk_widget_hide( MRU_items[MRU_used] ); } } } /* ====================================================================== FILE DIALOGS ====================================================================== */ qboolean ConfirmModified() { if( !modified ) return TRUE; int saveChoice = gtk_MessageBoxNew( g_pParentWnd->m_pWidget, "The current map has changed since it was last saved.\n" "Would you like to save before continuing?", "Radiant", MB_YESNOCANCEL | MB_ICONQUESTION ); switch( saveChoice ) { case IDYES: { g_pParentWnd->OnFileSave(); break; } case IDNO: { return TRUE; } case IDCANCEL: default: { return FALSE; } } return TRUE; } void ProjectDialog(){ const char *filename; char buffer[NAME_MAX]; /* * Obtain the system directory name and * store it in buffer. */ strcpy( buffer, g_qeglobals.m_strHomeGame.GetBuffer() ); strcat( buffer, g_pGameDescription->mBaseGame.GetBuffer() ); strcat( buffer, "/scripts/" ); // Display the Open dialog box filename = file_dialog( NULL, TRUE, _( "Open File" ), buffer, "project" ); if ( filename == NULL ) { return; // canceled } // Open the file. // NOTE: QE_LoadProject takes care of saving prefs with new path to the project file if ( !QE_LoadProject( filename ) ) { Sys_Printf( "Failed to load project from file: %s\n", filename ); } else{ // FIXME TTimo QE_Init is probably broken if you don't call it during startup right now .. QE_Init(); } } /* ======================================================= Menu modifications ======================================================= */ /* ================== FillBSPMenu ================== */ char *bsp_commands[256]; void FillBSPMenu(){ GtkWidget *item, *menu; // menu points to a GtkMenu (not an item) epair_t *ep; GList *lst; int i; menu = GTK_WIDGET( g_object_get_data( G_OBJECT( g_qeglobals_gui.d_main_window ), "menu_bsp" ) ); while ( ( lst = gtk_container_children( GTK_CONTAINER( menu ) ) ) != NULL ) gtk_container_remove( GTK_CONTAINER( menu ), GTK_WIDGET( lst->data ) ); if ( g_PrefsDlg.m_bDetachableMenus ) { item = gtk_tearoff_menu_item_new(); gtk_menu_append( GTK_MENU( menu ), item ); gtk_widget_set_sensitive( item, TRUE ); gtk_widget_show( item ); } if ( g_qeglobals.bBSPFrontendPlugin ) { CString str = g_BSPFrontendTable.m_pfnGetBSPMenu(); char cTemp[1024]; strcpy( cTemp, str ); char* token = strtok( cTemp, ",;" ); if ( token && *token == ' ' ) { while ( *token == ' ' ) token++; } i = 0; // first token is menu name item = gtk_menu_get_attach_widget( GTK_MENU( menu ) ); gtk_label_set_text( GTK_LABEL( GTK_BIN( item )->child ), token ); token = strtok( NULL, ",;" ); while ( token != NULL ) { g_BSPFrontendCommands = g_slist_append( g_BSPFrontendCommands, g_strdup( token ) ); item = gtk_menu_item_new_with_label( token ); gtk_widget_show( item ); gtk_container_add( GTK_CONTAINER( menu ), item ); gtk_signal_connect( GTK_OBJECT( item ), "activate", GTK_SIGNAL_FUNC( HandleCommand ), GINT_TO_POINTER( CMD_BSPCOMMAND + i ) ); token = strtok( NULL, ",;" ); i++; } } else { i = 0; for ( ep = g_qeglobals.d_project_entity->epairs; ep; ep = ep->next ) { if ( strncmp( ep->key, "bsp_", 4 ) == 0 ) { bsp_commands[i] = ep->key; item = gtk_menu_item_new_with_label( ep->key + 4 ); gtk_widget_show( item ); gtk_container_add( GTK_CONTAINER( menu ), item ); gtk_signal_connect( GTK_OBJECT( item ), "activate", GTK_SIGNAL_FUNC( HandleCommand ), GINT_TO_POINTER( CMD_BSPCOMMAND + i ) ); i++; } } } } //============================================== void AddSlash( CString& strPath ){ if ( strPath.GetLength() > 0 ) { if ( ( strPath.GetAt( strPath.GetLength() - 1 ) != '/' ) && ( strPath.GetAt( strPath.GetLength() - 1 ) != '\\' ) ) { strPath += '/'; } } } bool ExtractPath_and_Filename( const char* pPath, CString& strPath, CString& strFilename ){ CString strPathName; strPathName = pPath; int nSlash = strPathName.ReverseFind( '\\' ); if ( nSlash == -1 ) { // TTimo: try forward slash, some are using forward nSlash = strPathName.ReverseFind( '/' ); } if ( nSlash >= 0 ) { strPath = strPathName.Left( nSlash + 1 ); strFilename = strPathName.Right( strPathName.GetLength() - nSlash - 1 ); } // TTimo: try forward slash, some are using forward else{ strFilename = pPath; } return true; } //=========================================== //++timo FIXME: no longer used .. remove! char *TranslateString( char *buf ){ static char buf2[32768]; int i, l; char *out; l = strlen( buf ); out = buf2; for ( i = 0 ; i < l ; i++ ) { if ( buf[i] == '\n' ) { *out++ = '\r'; *out++ = '\n'; } else{ *out++ = buf[i]; } } *out++ = 0; return buf2; } // called whenever we need to open/close/check the console log file void Sys_LogFile( void ){ if ( g_PrefsDlg.mGamesDialog.m_bLogConsole && !g_qeglobals.hLogFile ) { // settings say we should be logging and we don't have a log file .. so create it // open a file to log the console (if user prefs say so) // the file handle is g_qeglobals.hLogFile // the log file is erased Str name; name = g_strTempPath; name += "radiant.log"; #if defined ( __linux__ ) || defined ( __APPLE__ ) g_qeglobals.hLogFile = open( name.GetBuffer(), O_TRUNC | O_CREAT | O_WRONLY, S_IREAD | S_IWRITE ); #endif #ifdef _WIN32 g_qeglobals.hLogFile = _open( name.GetBuffer(), _O_TRUNC | _O_CREAT | _O_WRONLY, _S_IREAD | _S_IWRITE ); #endif if ( g_qeglobals.hLogFile ) { Sys_Printf( "Started logging to %s\n", name.GetBuffer() ); time_t localtime; time( &localtime ); Sys_Printf( "Today is: %s", ctime( &localtime ) ); Sys_Printf( "This is radiant '" RADIANT_VERSION "' compiled " __DATE__ "\n" ); Sys_Printf( RADIANT_ABOUTMSG "\n" ); } else{ gtk_MessageBox( NULL, "Failed to create log file, check write permissions in Radiant directory.\n", "Console logging", MB_OK ); } } else if ( !g_PrefsDlg.mGamesDialog.m_bLogConsole && g_qeglobals.hLogFile ) { // settings say we should not be logging but still we have an active logfile .. close it time_t localtime; time( &localtime ); Sys_Printf( "Closing log file at %s\n", ctime( &localtime ) ); #ifdef _WIN32 _close( g_qeglobals.hLogFile ); #endif #if defined ( __linux__ ) || defined ( __APPLE__ ) close( g_qeglobals.hLogFile ); #endif g_qeglobals.hLogFile = 0; } } void Sys_ClearPrintf( void ){ GtkTextBuffer* buffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW( g_qeglobals_gui.d_edit ) ); gtk_text_buffer_set_text( buffer, "", -1 ); } // used to be around 32000, that should be way enough already #define BUFFER_SIZE 4096 extern "C" void Sys_FPrintf_VA( int level, const char *text, va_list args ) { char buf[BUFFER_SIZE]; buf[0] = 0; vsnprintf( buf, BUFFER_SIZE, text, args ); buf[BUFFER_SIZE - 1] = 0; const unsigned int length = strlen( buf ); if ( g_qeglobals.hLogFile ) { #ifdef _WIN32 _write( g_qeglobals.hLogFile, buf, length ); _commit( g_qeglobals.hLogFile ); #endif #if defined ( __linux__ ) || defined ( __APPLE__ ) write( g_qeglobals.hLogFile, buf, length ); #endif } if ( level != SYS_NOCON ) { // TTimo: FIXME: killed the console to avoid GDI leak fuckup if ( g_qeglobals_gui.d_edit != NULL ) { GtkTextBuffer* buffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW( g_qeglobals_gui.d_edit ) ); GtkTextIter iter; gtk_text_buffer_get_end_iter( buffer, &iter ); static GtkTextMark* end = gtk_text_buffer_create_mark( buffer, "end", &iter, FALSE ); const GdkColor yellow = { 0, 0xb0ff, 0xb0ff, 0x0000 }; const GdkColor red = { 0, 0xffff, 0x0000, 0x0000 }; const GdkColor black = { 0, 0x0000, 0x0000, 0x0000 }; static GtkTextTag* error_tag = gtk_text_buffer_create_tag( buffer, "red_foreground", "foreground-gdk", &red, NULL ); static GtkTextTag* warning_tag = gtk_text_buffer_create_tag( buffer, "yellow_foreground", "foreground-gdk", &yellow, NULL ); static GtkTextTag* standard_tag = gtk_text_buffer_create_tag( buffer, "black_foreground", "foreground-gdk", &black, NULL ); GtkTextTag* tag; switch ( level ) { case SYS_WRN: tag = warning_tag; break; case SYS_ERR: tag = error_tag; break; case SYS_STD: case SYS_VRB: default: tag = standard_tag; break; } gtk_text_buffer_insert_with_tags( buffer, &iter, buf, length, tag, (char *) NULL ); gtk_text_view_scroll_mark_onscreen( GTK_TEXT_VIEW( g_qeglobals_gui.d_edit ), end ); // update console widget immediatly if we're doing something time-consuming if ( !g_bScreenUpdates && GTK_WIDGET_REALIZED( g_qeglobals_gui.d_edit ) ) { gtk_grab_add( g_qeglobals_gui.d_edit ); while ( gtk_events_pending() ) gtk_main_iteration(); gtk_grab_remove( g_qeglobals_gui.d_edit ); } } } } // NOTE: this is the handler sent to synapse // must match PFN_SYN_PRINTF_VA extern "C" void Sys_Printf_VA( const char *text, va_list args ){ Sys_FPrintf_VA( SYS_STD, text, args ); } extern "C" void Sys_Printf( const char *text, ... ) { va_list args; va_start( args, text ); Sys_FPrintf_VA( SYS_STD, text, args ); va_end( args ); } extern "C" void Sys_FPrintf( int level, const char *text, ... ){ va_list args; va_start( args, text ); Sys_FPrintf_VA( level, text, args ); va_end( args ); }