mirror of
https://github.com/UberGames/GtkRadiant.git
synced 2024-11-29 23:22:23 +00:00
1297 lines
33 KiB
C++
1297 lines
33 KiB
C++
/*
|
|
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
|
|
*/
|
|
|
|
#include "stdafx.h"
|
|
#include <string.h>
|
|
#if defined ( __linux__ ) || defined ( __APPLE__ )
|
|
#include <unistd.h>
|
|
#endif
|
|
#include "preferences.h"
|
|
#include "mainframe.h"
|
|
#include "gtkmisc.h"
|
|
#include "filters.h"
|
|
|
|
extern MainFrame* g_pParentWnd;
|
|
|
|
int modified; // for quit confirmation (0 = clean, 1 = unsaved,
|
|
// 2 = autosaved, but not regular saved)
|
|
|
|
char currentmap[1024];
|
|
|
|
brush_t active_brushes; // brushes currently being displayed
|
|
brush_t selected_brushes; // highlighted
|
|
|
|
face_t *selected_face;
|
|
brush_t *selected_face_brush;
|
|
|
|
brush_t filtered_brushes; // brushes that have been filtered or regioned
|
|
|
|
entity_t entities; // head/tail of doubly linked list
|
|
|
|
entity_t *world_entity = NULL; // "classname" "worldspawn" !
|
|
|
|
void Map_Init(){
|
|
Map_Free();
|
|
}
|
|
|
|
|
|
bool g_bCancel_Map_LoadFile; // Hydra: moved this here
|
|
|
|
// TTimo
|
|
// need that in a variable, will have to tweak depending on the game
|
|
int g_MaxWorldCoord = 64 * 1024;
|
|
int g_MinWorldCoord = -64 * 1024;
|
|
|
|
// the max size we allow on brushes, this is dependant on world coords too
|
|
// makes more sense to say smaller I think?
|
|
int g_MaxBrushSize = ( g_MaxWorldCoord - 1 ) * 2;
|
|
|
|
void AddRegionBrushes( void );
|
|
void RemoveRegionBrushes( void );
|
|
|
|
/*
|
|
=============================================================
|
|
|
|
Cross map selection saving
|
|
|
|
this could fuck up if you have only part of a complex entity selected...
|
|
=============================================================
|
|
*/
|
|
|
|
brush_t between_brushes;
|
|
entity_t between_entities;
|
|
|
|
bool g_bRestoreBetween = false;
|
|
|
|
void Map_SaveBetween( void ){
|
|
if ( g_pParentWnd->ActiveXY() ) {
|
|
g_bRestoreBetween = true;
|
|
g_pParentWnd->ActiveXY()->Copy();
|
|
}
|
|
return;
|
|
}
|
|
|
|
void Map_RestoreBetween( void ){
|
|
if ( g_pParentWnd->ActiveXY() && g_bRestoreBetween ) {
|
|
g_pParentWnd->ActiveXY()->Paste();
|
|
}
|
|
}
|
|
|
|
//============================================================================
|
|
|
|
bool CheckForTinyBrush( brush_t* b, int n, float fSize ){
|
|
bool bTiny = false;
|
|
for ( int i = 0 ; i < 3 ; i++ )
|
|
{
|
|
if ( b->maxs[i] - b->mins[i] < fSize ) {
|
|
bTiny = true;
|
|
}
|
|
}
|
|
if ( bTiny ) {
|
|
Sys_Printf( "Possible problem brush (too small) #%i ", n );
|
|
}
|
|
return bTiny;
|
|
}
|
|
|
|
void Map_BuildBrushData( void ){
|
|
brush_t *b, *next;
|
|
|
|
if ( active_brushes.next == NULL ) {
|
|
return;
|
|
}
|
|
|
|
Sys_BeginWait(); // this could take a while
|
|
|
|
int n = 0;
|
|
for ( b = active_brushes.next ; b != NULL && b != &active_brushes ; b = next )
|
|
{
|
|
next = b->next;
|
|
Brush_Build( b, true, false, false );
|
|
if ( !b->brush_faces || ( g_PrefsDlg.m_bCleanTiny && CheckForTinyBrush( b, n++, g_PrefsDlg.m_fTinySize ) ) ) {
|
|
Brush_Free( b );
|
|
Sys_Printf( "Removed degenerate brush\n" );
|
|
}
|
|
}
|
|
Sys_EndWait();
|
|
}
|
|
|
|
entity_t *Map_FindClass( const char *cname ){
|
|
entity_t *ent;
|
|
|
|
for ( ent = entities.next ; ent != &entities ; ent = ent->next )
|
|
{
|
|
if ( !strcmp( cname, ValueForKey( ent, "classname" ) ) ) {
|
|
return ent;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
================
|
|
Map_Free
|
|
free all map elements, reinitialize the structures that depend on them
|
|
================
|
|
*/
|
|
void Map_Free( void ){
|
|
g_bRestoreBetween = false;
|
|
if ( selected_brushes.next &&
|
|
( selected_brushes.next != &selected_brushes ) ) {
|
|
if ( gtk_MessageBox( g_pParentWnd->m_pWidget, "Copy selection?", " ", MB_YESNO ) == IDYES ) {
|
|
Map_SaveBetween();
|
|
}
|
|
}
|
|
|
|
QERApp_ActiveShaders_SetInUse( false );
|
|
Pointfile_Clear();
|
|
g_qeglobals.d_num_entities = 0;
|
|
|
|
if ( !active_brushes.next ) {
|
|
// first map
|
|
active_brushes.prev = active_brushes.next = &active_brushes;
|
|
selected_brushes.prev = selected_brushes.next = &selected_brushes;
|
|
filtered_brushes.prev = filtered_brushes.next = &filtered_brushes;
|
|
entities.prev = entities.next = &entities;
|
|
}
|
|
else
|
|
{
|
|
// free selected faces array
|
|
g_ptrSelectedFaces.RemoveAll();
|
|
g_ptrSelectedFaceBrushes.RemoveAll();
|
|
while ( active_brushes.next != &active_brushes )
|
|
Brush_Free( active_brushes.next );
|
|
while ( selected_brushes.next != &selected_brushes )
|
|
Brush_Free( selected_brushes.next );
|
|
while ( filtered_brushes.next != &filtered_brushes )
|
|
Brush_Free( filtered_brushes.next );
|
|
while ( entities.next != &entities )
|
|
Entity_Free( entities.next );
|
|
}
|
|
|
|
if ( world_entity ) {
|
|
Entity_Free( world_entity );
|
|
}
|
|
world_entity = NULL;
|
|
}
|
|
|
|
entity_t *AngledEntity(){
|
|
entity_t *ent = Map_FindClass( "info_player_start" );
|
|
if ( !ent ) {
|
|
ent = Map_FindClass( "info_player_deathmatch" );
|
|
}
|
|
if ( !ent ) {
|
|
ent = Map_FindClass( "info_player_deathmatch" );
|
|
}
|
|
if ( !ent ) {
|
|
ent = Map_FindClass( "team_CTF_redplayer" );
|
|
}
|
|
if ( !ent ) {
|
|
ent = Map_FindClass( "team_CTF_blueplayer" );
|
|
}
|
|
if ( !ent ) {
|
|
ent = Map_FindClass( "team_CTF_redspawn" );
|
|
}
|
|
if ( !ent ) {
|
|
ent = Map_FindClass( "team_CTF_bluespawn" );
|
|
}
|
|
return ent;
|
|
}
|
|
|
|
//
|
|
// move the view to a start position
|
|
//
|
|
void Map_StartPosition(){
|
|
entity_t *ent = AngledEntity();
|
|
|
|
g_pParentWnd->GetCamWnd()->Camera()->angles[PITCH] = 0;
|
|
if ( ent ) {
|
|
GetVectorForKey( ent, "origin", g_pParentWnd->GetCamWnd()->Camera()->origin );
|
|
GetVectorForKey( ent, "origin", g_pParentWnd->GetXYWnd()->GetOrigin() );
|
|
g_pParentWnd->GetCamWnd()->Camera()->angles[YAW] = FloatForKey( ent, "angle" );
|
|
}
|
|
else
|
|
{
|
|
g_pParentWnd->GetCamWnd()->Camera()->angles[YAW] = 0;
|
|
VectorCopy( vec3_origin, g_pParentWnd->GetCamWnd()->Camera()->origin );
|
|
VectorCopy( vec3_origin, g_pParentWnd->GetXYWnd()->GetOrigin() );
|
|
}
|
|
}
|
|
|
|
void Map_FreeEntities( CPtrArray *ents ){
|
|
int i, j, num_ents, num_brushes;
|
|
entity_t* e;
|
|
CPtrArray* brushes;
|
|
|
|
num_ents = ents->GetSize();
|
|
for ( i = 0; i < num_ents; i++ )
|
|
{
|
|
e = (entity_t*)ents->GetAt( i );
|
|
brushes = (CPtrArray*)e->pData;
|
|
num_brushes = brushes->GetSize();
|
|
for ( j = 0; j < num_brushes; j++ )
|
|
Brush_Free( (brush_t*)brushes->GetAt( j ) );
|
|
brushes->RemoveAll();
|
|
delete (CPtrArray*)e->pData;
|
|
e->pData = NULL;
|
|
Entity_Free( e );
|
|
}
|
|
ents->RemoveAll();
|
|
}
|
|
|
|
/*!\todo Possibly make the import Undo-friendly by calling Undo_End for new brushes and ents */
|
|
void Map_ImportEntities( CPtrArray *ents, bool bAddSelected = false ){
|
|
int num_ents, num_brushes;
|
|
CPtrArray *brushes;
|
|
vec3_t mins, maxs;
|
|
entity_t *e;
|
|
brush_t *b;
|
|
face_t *f;
|
|
int i,j;
|
|
|
|
GPtrArray *new_ents = g_ptr_array_new();
|
|
|
|
g_qeglobals.bPrimitBrushes = false;
|
|
|
|
brush_t *pBrushList = ( bAddSelected ) ? &selected_brushes : &active_brushes;
|
|
|
|
bool bDoneBPCheck = false;
|
|
g_qeglobals.bNeedConvert = false;
|
|
// HACK: find out if this map file was a BP one
|
|
// check the first brush in the file that is NOT a patch
|
|
// this will not be necessary when we allow both formats in the same file
|
|
num_ents = ents->GetSize();
|
|
for ( i = 0; !bDoneBPCheck && i < num_ents; i++ )
|
|
{
|
|
e = (entity_t*)ents->GetAt( i );
|
|
brushes = (CPtrArray*)e->pData;
|
|
num_brushes = brushes->GetSize();
|
|
for ( j = 0; !bDoneBPCheck && j < num_brushes; j++ )
|
|
{
|
|
/*!todo Allow mixing texdef formats per-face. */
|
|
b = (brush_t *)brushes->GetAt( j );
|
|
if ( b->patchBrush ) {
|
|
continue;
|
|
}
|
|
bDoneBPCheck = true;
|
|
int BP_param = -1;
|
|
if ( b->bBrushDef && !g_qeglobals.m_bBrushPrimitMode ) {
|
|
BP_param = 0;
|
|
}
|
|
else if ( !b->bBrushDef && g_qeglobals.m_bBrushPrimitMode ) {
|
|
BP_param = 1;
|
|
}
|
|
|
|
if ( BP_param != -1 ) {
|
|
switch ( BP_MessageBox( BP_param ) )
|
|
{
|
|
case 0:
|
|
Map_FreeEntities( ents );
|
|
return;
|
|
case 1:
|
|
g_qeglobals.bNeedConvert = true;
|
|
break;
|
|
case 2:
|
|
g_qeglobals.bNeedConvert = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// process the entities into the world geometry
|
|
num_ents = ents->GetSize();
|
|
for ( i = 0; i < num_ents; i++ )
|
|
{
|
|
num_brushes = 0;
|
|
e = (entity_t*)ents->GetAt( i );
|
|
brushes = (CPtrArray*)e->pData;
|
|
|
|
num_brushes = brushes->GetSize();
|
|
// link brushes into entity
|
|
for ( j = 0; j < num_brushes; j++ )
|
|
{
|
|
Entity_LinkBrush( e, (brush_t *)brushes->GetAt( j ) );
|
|
g_qeglobals.d_parsed_brushes++;
|
|
}
|
|
brushes->RemoveAll();
|
|
delete brushes;
|
|
e->pData = NULL;
|
|
|
|
// set entity origin
|
|
GetVectorForKey( e, "origin", e->origin );
|
|
// set entity eclass
|
|
/*!\todo Make SetKeyValue check for "classname" change and assign appropriate eclass */
|
|
e->eclass = Eclass_ForName( ValueForKey( e, "classname" ),
|
|
( e->brushes.onext != &e->brushes ) );
|
|
|
|
// go through all parsed brushes and build stuff
|
|
for ( b = e->brushes.onext; b != &e->brushes; b = b->onext )
|
|
{
|
|
for ( f = b->brush_faces; f != NULL; f = f->next )
|
|
{
|
|
f->pShader = QERApp_Shader_ForName( f->texdef.GetName() );
|
|
f->d_texture = f->pShader->getTexture();
|
|
}
|
|
|
|
// when brushes are in final state, build the planes and windings
|
|
// NOTE: also converts BP brushes if g_qeglobals.bNeedConvert is true
|
|
Brush_Build( b );
|
|
}
|
|
|
|
//#define TERRAIN_HACK
|
|
#undef TERRAIN_HACK
|
|
|
|
#ifdef TERRAIN_HACK
|
|
if ( ( strcmp( ValueForKey( e, "terrain" ),"1" ) == 0 && strcmp( e->eclass->name,"func_group" ) == 0 ) ) {
|
|
|
|
// two aux pointers to the shaders used in the terrain entity
|
|
// we don't keep refcount on them since they are only temporary
|
|
// this avoids doing expensive lookups by name for all faces
|
|
IShader *pTerrainShader, *pCaulk;
|
|
|
|
pTerrainShader = NULL;
|
|
pCaulk = QERApp_Shader_ForName( SHADER_CAULK );
|
|
|
|
for ( b = e->brushes.onext; b != &e->brushes; b = b->onext )
|
|
{
|
|
if ( pTerrainShader == NULL ) {
|
|
for ( f = b->brush_faces; f != NULL; f = f->next )
|
|
if ( strcmp( f->texdef.GetName(), SHADER_CAULK ) != 0 ) {
|
|
pTerrainShader = f->pShader;
|
|
}
|
|
}
|
|
|
|
if ( pTerrainShader ) {
|
|
for ( f = b->brush_faces; f != NULL; f = f->next )
|
|
{
|
|
if ( strcmp( f->texdef.GetName(), SHADER_CAULK ) != 0 ) { // not caulk
|
|
Face_SetShader( f, pTerrainShader->getName() );
|
|
}
|
|
else{
|
|
Face_SetShader( f, pCaulk->getName() );
|
|
}
|
|
}
|
|
}
|
|
else{
|
|
Sys_Printf( "WARNING: no terrain shader found for brush\n" );
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#define PATCH_HACK
|
|
#ifdef PATCH_HACK
|
|
for ( b = e->brushes.onext; b != &e->brushes; b = b->onext )
|
|
{
|
|
// patch hack, to be removed when dependency on brush_faces is removed
|
|
if ( b->patchBrush ) {
|
|
Patch_CalcBounds( b->pPatch, mins, maxs );
|
|
for ( int i = 0; i < 3; i++ )
|
|
{
|
|
if ( (int)mins[i] == (int)maxs[i] ) {
|
|
mins[i] -= 4;
|
|
maxs[i] += 4;
|
|
}
|
|
}
|
|
Brush_Resize( b, mins, maxs );
|
|
Brush_Build( b );
|
|
}
|
|
}
|
|
#endif
|
|
// add brush for fixedsize entity
|
|
if ( e->eclass->fixedsize ) {
|
|
vec3_t mins, maxs;
|
|
VectorAdd( e->eclass->mins, e->origin, mins );
|
|
VectorAdd( e->eclass->maxs, e->origin, maxs );
|
|
b = Brush_Create( mins, maxs, &e->eclass->texdef );
|
|
Entity_LinkBrush( e, b );
|
|
Brush_Build( b );
|
|
}
|
|
|
|
for ( b = e->brushes.onext; b != &e->brushes; b = b->onext )
|
|
Brush_AddToList( b, pBrushList );
|
|
|
|
if ( strcmp( e->eclass->name, "worldspawn" ) == 0 ) {
|
|
if ( world_entity ) {
|
|
while ( e->brushes.onext != &e->brushes )
|
|
{
|
|
b = e->brushes.onext;
|
|
Entity_UnlinkBrush( b );
|
|
Entity_LinkBrush( world_entity, b );
|
|
}
|
|
Entity_Free( e );
|
|
}
|
|
else
|
|
{
|
|
world_entity = e;
|
|
}
|
|
}
|
|
else if ( strcmp( e->eclass->name, "group_info" ) == 0 ) {
|
|
// it's a group thing!
|
|
Group_Add( e );
|
|
Entity_Free( e );
|
|
}
|
|
else
|
|
{
|
|
// fix target/targetname collisions
|
|
if ( ( g_PrefsDlg.m_bDoTargetFix ) && ( strcmp( ValueForKey( e, "target" ), "" ) != 0 ) ) {
|
|
GPtrArray *t_ents = g_ptr_array_new();
|
|
entity_t *e_target;
|
|
const char *target = ValueForKey( e, "target" );
|
|
qboolean bCollision = FALSE;
|
|
|
|
// check the current map entities for an actual collision
|
|
for ( e_target = entities.next; e_target != &entities; e_target = e_target->next )
|
|
{
|
|
if ( !strcmp( target, ValueForKey( e_target, "target" ) ) ) {
|
|
bCollision = TRUE;
|
|
// make sure the collision is not between two imported entities
|
|
for ( j = 0; j < (int)new_ents->len; j++ )
|
|
{
|
|
if ( e_target == g_ptr_array_index( new_ents, j ) ) {
|
|
bCollision = FALSE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// find the matching targeted entity(s)
|
|
if ( bCollision ) {
|
|
for ( j = num_ents - 1; j > 0; j-- )
|
|
{
|
|
e_target = (entity_t*)ents->GetAt( j );
|
|
if ( e_target != NULL && e_target != e ) {
|
|
const char *targetname = ValueForKey( e_target, "targetname" );
|
|
if ( ( targetname != NULL ) && ( strcmp( target, targetname ) == 0 ) ) {
|
|
g_ptr_array_add( t_ents, (gpointer)e_target );
|
|
}
|
|
}
|
|
}
|
|
if ( t_ents->len > 0 ) {
|
|
// link the first to get a unique target/targetname
|
|
Entity_Connect( e, (entity_t*)g_ptr_array_index( t_ents,0 ) );
|
|
// set the targetname of the rest of them manually
|
|
for ( j = 1; j < (int)t_ents->len; j++ )
|
|
SetKeyValue( (entity_t*)g_ptr_array_index( t_ents, j ), "targetname", ValueForKey( e, "target" ) );
|
|
}
|
|
g_ptr_array_free( t_ents, FALSE );
|
|
}
|
|
}
|
|
|
|
// add the entity to the end of the entity list
|
|
Entity_AddToList( e, &entities );
|
|
g_qeglobals.d_num_entities++;
|
|
|
|
// keep a list of ents added to avoid testing collisions against them
|
|
g_ptr_array_add( new_ents, (gpointer)e );
|
|
}
|
|
}
|
|
g_ptr_array_free( new_ents, FALSE );
|
|
|
|
ents->RemoveAll();
|
|
|
|
g_qeglobals.bNeedConvert = false;
|
|
}
|
|
|
|
void Map_Import( IDataStream *in, const char *type, bool bAddSelected ){
|
|
CPtrArray ents;
|
|
|
|
g_pParentWnd->GetSynapseClient().ImportMap( in, &ents, type );
|
|
Map_ImportEntities( &ents, bAddSelected );
|
|
}
|
|
|
|
/*
|
|
================
|
|
Map_LoadFile
|
|
================
|
|
*/
|
|
void Map_LoadFile( const char *filename ){
|
|
clock_t start, finish;
|
|
double elapsed_time;
|
|
start = clock();
|
|
|
|
Sys_BeginWait();
|
|
Select_Deselect();
|
|
/*!
|
|
\todo FIXME TTimo why is this commented out?
|
|
stability issues maybe? or duplicate feature?
|
|
forcing to show the console during map load was a good thing IMO
|
|
*/
|
|
//SetInspectorMode(W_CONSOLE);
|
|
Sys_Printf( "Loading map from %s\n", filename );
|
|
|
|
Map_Free();
|
|
//++timo FIXME: maybe even easier to have Group_Init called from Map_Free?
|
|
Group_Init();
|
|
g_qeglobals.d_num_entities = 0;
|
|
g_qeglobals.d_parsed_brushes = 0;
|
|
|
|
|
|
// cancel the map loading process
|
|
// used when conversion between standard map format and BP format is required and the user cancels the process
|
|
g_bCancel_Map_LoadFile = false;
|
|
|
|
strcpy( currentmap, filename );
|
|
|
|
g_bScreenUpdates = false; // leo: avoid redraws while loading the map (see fenris:1952)
|
|
|
|
// prepare to let the map module do the parsing
|
|
FileStream file;
|
|
const char* type = strrchr( filename,'.' );
|
|
if ( type != NULL ) {
|
|
type++;
|
|
}
|
|
// NOTE TTimo opening has binary doesn't make a lot of sense
|
|
// but opening as text confuses the scriptlib parser
|
|
// this may be a problem if we "rb" and use the XML parser, might have an incompatibility
|
|
if ( file.Open( filename, "rb" ) ) {
|
|
Map_Import( &file, type );
|
|
}
|
|
else{
|
|
Sys_FPrintf( SYS_ERR, "ERROR: failed to open %s for read\n", filename );
|
|
}
|
|
file.Close();
|
|
|
|
g_bScreenUpdates = true;
|
|
|
|
if ( g_bCancel_Map_LoadFile ) {
|
|
Sys_Printf( "Map_LoadFile canceled\n" );
|
|
Map_New();
|
|
Sys_EndWait();
|
|
return;
|
|
}
|
|
|
|
if ( !world_entity ) {
|
|
Sys_Printf( "No worldspawn in map.\n" );
|
|
Map_New();
|
|
Sys_EndWait();
|
|
return;
|
|
}
|
|
finish = clock();
|
|
elapsed_time = (double)( finish - start ) / CLOCKS_PER_SEC;
|
|
|
|
Sys_Printf( "--- LoadMapFile ---\n" );
|
|
Sys_Printf( "%s\n", filename );
|
|
|
|
Sys_Printf( "%5i brushes\n", g_qeglobals.d_parsed_brushes );
|
|
Sys_Printf( "%5i entities\n", g_qeglobals.d_num_entities );
|
|
Sys_Printf( "%5.2f second(s) load time\n", elapsed_time );
|
|
|
|
Sys_EndWait();
|
|
|
|
Map_RestoreBetween();
|
|
|
|
//
|
|
// move the view to a start position
|
|
//
|
|
Map_StartPosition();
|
|
|
|
Map_RegionOff();
|
|
|
|
modified = false;
|
|
Sys_SetTitle( filename );
|
|
|
|
Texture_ShowInuse();
|
|
QERApp_SortActiveShaders();
|
|
|
|
Sys_UpdateWindows( W_ALL );
|
|
}
|
|
|
|
/*!
|
|
===========
|
|
Supporting functions for Map_SaveFile, builds a CPtrArray with the filtered / non filtered brushes
|
|
===========
|
|
*/
|
|
void CleanFilter( entity_t *ent ){
|
|
if ( ent->pData ) {
|
|
delete static_cast<CPtrArray*>( ent->pData );
|
|
ent->pData = NULL;
|
|
}
|
|
}
|
|
|
|
/*!
|
|
filters out the region brushes if necessary
|
|
returns true if this entity as a whole is out of the region
|
|
(if all brushes are filtered out, then the entity will be completely dropped .. except if it's worldspawn of course)
|
|
*/
|
|
bool FilterChildren( entity_t *ent, bool bRegionOnly = false, bool bSelectedOnly = false ){
|
|
if ( ent->brushes.onext == &ent->brushes ) {
|
|
return false;
|
|
}
|
|
// entity without a brush, ignore it... this can be caused by Undo
|
|
|
|
// filter fixedsize ents by their eclass bounding box
|
|
// don't add their brushes
|
|
if ( ent->eclass->fixedsize ) {
|
|
if ( bSelectedOnly && !IsBrushSelected( ent->brushes.onext ) ) {
|
|
return false;
|
|
}
|
|
|
|
if ( bRegionOnly && region_active ) {
|
|
for ( int i = 0 ; i < 3 ; i++ )
|
|
{
|
|
if ( ( ent->origin[i] + ent->eclass->mins[i] ) > region_maxs[i] ) {
|
|
return false;
|
|
}
|
|
if ( ( ent->origin[i] + ent->eclass->maxs[i] ) < region_mins[i] ) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for ( brush_t *b = ent->brushes.onext ; b != &ent->brushes ; b = b->onext )
|
|
{
|
|
// set flag to use brushprimit_texdef
|
|
if ( g_qeglobals.m_bBrushPrimitMode ) {
|
|
b->bBrushDef = true;
|
|
}
|
|
else{
|
|
b->bBrushDef = false;
|
|
}
|
|
|
|
// add brush, unless it's excluded by region
|
|
if ( !( bRegionOnly && Map_IsBrushFiltered( b ) ) &&
|
|
!( bSelectedOnly && !IsBrushSelected( b ) ) ) {
|
|
( (CPtrArray*)ent->pData )->Add( b );
|
|
}
|
|
}
|
|
|
|
if ( ( (CPtrArray*)ent->pData )->GetSize() <= 0 ) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
entity_t *region_startpoint = NULL;
|
|
void Map_ExportEntities( CPtrArray* ents, bool bRegionOnly = false, bool bSelectedOnly = false ){
|
|
int i;
|
|
entity_t *e;
|
|
|
|
/*!
|
|
\todo the entity_t needs to be reworked and asbtracted some more
|
|
|
|
keeping the entity_t as the struct providing access to a list of map objects, a list of epairs and various other info?
|
|
but separating some more the data that belongs to the entity_t and the 'sons' data
|
|
on a side note, I don't think that doing that with linked list would be a good thing
|
|
|
|
for now, we use the blind void* in entity_t casted to a CPtrArray of brush_t* to hand out a list of the brushes for map write
|
|
the next step is very likely to be a change of the brush_t* to a more abstract object?
|
|
*/
|
|
|
|
FilterChildren( world_entity, bRegionOnly, bSelectedOnly );
|
|
ents->Add( world_entity );
|
|
|
|
for ( e = entities.next ; e != &entities ; e = e->next )
|
|
{
|
|
// not sure this still happens, probably safe to leave it in
|
|
if ( ( !strcmp( ValueForKey( e, "classname" ), "worldspawn" ) ) && ( e != world_entity ) ) {
|
|
Sys_FPrintf( SYS_ERR, "Dropping parasite worldspawn entity\n" );
|
|
continue;
|
|
}
|
|
|
|
// entities which brushes are completely filtered out by regioning are not printed to the map
|
|
if ( FilterChildren( e, bRegionOnly, bSelectedOnly ) ) {
|
|
ents->Add( e );
|
|
}
|
|
}
|
|
|
|
if ( bRegionOnly && region_active ) {
|
|
for ( i = 0; i < 6; i++ )
|
|
( (CPtrArray*)world_entity->pData )->Add( region_sides[i] );
|
|
|
|
ents->Add( region_startpoint );
|
|
}
|
|
}
|
|
|
|
void Map_Export( IDataStream *out, const char *type, bool bRegionOnly, bool bSelectedOnly ){
|
|
entity_t *e;
|
|
|
|
CPtrArray ents;
|
|
|
|
if ( bRegionOnly && region_active ) {
|
|
AddRegionBrushes();
|
|
}
|
|
|
|
// create the filters
|
|
world_entity->pData = new CPtrArray();
|
|
for ( e = entities.next; e != &entities; e = e->next )
|
|
e->pData = new CPtrArray();
|
|
|
|
Map_ExportEntities( &ents, bRegionOnly, bSelectedOnly );
|
|
|
|
g_pParentWnd->GetSynapseClient().ExportMap( &ents, out, type );
|
|
|
|
// cleanup the filters
|
|
CleanFilter( world_entity );
|
|
for ( e = entities.next ; e != &entities ; e = e->next )
|
|
CleanFilter( e );
|
|
|
|
if ( bRegionOnly && region_active ) {
|
|
RemoveRegionBrushes();
|
|
}
|
|
}
|
|
|
|
const char* filename_get_extension( const char* filename ){
|
|
const char* type = strrchr( filename,'.' );
|
|
if ( type != NULL ) {
|
|
return ++type;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
/*
|
|
===========
|
|
Map_SaveFile
|
|
\todo FIXME remove the use_region, this is broken .. work with a global flag to set region mode or not
|
|
===========
|
|
*/
|
|
void Map_SaveFile( const char *filename, qboolean use_region ){
|
|
clock_t start, finish;
|
|
double elapsed_time;
|
|
start = clock();
|
|
Sys_Printf( "Saving map to %s\n",filename );
|
|
|
|
Pointfile_Clear();
|
|
|
|
if ( !use_region ) {
|
|
char backup[1024];
|
|
|
|
// rename current to .bak
|
|
strcpy( backup, filename );
|
|
StripExtension( backup );
|
|
strcat( backup, ".bak" );
|
|
unlink( backup );
|
|
rename( filename, backup );
|
|
}
|
|
|
|
Sys_Printf( "Map_SaveFile: %s\n", filename );
|
|
|
|
// build the out data stream
|
|
FileStream file;
|
|
if ( !file.Open( filename,"w" ) ) {
|
|
Sys_FPrintf( SYS_ERR, "ERROR: couldn't open %s for write\n", filename );
|
|
return;
|
|
}
|
|
|
|
// extract filetype
|
|
Map_Export( &file, filename_get_extension( filename ), use_region );
|
|
|
|
file.Close();
|
|
|
|
finish = clock();
|
|
elapsed_time = (double)( finish - start ) / CLOCKS_PER_SEC;
|
|
|
|
Sys_Printf( "Saved in %-.2f second(s).\n",elapsed_time );
|
|
modified = false;
|
|
|
|
if ( !strstr( filename, "autosave" ) ) {
|
|
Sys_SetTitle( filename );
|
|
}
|
|
|
|
if ( !use_region ) {
|
|
time_t timer;
|
|
|
|
time( &timer );
|
|
|
|
Sys_Beep();
|
|
|
|
Sys_Status( "Saved.", 0 );
|
|
}
|
|
}
|
|
|
|
/*
|
|
===========
|
|
Map_New
|
|
|
|
===========
|
|
*/
|
|
void Map_New( void ){
|
|
Sys_Printf( "Map_New\n" );
|
|
Map_Free();
|
|
|
|
strcpy( currentmap, "unnamed.map" );
|
|
Sys_SetTitle( currentmap );
|
|
|
|
world_entity = (entity_s*)qmalloc( sizeof( *world_entity ) );
|
|
world_entity->brushes.onext =
|
|
world_entity->brushes.oprev = &world_entity->brushes;
|
|
SetKeyValue( world_entity, "classname", "worldspawn" );
|
|
world_entity->eclass = Eclass_ForName( "worldspawn", true );
|
|
|
|
g_pParentWnd->GetCamWnd()->Camera()->angles[YAW] = 0;
|
|
g_pParentWnd->GetCamWnd()->Camera()->angles[PITCH] = 0;
|
|
VectorCopy( vec3_origin, g_pParentWnd->GetCamWnd()->Camera()->origin );
|
|
g_pParentWnd->GetCamWnd()->Camera()->origin[2] = 48;
|
|
VectorCopy( vec3_origin, g_pParentWnd->GetXYWnd()->GetOrigin() );
|
|
|
|
Map_RestoreBetween();
|
|
|
|
Group_Init();
|
|
|
|
Sys_UpdateWindows( W_ALL );
|
|
modified = false;
|
|
}
|
|
|
|
/*
|
|
===========================================================
|
|
|
|
REGION
|
|
|
|
===========================================================
|
|
*/
|
|
qboolean region_active;
|
|
vec3_t region_mins = {g_MinWorldCoord, g_MinWorldCoord, g_MinWorldCoord};
|
|
vec3_t region_maxs = {g_MaxWorldCoord, g_MaxWorldCoord, g_MaxWorldCoord};
|
|
|
|
brush_t *region_sides[6];
|
|
|
|
/*
|
|
===========
|
|
AddRegionBrushes
|
|
a regioned map will have temp walls put up at the region boundary
|
|
\todo TODO TTimo old implementation of region brushes
|
|
we still add them straight in the worldspawn and take them out after the map is saved
|
|
with the new implementation we should be able to append them in a temporary manner to the data we pass to the map module
|
|
===========
|
|
*/
|
|
void AddRegionBrushes( void ){
|
|
vec3_t mins, maxs;
|
|
int i;
|
|
texdef_t td;
|
|
|
|
if ( !region_active ) {
|
|
#ifdef _DEBUG
|
|
Sys_FPrintf( SYS_WRN, "Unexpected AddRegionBrushes call.\n" );
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
memset( &td, 0, sizeof( td ) );
|
|
td.SetName( SHADER_NOT_FOUND );
|
|
|
|
// set mins
|
|
VectorSet( mins, region_mins[0] - 32, region_mins[1] - 32, region_mins[2] - 32 );
|
|
|
|
// vary maxs
|
|
for ( i = 0; i < 3; i++ )
|
|
{
|
|
VectorSet( maxs, region_maxs[0] + 32, region_maxs[1] + 32, region_maxs[2] + 32 );
|
|
maxs[i] = region_mins[i];
|
|
region_sides[i] = Brush_Create( mins, maxs, &td );
|
|
}
|
|
|
|
// set maxs
|
|
VectorSet( maxs, region_maxs[0] + 32, region_maxs[1] + 32, region_maxs[2] + 32 );
|
|
|
|
// vary mins
|
|
for ( i = 0; i < 3; i++ )
|
|
{
|
|
VectorSet( mins, region_mins[0] - 32, region_mins[1] - 32, region_mins[2] - 32 );
|
|
mins[i] = region_maxs[i];
|
|
region_sides[i + 3] = Brush_Create( mins, maxs, &td );
|
|
}
|
|
|
|
|
|
// this is a safe check, but it should not really happen anymore
|
|
vec3_t vOrig;
|
|
VectorSet( vOrig,
|
|
(int)g_pParentWnd->GetCamWnd()->Camera()->origin[0],
|
|
(int)g_pParentWnd->GetCamWnd()->Camera()->origin[1],
|
|
(int)g_pParentWnd->GetCamWnd()->Camera()->origin[2] );
|
|
|
|
for ( i = 0 ; i < 3 ; i++ )
|
|
{
|
|
if ( vOrig[i] > region_maxs[i] || vOrig[i] < region_mins[i] ) {
|
|
Sys_FPrintf( SYS_ERR, "Camera is NOT in the region, it's likely that the region won't compile correctly\n" );
|
|
}
|
|
}
|
|
|
|
// write the info_playerstart
|
|
region_startpoint = Entity_Alloc();
|
|
SetKeyValue( region_startpoint, "classname", "info_player_start" );
|
|
region_startpoint->eclass = Eclass_ForName( "info_player_start", false );
|
|
char sTmp[1024];
|
|
sprintf( sTmp, "%d %d %d", (int)vOrig[0], (int)vOrig[1], (int)vOrig[2] );
|
|
SetKeyValue( region_startpoint, "origin", sTmp );
|
|
sprintf( sTmp, "%d", (int)g_pParentWnd->GetCamWnd()->Camera()->angles[YAW] );
|
|
SetKeyValue( region_startpoint, "angle", sTmp );
|
|
// empty array of children
|
|
region_startpoint->pData = new CPtrArray;
|
|
}
|
|
|
|
void RemoveRegionBrushes( void ){
|
|
int i;
|
|
|
|
if ( !region_active ) {
|
|
return;
|
|
}
|
|
for ( i = 0 ; i < 6 ; i++ )
|
|
Brush_Free( region_sides[i] );
|
|
|
|
CleanFilter( region_startpoint );
|
|
Entity_Free( region_startpoint );
|
|
}
|
|
|
|
qboolean Map_IsBrushFiltered( brush_t *b ){
|
|
int i;
|
|
|
|
for ( i = 0 ; i < 3 ; i++ )
|
|
{
|
|
if ( b->mins[i] > region_maxs[i] ) {
|
|
return true;
|
|
}
|
|
if ( b->maxs[i] < region_mins[i] ) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
Map_RegionOff
|
|
|
|
Other filtering options may still be on
|
|
===========
|
|
*/
|
|
void Map_RegionOff( void ){
|
|
brush_t *b, *next;
|
|
int i;
|
|
|
|
region_active = false;
|
|
for ( i = 0 ; i < 3 ; i++ )
|
|
{
|
|
region_maxs[i] = g_MaxWorldCoord - 64;
|
|
region_mins[i] = g_MinWorldCoord + 64;
|
|
}
|
|
|
|
for ( b = filtered_brushes.next ; b != &filtered_brushes ; b = next )
|
|
{
|
|
next = b->next;
|
|
if ( Map_IsBrushFiltered( b ) ) {
|
|
continue; // still filtered
|
|
}
|
|
Brush_RemoveFromList( b );
|
|
if ( active_brushes.next == NULL || active_brushes.prev == NULL ) {
|
|
active_brushes.next = &active_brushes;
|
|
active_brushes.prev = &active_brushes;
|
|
}
|
|
Brush_AddToList( b, &active_brushes );
|
|
b->bFiltered = FilterBrush( b );
|
|
}
|
|
Sys_UpdateWindows( W_ALL );
|
|
}
|
|
|
|
void Map_ApplyRegion( void ){
|
|
brush_t *b, *next;
|
|
|
|
region_active = true;
|
|
for ( b = active_brushes.next ; b != &active_brushes ; b = next )
|
|
{
|
|
next = b->next;
|
|
if ( !Map_IsBrushFiltered( b ) ) {
|
|
continue; // still filtered
|
|
}
|
|
Brush_RemoveFromList( b );
|
|
Brush_AddToList( b, &filtered_brushes );
|
|
}
|
|
|
|
Sys_UpdateWindows( W_ALL );
|
|
}
|
|
|
|
|
|
/*
|
|
========================
|
|
Map_RegionSelectedBrushes
|
|
========================
|
|
*/
|
|
void Map_RegionSelectedBrushes( void ){
|
|
Map_RegionOff();
|
|
|
|
if ( selected_brushes.next == &selected_brushes ) { // nothing selected
|
|
Sys_Printf( "Tried to region with no selection...\n" );
|
|
return;
|
|
}
|
|
region_active = true;
|
|
Select_GetBounds( region_mins, region_maxs );
|
|
|
|
#ifdef _DEBUG
|
|
if ( filtered_brushes.next != &filtered_brushes ) {
|
|
Sys_Printf( "WARNING: filtered_brushes list may not be empty in Map_RegionSelectedBrushes\n" );
|
|
}
|
|
#endif
|
|
|
|
if ( active_brushes.next == &active_brushes ) {
|
|
// just have an empty filtered_brushes list
|
|
// this happens if you set region after selecting all the brushes in your map (some weird people do that, ask MrE!)
|
|
filtered_brushes.next = filtered_brushes.prev = &filtered_brushes;
|
|
}
|
|
else
|
|
{
|
|
// move the entire active_brushes list to filtered_brushes
|
|
filtered_brushes.next = active_brushes.next;
|
|
filtered_brushes.prev = active_brushes.prev;
|
|
filtered_brushes.next->prev = &filtered_brushes;
|
|
filtered_brushes.prev->next = &filtered_brushes;
|
|
}
|
|
|
|
// move the entire selected_brushes list to active_brushes
|
|
active_brushes.next = selected_brushes.next;
|
|
active_brushes.prev = selected_brushes.prev;
|
|
active_brushes.next->prev = &active_brushes;
|
|
active_brushes.prev->next = &active_brushes;
|
|
|
|
// deselect patches
|
|
for ( brush_t *b = active_brushes.next; b != &active_brushes; b = b->next )
|
|
if ( b->patchBrush ) {
|
|
b->pPatch->bSelected = false;
|
|
}
|
|
|
|
// clear selected_brushes
|
|
selected_brushes.next = selected_brushes.prev = &selected_brushes;
|
|
|
|
Sys_UpdateWindows( W_ALL );
|
|
}
|
|
|
|
|
|
/*
|
|
===========
|
|
Map_RegionXY
|
|
===========
|
|
*/
|
|
void Map_RegionXY( void ){
|
|
Map_RegionOff();
|
|
|
|
region_mins[0] = g_pParentWnd->GetXYWnd()->GetOrigin()[0] - 0.5 * g_pParentWnd->GetXYWnd()->Width() / g_pParentWnd->GetXYWnd()->Scale();
|
|
region_maxs[0] = g_pParentWnd->GetXYWnd()->GetOrigin()[0] + 0.5 * g_pParentWnd->GetXYWnd()->Width() / g_pParentWnd->GetXYWnd()->Scale();
|
|
region_mins[1] = g_pParentWnd->GetXYWnd()->GetOrigin()[1] - 0.5 * g_pParentWnd->GetXYWnd()->Height() / g_pParentWnd->GetXYWnd()->Scale();
|
|
region_maxs[1] = g_pParentWnd->GetXYWnd()->GetOrigin()[1] + 0.5 * g_pParentWnd->GetXYWnd()->Height() / g_pParentWnd->GetXYWnd()->Scale();
|
|
region_mins[2] = g_MinWorldCoord + 64;
|
|
region_maxs[2] = g_MaxWorldCoord - 64;
|
|
Map_ApplyRegion();
|
|
}
|
|
|
|
/*
|
|
===========
|
|
Map_RegionTallBrush
|
|
===========
|
|
*/
|
|
void Map_RegionTallBrush( void ){
|
|
brush_t *b;
|
|
|
|
if ( !QE_SingleBrush() ) {
|
|
return;
|
|
}
|
|
|
|
b = selected_brushes.next;
|
|
|
|
Map_RegionOff();
|
|
|
|
VectorCopy( b->mins, region_mins );
|
|
VectorCopy( b->maxs, region_maxs );
|
|
region_mins[2] = g_MinWorldCoord + 64;
|
|
region_maxs[2] = g_MaxWorldCoord - 64;
|
|
|
|
Undo_Start( "delete" );
|
|
Undo_AddBrushList( &selected_brushes );
|
|
Undo_AddEntity( b->owner );
|
|
Select_Delete();
|
|
Undo_EndBrushList( &selected_brushes );
|
|
Undo_End();
|
|
|
|
Map_ApplyRegion();
|
|
}
|
|
|
|
/*
|
|
===========
|
|
Map_RegionBrush
|
|
===========
|
|
*/
|
|
void Map_RegionBrush( void ){
|
|
brush_t *b;
|
|
|
|
if ( !QE_SingleBrush() ) {
|
|
return;
|
|
}
|
|
|
|
b = selected_brushes.next;
|
|
|
|
Map_RegionOff();
|
|
|
|
VectorCopy( b->mins, region_mins );
|
|
VectorCopy( b->maxs, region_maxs );
|
|
|
|
Undo_Start( "delete" );
|
|
Undo_AddBrushList( &selected_brushes );
|
|
Undo_AddEntity( b->owner );
|
|
Select_Delete();
|
|
Undo_EndBrushList( &selected_brushes );
|
|
Undo_End();
|
|
|
|
Map_ApplyRegion();
|
|
}
|
|
|
|
GList *find_string( GList *glist, const char *buf ){
|
|
while ( glist )
|
|
{
|
|
if ( strcmp( (char *)glist->data, buf ) == 0 ) {
|
|
break; // this name is in our list already
|
|
}
|
|
glist = glist->next;
|
|
}
|
|
return glist;
|
|
}
|
|
|
|
void Map_ImportBuffer( char *buf ){
|
|
Select_Deselect();
|
|
|
|
Undo_Start( "import buffer" );
|
|
|
|
MemStream stream;
|
|
|
|
stream.Write( buf, strlen( buf ) );
|
|
Map_Import( &stream, "xmap" );
|
|
stream.Close();
|
|
|
|
Sys_UpdateWindows( W_ALL );
|
|
Sys_MarkMapModified();
|
|
|
|
Undo_End();
|
|
}
|
|
|
|
|
|
//
|
|
//================
|
|
//Map_ImportFile
|
|
//================
|
|
//
|
|
void Map_ImportFile( const char *filename ){
|
|
FileStream file;
|
|
Sys_BeginWait();
|
|
|
|
Sys_Printf( "Importing map from %s\n",filename );
|
|
|
|
const char* type = strrchr( filename,'.' );
|
|
if ( type != NULL ) {
|
|
type++;
|
|
}
|
|
/*!\todo Resolve "r" problem in scriptlib" */
|
|
if ( file.Open( filename, "rb" ) ) {
|
|
Map_Import( &file, type, true );
|
|
}
|
|
else{
|
|
Sys_FPrintf( SYS_ERR, "ERROR: couldn't open %s for read\n", filename );
|
|
}
|
|
|
|
file.Close();
|
|
|
|
Sys_UpdateWindows( W_ALL );
|
|
modified = true;
|
|
Sys_EndWait();
|
|
}
|
|
|
|
//
|
|
//===========
|
|
//Map_SaveSelected
|
|
//===========
|
|
//
|
|
// Saves selected world brushes and whole entities with partial/full selections
|
|
//
|
|
void Map_SaveSelected( const char* filename ){
|
|
FileStream file;
|
|
|
|
Sys_Printf( "Saving selection to %s\n",filename );
|
|
|
|
const char* type = strrchr( filename,'.' );
|
|
if ( type != NULL ) {
|
|
type++;
|
|
}
|
|
if ( file.Open( filename, "w" ) ) {
|
|
Map_Export( &file, type, false, true );
|
|
}
|
|
else{
|
|
Sys_FPrintf( SYS_ERR, "ERROR: failed to open %s for write\n", filename );
|
|
}
|
|
|
|
file.Close();
|
|
|
|
}
|
|
|
|
//
|
|
//===========
|
|
//Map_SaveSelected
|
|
//===========
|
|
//
|
|
// Saves selected world brushes and whole entities with partial/full selections
|
|
//
|
|
void Map_SaveSelected( MemStream* pMemFile, MemStream* pPatchFile ){
|
|
Map_Export( pMemFile, "xmap", false, true );
|
|
|
|
/*
|
|
// write world entity first
|
|
Entity_WriteSelected(world_entity, pMemFile);
|
|
|
|
// then write all other ents
|
|
count = 1;
|
|
for (e=entities.next ; e != &entities ; e=next)
|
|
{
|
|
MemFile_fprintf(pMemFile, "// entity %i\n", count);
|
|
count++;
|
|
Entity_WriteSelected(e, pMemFile);
|
|
next = e->next;
|
|
}
|
|
|
|
//if (pPatchFile)
|
|
// Patch_WriteFile(pPatchFile);
|
|
*/
|
|
}
|
|
|
|
|
|
void MemFile_fprintf( MemStream* pMemFile, const char* pText, ... ){
|
|
char Buffer[4096];
|
|
va_list args;
|
|
va_start( args,pText );
|
|
vsprintf( Buffer, pText, args );
|
|
pMemFile->Write( Buffer, strlen( Buffer ) );
|
|
}
|
|
|
|
/*!
|
|
==============
|
|
Region_SpawnPoint
|
|
push the region spawn point
|
|
\todo FIXME TTimo this was in the #1 MAP module implementation (in the core)
|
|
not sure it has any use anymore, should prolly drop it
|
|
==============
|
|
*/
|
|
void Region_SpawnPoint( FILE *f ){
|
|
// write the info_player_start, we use the camera position
|
|
fprintf( f, "{\n" );
|
|
fprintf( f, "\"classname\" \"info_player_start\"\n" );
|
|
fprintf( f, "\"origin\" \"%i %i %i\"\n",
|
|
(int)g_pParentWnd->GetCamWnd()->Camera()->origin[0],
|
|
(int)g_pParentWnd->GetCamWnd()->Camera()->origin[1],
|
|
(int)g_pParentWnd->GetCamWnd()->Camera()->origin[2] );
|
|
fprintf( f, "\"angle\" \"%i\"\n", (int)g_pParentWnd->GetCamWnd()->Camera()->angles[YAW] );
|
|
fprintf( f, "}\n" );
|
|
}
|