diff --git a/docs/Complete_list_of_command_line_parameters.htm b/docs/Complete_list_of_command_line_parameters.htm
index b1f78a82..9f5caae6 100644
--- a/docs/Complete_list_of_command_line_parameters.htm
+++ b/docs/Complete_list_of_command_line_parameters.htm
@@ -429,6 +429,15 @@ td.formatted_questions ol { margin-top: 0px; margin-bottom: 0px; }
+
BSP merge¶
+
+ -mergebsp ... mainBsp.bsp bspToinject.bsp: Inject latter BSP to former. Tree and vis data of the main one are preserved.
+ -fixnames: Make incoming BSP target/targetname names unique to not collide with existing names
+ -world: Also merge worldspawn model (brushes as if they were detail, no BSP tree is affected) (only merges entities by default)
+
+
+
+
diff --git a/tools/quake3/q3map2/convert_bsp.cpp b/tools/quake3/q3map2/convert_bsp.cpp
index b8d5ce17..6ef0ec7b 100644
--- a/tools/quake3/q3map2/convert_bsp.cpp
+++ b/tools/quake3/q3map2/convert_bsp.cpp
@@ -664,6 +664,288 @@ int ShiftBSPMain( Args& args ){
}
+/*
+ MergeBSPMain()
+ merges two bsps
+ */
+
+int MergeBSPMain( Args& args ){
+ /* arg checking */
+ if ( args.size() < 2 ) {
+ Sys_Printf( "Usage: q3map2 [-v] -mergebsp [-fixnames] [-world] \n" );
+ return 0;
+ }
+
+ const char *fileName2 = args.takeBack();
+ const char *fileName1 = args.takeBack();
+ const auto argsToInject = args.getVector();
+
+ const bool fixnames = args.takeArg( "-fixnames" );
+ const bool addworld = args.takeArg( "-world" );
+
+ /* do some path mangling */
+ strcpy( source, ExpandArg( fileName2 ) );
+ path_set_extension( source, ".bsp" );
+
+ /* load the bsp */
+ Sys_Printf( "Loading %s\n", source );
+ LoadBSPFile( source );
+ ParseEntities();
+
+ struct bsp
+ {
+ std::vector entities;
+ std::vector bspModels;
+ std::vector bspShaders;
+ std::vector bspLeafs;
+ std::vector bspPlanes;
+ std::vector bspNodes;
+ std::vector bspLeafSurfaces;
+ std::vector bspLeafBrushes;
+ std::vector bspBrushes;
+ std::vector bspBrushSides;
+ std::vector bspLightBytes;
+ std::vector bspGridPoints;
+ std::vector bspVisBytes;
+ std::vector bspDrawVerts;
+ std::vector bspDrawIndexes;
+ std::vector bspDrawSurfaces;
+ std::vector bspFogs;
+ } bsp;
+
+ bsp.entities = std::move( entities );
+ bsp.bspModels = std::move( bspModels );
+ bsp.bspShaders = std::move( bspShaders );
+ bsp.bspLeafs = std::move( bspLeafs );
+ bsp.bspPlanes = std::move( bspPlanes );
+ bsp.bspNodes = std::move( bspNodes );
+ bsp.bspLeafSurfaces = std::move( bspLeafSurfaces );
+ bsp.bspLeafBrushes = std::move( bspLeafBrushes );
+ bsp.bspBrushes = std::move( bspBrushes );
+ bsp.bspBrushSides = std::move( bspBrushSides );
+ bsp.bspLightBytes = std::move( bspLightBytes );
+ bsp.bspGridPoints = std::move( bspGridPoints );
+ bsp.bspVisBytes = std::move( bspVisBytes );
+ bsp.bspDrawVerts = std::move( bspDrawVerts );
+ bsp.bspDrawIndexes = std::move( bspDrawIndexes );
+ bsp.bspDrawSurfaces = std::move( bspDrawSurfaces );
+ bsp.bspFogs = std::move( bspFogs );
+
+ /* do some path mangling */
+ strcpy( source, ExpandArg( fileName1 ) );
+ path_set_extension( source, ".bsp" );
+
+ /* load the bsp */
+ Sys_Printf( "Loading %s\n", source );
+ LoadBSPFile( source );
+ ParseEntities();
+
+
+ /* reindex */
+ {
+ for( auto&& model : bsp.bspModels )
+ {
+ model.firstBSPSurface += bspDrawSurfaces.size();
+ model.firstBSPBrush += bspBrushes.size();
+ }
+
+ for( auto&& side : bsp.bspBrushSides ){
+ side.planeNum += bspPlanes.size();
+ side.shaderNum += bspShaders.size();
+ side.surfaceNum += bspDrawSurfaces.size();
+ }
+
+ for( auto&& brush : bsp.bspBrushes )
+ {
+ brush.shaderNum += bspShaders.size();
+ brush.firstSide += bspBrushSides.size();
+ }
+
+ for( auto&& fog : bsp.bspFogs )
+ fog.brushNum += bspBrushes.size();
+
+ /* deduce max lm index, using bspLightBytes is insufficient for native external lightmaps */
+ int maxLmIndex = -3;
+ for( const auto& surf : bspDrawSurfaces )
+ for( auto index : surf.lightmapNum )
+ value_maximize( maxLmIndex, index );
+ for( auto&& surf : bsp.bspDrawSurfaces )
+ {
+ surf.shaderNum += bspShaders.size();
+ surf.fogNum += bspFogs.size();
+ surf.firstVert += bspDrawVerts.size();
+ surf.firstIndex += bspDrawIndexes.size();
+ for( auto&& index : surf.lightmapNum )
+ if( index >= 0 && maxLmIndex >= 0 )
+ index += maxLmIndex + 1;
+ }
+
+ ENSURE( bsp.entities[0].classname_is( "worldspawn" ) );
+ for( auto&& e : bsp.entities )
+ {
+ const char *model = e.valueForKey( "model" );
+ if( model[0] == '*' ){
+ e.setKeyValue( "model", StringOutputStream( 8 )( '*', atoi( model + 1 ) + bspModels.size() - 1 ) ); // -1 : minus world
+ }
+ }
+ /* make target/targetname names unique */
+ if( fixnames ){ ///! assumes there are no dummy targetnames to fix in incoming bsp
+ const auto has_name = []( const std::vector& entities, const char *name ){
+ for( auto&& e : entities )
+ if( striEqual( name, e.valueForKey( "target" ) )
+ || striEqual( name, e.valueForKey( "targetname" ) ) )
+ return true;
+ return false;
+ };
+ for( auto&& e : bsp.entities )
+ {
+ if( const char *name; e.read_keyvalue( name, "target" ) ){
+ if( has_name( entities, name ) ){
+ StringOutputStream newName;
+ int id = 0;
+ do{
+ newName( name, '_', id++ );
+ } while( has_name( entities, newName )
+ || has_name( bsp.entities, newName ) );
+
+ const CopiedString oldName = name; // backup it, original will change
+ for( auto&& e : bsp.entities )
+ for( auto&& ep : e.epairs )
+ if( ( striEqual( ep.key.c_str(), "target" ) || striEqual( ep.key.c_str(), "targetname" ) )
+ && striEqual( ep.value.c_str(), oldName.c_str() ) )
+ ep.value = newName;
+ }
+ }
+ }
+ }
+ }
+
+ {
+ entities.insert( entities.cend(), bsp.entities.cbegin() + 1, bsp.entities.cend() ); // minus world
+ numBSPEntities = entities.size();
+ bspModels.insert( bspModels.cend(), bsp.bspModels.cbegin() + 1, bsp.bspModels.cend() ); // minus world
+ bspShaders.insert( bspShaders.cend(), bsp.bspShaders.cbegin(), bsp.bspShaders.cend() );
+ // bspLeafs
+ bspPlanes.insert( bspPlanes.cend(), bsp.bspPlanes.cbegin(), bsp.bspPlanes.cend() );
+ // bspNodes
+ // bspLeafSurfaces
+ // bspLeafBrushes
+ bspBrushes.insert( bspBrushes.cend(), bsp.bspBrushes.cbegin(), bsp.bspBrushes.cend() );
+ bspBrushSides.insert( bspBrushSides.cend(), bsp.bspBrushSides.cbegin(), bsp.bspBrushSides.cend() );
+ bspLightBytes.insert( bspLightBytes.cend(), bsp.bspLightBytes.cbegin(), bsp.bspLightBytes.cend() );
+ // bspGridPoints
+ // bspVisBytes
+ bspDrawVerts.insert( bspDrawVerts.cend(), bsp.bspDrawVerts.cbegin(), bsp.bspDrawVerts.cend() );
+ bspDrawIndexes.insert( bspDrawIndexes.cend(), bsp.bspDrawIndexes.cbegin(), bsp.bspDrawIndexes.cend() );
+ bspDrawSurfaces.insert( bspDrawSurfaces.cend(), bsp.bspDrawSurfaces.cbegin(), bsp.bspDrawSurfaces.cend() );
+ bspFogs.insert( bspFogs.cend(), bsp.bspFogs.cbegin(), bsp.bspFogs.cend() );
+ }
+
+ if( addworld ){
+ /* insert new world surfaces */
+ const std::vector surfs( bspDrawSurfaces.cbegin() + bsp.bspModels[0].firstBSPSurface,
+ bspDrawSurfaces.cbegin() + bsp.bspModels[0].firstBSPSurface + bsp.bspModels[0].numBSPSurfaces );
+ bspDrawSurfaces.insert( bspDrawSurfaces.cbegin() + bspModels[0].firstBSPSurface + bspModels[0].numBSPSurfaces,
+ surfs.cbegin(), surfs.cend() );
+ // reindex
+ for( auto&& index : bspLeafSurfaces )
+ if( index >= bspModels[0].firstBSPSurface + bspModels[0].numBSPSurfaces )
+ index += surfs.size();
+ for( auto&& side : bspBrushSides )
+ if( side.surfaceNum >= bspModels[0].firstBSPSurface + bspModels[0].numBSPSurfaces )
+ side.surfaceNum += surfs.size();
+ for( auto&& model : bspModels )
+ if( model.firstBSPSurface >= bspModels[0].firstBSPSurface + bspModels[0].numBSPSurfaces )
+ model.firstBSPSurface += surfs.size();
+ bspModels[0].numBSPSurfaces += surfs.size();
+ /* insert new world brushes */
+ const std::vector brushes( bspBrushes.cbegin() + bsp.bspModels[0].firstBSPBrush,
+ bspBrushes.cbegin() + bsp.bspModels[0].firstBSPBrush + bsp.bspModels[0].numBSPBrushes );
+ bspBrushes.insert( bspBrushes.cbegin() + bspModels[0].firstBSPBrush + bspModels[0].numBSPBrushes,
+ brushes.cbegin(), brushes.cend() );
+ // reindex
+ for( auto&& index : bspLeafBrushes )
+ if( index >= bspModels[0].firstBSPBrush + bspModels[0].numBSPBrushes )
+ index += brushes.size();
+ for( auto&& fog : bspFogs )
+ if( fog.brushNum >= bspModels[0].firstBSPBrush + bspModels[0].numBSPBrushes )
+ fog.brushNum += brushes.size();
+ for( auto&& model : bspModels )
+ if( model.firstBSPBrush >= bspModels[0].firstBSPBrush + bspModels[0].numBSPBrushes )
+ model.firstBSPBrush += brushes.size();
+ bspModels[0].numBSPBrushes += brushes.size();
+ /* reference surfaces */
+ for( auto end = bspDrawSurfaces.cbegin() + bspModels[0].firstBSPSurface + bspModels[0].numBSPSurfaces,
+ surf = end - surfs.size(); surf != end; ++surf ){
+ MinMax minmax; // cheap minmax test
+ if( surf->surfaceType == MST_BAD ){
+ continue;
+ }
+ else if( surf->surfaceType == MST_PATCH ){
+ minmax = { surf->lightmapVecs[0], surf->lightmapVecs[1] };
+ }
+ else{
+ for( int i = 0; i < surf->numIndexes; ++i )
+ minmax.extend( bspDrawVerts[surf->firstVert + bspDrawIndexes[surf->firstIndex + i]].xyz );
+ }
+
+ for( auto&& leaf : bspLeafs ){
+ if( leaf.minmax.test( minmax ) ){
+ for( auto&& l : bspLeafs )
+ if( &l != &leaf && l.firstBSPLeafSurface >= leaf.firstBSPLeafSurface )
+ ++l.firstBSPLeafSurface;
+
+ bspLeafSurfaces.insert( bspLeafSurfaces.cbegin() + leaf.firstBSPLeafSurface, int( std::distance( bspDrawSurfaces.cbegin(), surf ) ) );
+ ++leaf.numBSPLeafSurfaces;
+ }
+ }
+ }
+ /* reference brushes */
+ /* convert bsp planes to map planes */
+ mapplanes.resize( bspPlanes.size() );
+ for ( size_t i = 0; i < bspPlanes.size(); ++i )
+ {
+ mapplanes[i].plane = bspPlanes[i];
+ }
+
+ for( auto end = bspBrushes.cbegin() + bspModels[0].firstBSPBrush + bspModels[0].numBSPBrushes,
+ brush = end - brushes.size(); brush != end; ++brush ){
+ buildBrush.sides.clear();
+ for( auto side = bspBrushSides.cbegin() + brush->firstSide, end = side + brush->numSides; side != end; ++side ){
+ auto& s = buildBrush.sides.emplace_back();
+ s.planenum = side->planeNum;
+ }
+ if( CreateBrushWindings( buildBrush ) ){
+ // cheap minmax test
+ for( auto&& leaf : bspLeafs ){
+ if( leaf.minmax.test( buildBrush.minmax ) ){
+ for( auto&& l : bspLeafs )
+ if( &l != &leaf && l.firstBSPLeafBrush >= leaf.firstBSPLeafBrush )
+ ++l.firstBSPLeafBrush;
+
+ bspLeafBrushes.insert( bspLeafBrushes.cbegin() + leaf.firstBSPLeafBrush, int( std::distance( bspBrushes.cbegin(), brush ) ) );
+ ++leaf.numBSPLeafBrushes;
+ }
+ }
+ }
+ }
+ }
+
+
+ /* inject command line parameters */
+ InjectCommandLine( "-mergebsp", argsToInject );
+
+ /* write the bsp */
+ UnparseEntities();
+ path_set_extension( source, "_merged.bsp" );
+ Sys_Printf( "Writing %s\n", source );
+ WriteBSPFile( source );
+
+ /* return to sender */
+ return 0;
+}
+
+
/*
PseudoCompileBSP()
a stripped down ProcessModels
diff --git a/tools/quake3/q3map2/help.cpp b/tools/quake3/q3map2/help.cpp
index c3319ab8..c4622d5b 100644
--- a/tools/quake3/q3map2/help.cpp
+++ b/tools/quake3/q3map2/help.cpp
@@ -433,6 +433,17 @@ void HelpJson()
HelpOptions("BSP json export/import", 0, 80, options);
}
+void HelpMergeBsp()
+{
+ const std::vector options = {
+ {"-mergebsp [options] ", "Inject latter BSP to former. Tree and vis data of the main one are preserved."},
+ {"-fixnames", "Make incoming BSP target/targetname names unique to not collide with existing names"},
+ {"-world", "Also merge worldspawn model (brushes as if they were detail, no BSP tree is affected) (only merges entities by default)"},
+ };
+
+ HelpOptions("BSP merge", 0, 80, options);
+}
+
void HelpCommon()
{
const std::vector options = {
@@ -486,6 +497,7 @@ void HelpMain(const char* arg)
{"-pk3", "PK3 creation"},
{"-repack", "Maps repack creation"},
{"-json", "BSP json export/import"},
+ {"-mergebsp", "BSP merge"},
};
void(*help_funcs[])() = {
HelpBsp,
@@ -504,6 +516,7 @@ void HelpMain(const char* arg)
HelpPk3,
HelpRepack,
HelpJson,
+ HelpMergeBsp,
};
if ( !strEmptyOrNull( arg ) )
diff --git a/tools/quake3/q3map2/main.cpp b/tools/quake3/q3map2/main.cpp
index 73d1fdfc..cadb318e 100644
--- a/tools/quake3/q3map2/main.cpp
+++ b/tools/quake3/q3map2/main.cpp
@@ -225,6 +225,11 @@ int main( int argc, char **argv ){
r = ConvertJsonMain( args );
}
+ /* merge two bsps */
+ else if ( args.takeFront( "-mergebsp" ) ) {
+ r = MergeBSPMain( args );
+ }
+
/* div0: minimap */
else if ( args.takeFront( "-minimap" ) ) {
r = MiniMapBSPMain( args );
diff --git a/tools/quake3/q3map2/q3map2.h b/tools/quake3/q3map2/q3map2.h
index 1d70c2c7..70fe6455 100644
--- a/tools/quake3/q3map2/q3map2.h
+++ b/tools/quake3/q3map2/q3map2.h
@@ -1528,6 +1528,7 @@ int AnalyzeBSP( Args& args );
int BSPInfo( Args& args );
int ScaleBSPMain( Args& args );
int ShiftBSPMain( Args& args );
+int MergeBSPMain( Args& args );
int ConvertBSPMain( Args& args );
/* convert_map.c */