Skip to content

Merging workflow

wootguy edited this page Mar 31, 2025 · 14 revisions

Contents

First step

Let's say you want to merge echoes14.bsp and echoes14b.bsp together. First, try merging without any special options.

bspguy merge test.bsp -maps "echoes14, echoes14b"

After the process finishes, you'll see some stats about the new map file.

 Data Type     Current / Max       Fullness
------------  -------------------  --------
models             153 / 4096         3.7%
planes           14831 / 65535       22.6%
vertexes         31226 / 65535       47.6%
nodes            11647 / 32768       35.5%
texinfos          7091 / 32767       21.6%
faces            20846 / 65535       31.8%
clipnodes        33577 / 32767      102.5%  (OVERFLOW!!!)
leaves            7537 / 65536       11.5%
marksurfaces     28153 / 65536       43.0%
surfedges        98479 / 512000      19.2%
edges            52964 / 512000      10.3%
textures           272 / 4096         6.6%
lightdata          2.6 / 48.0  MB     5.4%
visdata            2.7 / 8.0   MB    34.0%
entities          1760 / 8192        21.5%

In this case, there were too many clipnodes in each map and they don't fit in the result file. They almost fit though. Read below to see what can be done about this.

Clipnodes reduction

First, try adding the -optimize option. If that's not enough, then also add -nohull2. If that's still not enough, then see the Unused Model Hulls and Simplify Model Hulls sections.

If those options were enough, but collision is broken for some things, then read on to understand why.

Delete Hull 2

The easiest way to reduce clipnodes is to try adding the -nohull2 option while merging.

bspguy merge test.bsp -maps "echoes14, echoes14b" -nohull2

 Data Type     Current / Max       Fullness
------------  -------------------  --------
...
clipnodes        23121 / 32767       70.6%
...

Problem solved! However, you usually shouldn't use this option for maps that have large monsters or large func_pushables. If you do, collision will be inaccurate for those entities.

Let's say one of the maps being merged did have large monsters, and the other didn't. In that case you can strip hull 2 from one of the maps before doing the merge, and still get a large reduction in clipnodes.

bspguy noclip echoes14 -hull 2 -o echoes14_nohull2
bspguy merge test.bsp -maps "echoes14_nohull2, echoes14b"
 Data Type     Current / Max       Fullness
------------  -------------------  --------
...
clipnodes        28473 / 32767       86.9%
...

Now let's say that both maps have large monsters. Read the next section to see how to deal with that.

Optimize

The -optimize flag conditionally deletes model hulls that appear to be unused. For example, some invisible entities like trigger_once don't always need hull 0 (used for rendering as well as collision), and some entities like func_illusionary don't need any clipnodes. -optimize will also delete hull 2 if there are no large monsters or pushables in the map.

The drawback to using this command is that it's possible for entities to change state in such a way that the deleted hulls are needed later. For instance, if a func_illusionary model is used with trigger_createentity to create a func_wall, then the func_wall would have no collision because -optimize would have deleted all collision hulls for the func_illusionary model. Similarly, it's possible for the game to crash if -optimize deletes hull 0 for an invisible entity, and that entity is later made visible due to a script or some entity logic.

If you use this option, you might also want to use the -v flag to see which models/entities had their hulls stripped. It might help debug problems with entity collision or crashing.

Unused Model Hulls

If you can't delete all of hull 2 (or if that's not enough) then you need to start selectively removing clipnodes from specific models in the map. The Map Limits widget in the 3D editor will tell you which models are worth considering. Click Widgets -> Map Limits.

image

Nothing can be done about worldspawn, but those func_rotating entities look like good candidates. Double click them to focus the camera on each of them. These are the ceiling fans in the garage area.

image

There's no way a large monster is going to be able to reach these fans normally, so let's delete hull 2 on these specific fan models, and try merging again. Select them all with Ctrl, then right click any of them and select Delete Hull -> Hull 2.

image

bspguy merge test.bsp -maps "echoes14, echoes14b"
 Data Type     Current / Max       Fullness
------------  -------------------  --------
...
clipnodes        32653 / 32767       99.7%
...

Just barely!

Out of curiosity, let's check the other map. Open echoes14b in the editor.

image

These func_train entities are hogging tons of clipnodes! Aren't those just cinematic props you see outside a window?

image

If you check in-game, you'll see that not only are those entities unreachable, but they have collision disabled anyway. You can walk right through the tonk and all the other vehicles outside. Also, why does that func_illusionary have any clipnodes? func_illusionary is supposed to be non-solid.

It's safe to say we can delete all the clipnode hulls for those entities. They're useless!

image

bspguy merge test.bsp -maps "echoes14, echoes14b"
 Data Type     Current / Max       Fullness
------------  -------------------  --------
...
clipnodes        28615 / 32767       87.3%
...

Much better. Note that the -optimize command would have deleted these hulls automatically because they're marked as non-solid, but manual work will be needed to delete hulls for things that are solid and unreachable.

Simplify model hulls

In this scenario, you've found a model that uses up a lot of clipnodes, but you can't just delete its hulls because the entity needs collision to work properly. Your last available option now is to simplify the collision data for that model.

Here are the models in echoes01.bsp that use the most clipnodes:

image

The func_wall_toggle entities are found in a hallway, partially blocking the path.

image

The clipnode hulls for these entities shouldn't be deleted because players can interact with them, but the hulls are way more complicated than they need to be. On the right screenshot I've enabled View -> Clipnodes -> Hull 1 to visualize clipnodes as a colored shell around the model.

As you can see, the hulls are pretty much just a box but with some slight bulging and rounded edges. You could replace the collision hulls in these models with a simple box and no one would know the difference. So, that's just what we'll do.

image

Simplifying replaces the clipnode hull with an axis-aligned bounding box. This means that objects that are angled and/or not box-shaped won't work well with this.

image

Other limits

Other limits you are likely to hit are nodes, vertexes, and marksurfaces. These all have the same culprit - too many polygons and/or complicated structures in the map.

Hull 0

Deleting hull 0 in a model reduces all types of data usage except clipnodes, but be warned, the game will crash if you delete hull 0 in these scenarios:

  • The entity is visible = game crash the first time you shoot
  • The entity is solid = game crash when you stand on it

The -optimize flag will automatically delete hull 0 for models that don't appear to need them, so try that first. If -optimize causes instability or collision problems, use the -v option with -optimize to get a list of all the models that had their hulls deleted. Then you can selectively delete hulls from the models in that list.

An example of an entity that doesn't need hull 0 would be func_tankcontrols. This is because it's invisible and doesn't interact with point-sized entities or bullets. A more complex example would be trigger_once. While it's an invisible entity, it can be triggered by point-size entities if the "Everything else" flag is checked. If that flag is checked (or if that flag is ever added during gameplay), then deleting hull 0 might break the map.

If you know an entity can function properly without hull 0, then select it and click Delete Hull -> Hull 0. The hull displayed in the editor swill switch to one of the collision hulls after this.

image

The "Shared Data" warning can be ignored because we're not touching any structures that could potentially be shared. This warning just prevents you from using some features in the Transformation widget.

Models

BSP models are often duplicated and used all over the map (e.g. picard coins in keen halloween). Duplicate models can be deleted and the remaining model can be used in multiple entities. Be aware that decals and lighting will be duplicated on entities that use the same model, but that's probably a small price to pay for the reduction in vertexes/nodes/etc.

Use the Deduplicate models option in the Tools menu to remove duplicate models. BSP data is not deleted until you use the Clean option. This is useful if you want to lower the model count without triggering a "Your map differs from the servers" error for players. Said differently, this is a ripent-safe option.

Map Script Setup

Bspguy comes with map scripts that are required for playing maps merged without -noscript. Install the bspguy scripts by extracting the scripts folder in the bspguy archive to svencoop_addon.

When a map is merged, an .ent file is generated along with the merged map. The .ent file needs to be copied to scripts/maps/bspguy/maps/. The script uses that file to create/delete entities as new map sections are entered. This file needs to be regenerated whenever the map entities change. That can be done with with ripent: ripent -export mapname

Lastly, add the following line to the map CFG for your merged map. This loads the map script.
map_script bspguy/v1/map

If your merged map already has its own map script, then you'll need to instead call the bspguy MapInit/MapActivate methods from within that script. For example, if your map uses the func_vehicle_custom script, then this is what the edited version should look like:

#include "func_vehicle_custom"
#include "bspguy/v1/bspguy"

void MapInit()
{
	VehicleMapInit( true, true );
	bspguy::MapInit();
}

void MapActivate()
{
	bspguy::MapActivate();
}

Note: the scripts are in versioned folders. v1 is the latest version at the time of this writing. Use the latest version.

If you really don't want to use map scripts, you can use the -noscript option with bspguy. Just be aware that maps with lots of ents may be laggy if you do this. There might also be more issues you'll have to fix with ripent (e.g. entities that had their classes changed to something less configurable).

Some entities are always edited so that levels can be played one after another (e.g. trigger_changelevel -> trigger_once). If you want to do all the ripent changes yourself, then add the -noripent option as well. This will merge maps without touching any entity logic.

Special Targets

The bspguy script creates 3 trigger_script entities. These control entity loading and cleanup

  • Target: bspguy_mapchange
    • loads entities in the next map section
      • The section name is read from a custom keyvalue in the !caller ($s_bspguy_next_map)
    • respawns all players in the next level
    • deletes entities in the current level
      • The section name is read from a custom keyvalue in the !caller ($s_bspguy_map_source)
  • Target: bspguy_mapload
    • loads entities in a map section.
      • The section name is read from a custom keyvalue in the !caller ($s_bspguy_next_map)
  • Target: bspguy_mapclean
    • deletes entities in a map section
      • The section name is read from a custom keyvalue in the !caller ($s_bspguy_map_source)

You don't need to call these manually unless you're setting up seamless transitions. If you mix seamless transitions with the default ones, then you might want to call bspguy_mapclean manually for maps that were part of a seamless section.

Repeat calls to mapload/mapclean/mapchange are ignored, if the same maps are requested.

Console Commands

Type bspguy in console for debug commands.

To quickly test each map section, use the bspguy mapchange command. In addition to skipping sections, this can also restart the current section or go back to previous ones.

Clone this wiki locally