Skip to content

Merging workflow

wootguy edited this page Jul 26, 2020 · 13 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 info command will tell you which models are worth considering.
bspguy info echoes14 -limit clipnodes

       Classname                  Targetname          Model  Clipnodes   Usage
-------------------------  -------------------------  -----  ----------  --------
worldspawn                                            *0          10733    66.0%
func_rotating                                         *34           360     2.2%
func_rotating                                         *22           359     2.2%
func_rotating                                         *35           359     2.2%
func_rotating                                         *38           359     2.2%
func_rotating                                         *79           355     2.2%
func_rotating                                         *10           346     2.1%
func_rotating                                         *11           346     2.1%
func_rotating                                         *24           346     2.1%
func_wall                                             *62           206     1.3%

Nothing can be done about worldspawn, but those func_rotating entities look like good candidates. After opening the map in BSP Viewer you can see that those are the ceiling fans in the garage area.

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.
(you might want to start using a script to run these commands)

copy echoes14.bsp echoes14_temp.bsp
bspguy noclip echoes14_temp -hull 2 -model 34
bspguy noclip echoes14_temp -hull 2 -model 22
bspguy noclip echoes14_temp -hull 2 -model 35
bspguy noclip echoes14_temp -hull 2 -model 38
bspguy noclip echoes14_temp -hull 2 -model 79
bspguy noclip echoes14_temp -hull 2 -model 10
bspguy noclip echoes14_temp -hull 2 -model 11
bspguy noclip echoes14_temp -hull 2 -model 24
bspguy merge test.bsp -maps "echoes14_temp, echoes14b"
 Data Type     Current / Max       Fullness
------------  -------------------  --------
...
clipnodes        32653 / 32767       99.7%
...

Just barely!

Out of curiosity, let's check the other map:
bspguy info echoes14b -limit clipnodes

       Classname                  Targetname          Model  Clipnodes   Usage
-------------------------  -------------------------  -----  ----------  --------
worldspawn                                            *0          11949    69.0%
func_train                 jet1                       *39          1479     8.5%
func_train                 jet2                       *17          1474     8.5%
func_train                 tonk                       *42           971     5.6%
func_illusionary           to_il                      *37           114     0.7%
func_breakable                                        *25            69     0.4%
func_conveyor                                         *20            69     0.4%
func_conveyor                                         *21            69     0.4%
func_breakable                                        *22            69     0.4%
func_breakable                                        *23            69     0.4%

jet1, jet2 and tonk are hogging tons of clipnodes! Aren't those just cinematic props you see outside the window?

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. I guess someone forgot to compile the map with -clipeconomy.

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

copy echoes14b.bsp echoes14b_temp.bsp
bspguy noclip echoes14b_temp -model 39
bspguy noclip echoes14b_temp -model 17
bspguy noclip echoes14b_temp -model 42
bspguy noclip echoes14b_temp -model 37
bspguy merge test.bsp -maps "echoes14_temp, echoes14b_temp"
 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 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 sorted by clipnode usage:

       Classname                  Targetname          Model  Clipnodes   Usage
-------------------------  -------------------------  -----  ----------  --------
worldspawn                                            *0          11326    76.8%
func_wall_toggle           sci_cart                   *53           199     1.3%
func_wall_toggle           sci_cart                   *54           170     1.2%
func_wall                  quake1_light1              *49           156     1.1%
func_wall                                             *75           124     0.8%
func_wall                  quake1_light2              *48            86     0.6%
func_wall                  quake2_light2              *42            72     0.5%
func_door                  toilet1                    *61            66     0.4%
func_door_rotating         exit_door2                 *27            60     0.4%
func_door_rotating         exit_door1                 *25            58     0.4%

The sci_cart entities are found in a hallway, partially blocking the path. 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. 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.

bspguy simplify echoes01 -model 53

Simplifying HULL 1, 2, and 3 in model 53:
    Deleted 43 planes
    Deleted 181 clipnodes

Currently only axis-aligned bounding boxes are supported right now. This means that objects that are angled and/or not box-shaped won't work well with this.

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 delete it with the noclip command. For example, to delete hull 0 from model 1 in test.bsp: bspguy noclip test.bsp -model 1 -hull 0

Duplicate models

Sometimes, models are 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.

Duplicate models can be deleted with the delete command. For example, if models *1 and *2 were duplicates, you could run bspguy delete test.bsp -model 2 and then update the model key in the entities that used *2. You might also have to update the origin of the entity.

Map Script Setup

Bspguy comes with map scripts that are required for playing merged maps. 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.