Tutorial: Going beyond 8192 units: Part 1 Last edited 1 month ago2020-04-09 10:47:25 UTC

Note: This tutorial is meant only for custom mods and requires some basic knowledge of C++

One day, 14-year-old me wanted to make a huge, giant map. However, he couldn't. Entities stopped working as soon as they reached 4096 units in any of the axes.
You've all heard of the infamous limitation that entities can't go beyond +-4096 units, and players can't go beyond +-8192 units. Believe it or not, this is not an engine limit! (at least since 2003)

It's very simple and straightforward to do, so let's get straight into it.

Modifications in the HL SDK

First of all, head down to CBaseEntity::IsInWorld() in cbase.cpp.
BOOL CBaseEntity :: IsInWorld( void )
{
    // position
    if (pev->origin.x >= 4096) return FALSE;
    if (pev->origin.y >= 4096) return FALSE;
    if (pev->origin.z >= 4096) return FALSE;
    if (pev->origin.x <= -4096) return FALSE;
    if (pev->origin.y <= -4096) return FALSE;
    if (pev->origin.z <= -4096) return FALSE;
    // speed
    if (pev->velocity.x >= 2000) return FALSE;
    if (pev->velocity.y >= 2000) return FALSE;
    if (pev->velocity.z >= 2000) return FALSE;
    if (pev->velocity.x <= -2000) return FALSE;
    if (pev->velocity.y <= -2000) return FALSE;
    if (pev->velocity.z <= -2000) return FALSE;

    return TRUE;
}
This is where you'll make the first edit. You might wanna either define a macro, or a constexpr int, or just replace the numbers, for example, 65536 instead of 4096.

IsInWorld() is generally called by some entities that have the potential of falling out of the map, like through the skybox.
However, not all such entities call IsInWorld().

In controller.cpp, around line 1210:
// check world boundaries
if (gpGlobals->time - pev->dmgtime > 5 || pev->renderamt < 64 || m_hEnemy == NULL || m_hOwner == NULL || pev->origin.x < -4096 || pev->origin.x > 4096 || pev->origin.y < -4096 || pev->origin.y > 4096 || pev->origin.z < -4096 || pev->origin.z > 4096)
{
    SetTouch( NULL );
    UTIL_Remove( this );
    return;
}
In apache.cpp, around line 1030:
// check world boundaries
if (pev->origin.x < -4096 || pev->origin.x > 4096 || pev->origin.y < -4096 || pev->origin.y > 4096 || pev->origin.z < -4096 || pev->origin.z > 4096)
{
    UTIL_Remove( this );
    return;
}
It would be a good idea to replace these pev->origin comparisons with a single IsInWorld() call.
However, this is not done yet.

Modifications to delta.lst

delta.lst is the biggest reason why this is possible. delta.lst defines how many bits are used to transfer data for a certain variable and at which precision. Let's open it up and see what we gotta look for.
clientdata_t none
{
    DEFINE_DELTA( flTimeStepSound, DT_INTEGER, 10, 1.0 ),
    DEFINE_DELTA( origin[0], DT_SIGNED | DT_FLOAT, 21, 128.0 ),
    DEFINE_DELTA( origin[1], DT_SIGNED | DT_FLOAT, 21, 128.0 ),
    DEFINE_DELTA( velocity[0], DT_SIGNED | DT_FLOAT, 16, 8.0 ),
    DEFINE_DELTA( velocity[1], DT_SIGNED | DT_FLOAT, 16, 8.0 ),
Hmm hmm hmm.
clientdata_t defines encoding for the local player.

The flags DT_SIGNED and DT_FLOAT are pretty much self-explanatory, however, what we're truly interested in are the two numbers: 21 and 128.
21 is the number of bits being transmitted for origin.x, for example. And at the precision of 1/128 of a unit.
So, hmm, what does this mean? What's the maximum value origin.x could have?

2^21 is 2'097'152‬. Divided by 128, it's 16384. Wow! That exactly matches the range that the player can move in, which is -8192 units to +8192 units.

From this, we can imagine a formula which we'll use to calculate the number of bits we need:

sign bit + value bits + precision bits

Let's imagine we want +-8192 units at 1/128 precision.
Precision bits is 7 because 128 is 2^7. Sign bit is always 1. Value bits is 13 because 8192 is 2^13.
When you add these up, you get 21, just like the original value in delta.lst

Here's a cheat sheet, assuming you won't change the precision (128.0) to any other value:
21 -> +-8192
22 -> +-16384
23 -> +-32768
24 -> +- 65536
25 -> +-131072

You probably don't need to go any further than that, especially for multiplayer, where every bit counts. You'll generally replace the number of bits for origin[0], origin[1] and origin[2].

Now, that was for clientdata_t. Let's move on to entity_state_t:
entity_state_t gamedll Entity_Encode
{
    DEFINE_DELTA( animtime, DT_TIMEWINDOW_8, 8, 1.0 ),
    DEFINE_DELTA( frame, DT_FLOAT, 10, 4.0 ),
    DEFINE_DELTA( origin[0], DT_SIGNED | DT_FLOAT, 16, 8.0 ),
    DEFINE_DELTA( angles[0], DT_ANGLE, 16, 1.0 ),
    DEFINE_DELTA( angles[1], DT_ANGLE, 16, 1.0 ),
    DEFINE_DELTA( origin[1], DT_SIGNED | DT_FLOAT, 16, 8.0 ),
    DEFINE_DELTA( origin[2], DT_SIGNED | DT_FLOAT, 16, 8.0 ),
This encoding is for non-player entities.
Using the same formula, let's call it that way, we can calculate how far these can go.
2^16 is 65536, divided by 8 is 8192. So +-4096.

Here's a cheat sheet for these too:
18 -> +-16384
19 -> +-32768
20 -> +- 65536
21 -> +-131072

Ultimately, you must change origin[0], origin[1] and origin[2] bits here:
entity_state_player_t gamedll Player_Encode
{
    DEFINE_DELTA( animtime, DT_TIMEWINDOW_8, 8, 1.0 ),
    DEFINE_DELTA( frame, DT_FLOAT, 8, 1.0 ),
    DEFINE_DELTA( origin[0], DT_SIGNED | DT_FLOAT, 18, 32.0 ),
    DEFINE_DELTA( angles[0], DT_ANGLE, 16, 1.0 ),
    DEFINE_DELTA( angles[1], DT_ANGLE, 16, 1.0 ),
    DEFINE_DELTA( origin[1], DT_SIGNED | DT_FLOAT, 18, 32.0 ),
    DEFINE_DELTA( origin[2], DT_SIGNED | DT_FLOAT, 18, 32.0 ),
This is for players other than the local player.

To calculate how many bits you need for these, I'll leave that as an exercise to the reader.
For real.

Now you can go a long way in your maps, literally.
Video, proof of this concept

However, one last thing remains.

Modifications to the compilers

I personally recommend Solokiller's VHLT-clean.
The original VHLT code is damn messy, with tons of #ifdefs and whatnot.

In the hlcsg project, in csg.h, you'll find this macro:
#define BOGUS_RANGE    65534
You may change that as you wish. For example, 262144. I added a parameter to the compiler so the mapper can choose, but I won't cover that in this tutorial.

Same thing goes for this line inside bspfile.h:
#define ENGINE_ENTITY_RANGE 4096.0
Alternatively, if you don't want to modify the compilers, you can use the Sven Co-op compilers, or my VHLT version.

To be continued

This is only part 1.
There are just a couple of things that don't work well in this new range. Precisely, it is the TE_ effects.
This means explosion sprites and decals, from grenades, rockets etc. will not show up where you expect them to. They will clamp back to 0,0,0.

Also pay attention to weapons, NPC smell and some other details, as weapons perform UTIL_TraceLine either 4096 or 8192 units in the aim direction. But, I suppose this won't be really necessary to change unless you're making a sniper mod. :)

Potentially, AI nodes may also not work very well as they're designed with +-8192 units in mind.
// Convert from [-8192,8192] to [0, 255]
//
inline int CALC_RANGE(int x, int lower, int upper)
{
    return NUM_RANGES*(x-lower)/((upper-lower+1));
}
This may need some changes. But otherwise, ANY entity is gonna work outside of 8192 units now. :)

Also, one word for level designers/mappers: it's important to understand that all the other limits still apply. So if you want to make a feasible map that is still 32k x 32k units in area or larger, you'll have to make them really low-detail in places, or use high texture scales, and definitely save on those clipnodes.

We'll see how to work around some of these 'bugs' in the next part of this tutorial, which, I'll likely write this summer. There will be a tutorial about mapping too, explaining some techniques for making huge maps.
Until then, happy programming!
-Admer

1 Comment

Commented 1 month ago2020-04-25 22:45:15 UTC Comment #102686
Thank you for this tutorial!

You must log in to post a comment. You can login or register a new account.