Tutorial: Going beyond 8192 units: Part 2 Last edited 7 months ago2023-08-30 20:08:53 UTC

Note: This tutorial is meant only for custom mods and requires some basic knowledge of C++.
It is assumed you already know how to add HUD components and work with user messages.
This tutorial is a work in progress. Some parts are not finished. You may contribute to the tutorial or simply wait until it is.
In the first part of this tutorial, you have managed to add in basic support for large map sizes. However, you soon found out that temporary entities don't work beyond that. They simply get clamped to the centre of the map.
MESSAGE_BEGIN( MSG_PAS, SVC_TEMPENTITY, pev->origin );
WRITE_BYTE( TE_EXPLOSION );
WRITE_COORD( pev->origin.x );
WRITE_COORD( pev->origin.y );
WRITE_COORD( pev->origin.z );
...
MESSAGE_END();
WRITE_COORD has a hardcoded maximum of ±8192 units, not affected by the values in delta.lst at all. In order to work around this, we can write a custom user message and intercept all instances of SVC_TEMPENTITY to use that custom message.

Once the message is implemented, there will be a significant amount of find-and-replace work. There is really no easy way out. You may either go that route, or intelligently detect when temporary entities are being written to a message, and switch what WRITE_COORDS actually points to. For the purposes of this tutorial, we'll go with the former.

In the end, it would look something like this:
MESSAGE_BEGIN( MSG_PAS, gmsgTempEntity, pev->origin );
WRITE_BYTE( TE_EXPLOSION );
WRITE_LONG( pev->origin.x );
WRITE_LONG( pev->origin.y );
WRITE_LONG( pev->origin.z );
...
MESSAGE_END();

Temporary entity user message

For now, we're only going to implement the explosion TE, so you can get the general idea of the workflow.

In hud.h you may add this:
// Custom temporary entities because some builtin ones
// simply do not support large (+8192) map sizes
class CHudTemporaryEntities final : public CHudBase
{
public:
    int Init() override;
    int MsgFunc_TempEnt(const char* pszName, int iSize, void* pbuf);

private:
    void CreateExplosion(); // each TE will have its own method
};
You may add CHudTemporaryEntities m_TempEnts; to CHud, and actually register this in the HUD system.

Here is the implementation:
#include "hud.h"
#include "cl_util.h"
#include "parsemsg.h"
#include "CustomTemporaryEntities.h"

DECLARE_MESSAGE( m_TempEnts, TempEnt );

int CHudTemporaryEntities::Init()
{
    HOOK_MESSAGE( CustomTE );

    m_iFlags = HUD_ACTIVE;
    gHUD.AddHudElem( this );

    return 1;
}

int CHudTemporaryEntities::MsgFunc_TempEnt( const char* pszName, int iSize, void* pBuf )
{
    BEGIN_READ( pBuf, iSize );

    uint8_t type = READ_BYTE();
    switch ( type )
    {
    case TemporaryEntities::Explosion: CreateExplosion(); break;
    default:
        gEngfuncs.Con_DPrintf( "ERROR: TempEnt: tempent type is unknown! (%u)\n", uint32_t(type) );
        break;
    }

    return 1;
}

void CHudTemporaryEntities::CreateExplosion()
{
    // Read 3 longs because the server will send 3 longs, duh
    Vector position = { READ_LONG(), READ_LONG(), READ_LONG() };
    int spriteIndex = READ_SHORT();
    float scale = READ_BYTE() / 10.0f;
    float framerate = READ_BYTE();
    int flags = READ_BYTE();

    // It just so happens that the TE explosion flags map directly to the engine API explosion flags, so we can pass them as-is
    gEngfuncs.pEfxAPI->R_Explosion( position, spriteIndex, scale, framerate, flags );
}
You may have noticed the CustomTemporaryEntities.h header. Here are its contents:
#pragma once

struct TemporaryEntities
{
    enum Type : uint8_t
    {
        // long long long (position)
        // short (sprite index)
        // byte (scale in tens of units)
        // byte (framerate)
        // byte (flags, refer to TE_EXPLFLAG_* constants)
        Explosion = TE_EXPLOSION,
    };
};
You may have noticed by now, that this allows us to have custom temporary effects, just like in Source.

Also, don't forget to do this on the serverside in LinkUserMessages:
gmsgTempEntities = REG_USER_MSG( "TempEnt", -1 );

Replacing SVC_TEMPENTITY references

Since we have a replacement for the explosion TE, we may begin replacing instances of it in the SDK. So begin by locating all the occurrences of TE_EXPLOSION: A few examples:

CApache::DyingThink

Before:
MESSAGE_BEGIN( MSG_PVS, SVC_TEMPENTITY, pev->origin );
WRITE_BYTE( TE_EXPLOSION );
WRITE_COORD( pev->origin.x + RANDOM_FLOAT( -150, 150 ) );
WRITE_COORD( pev->origin.y + RANDOM_FLOAT( -150, 150 ) );
WRITE_COORD( pev->origin.z + RANDOM_FLOAT( -150, -50 ) );
WRITE_SHORT( g_sModelIndexFireball );
WRITE_BYTE( RANDOM_LONG( 0, 29 ) + 30 );
WRITE_BYTE( 12 );
WRITE_BYTE( TE_EXPLFLAG_NONE );
MESSAGE_END();
After:
MESSAGE_BEGIN( MSG_PVS, gmsgTempEntity, pev->origin );
WRITE_BYTE( TE_EXPLOSION );
WRITE_LONG( pev->origin.x + RANDOM_FLOAT( -150, 150 ) );
WRITE_LONG( pev->origin.y + RANDOM_FLOAT( -150, 150 ) );
WRITE_LONG( pev->origin.z + RANDOM_FLOAT( -150, -50 ) );
WRITE_SHORT( g_sModelIndexFireball );
WRITE_BYTE( RANDOM_LONG( 0, 29 ) + 30 );
WRITE_BYTE( 12 );
WRITE_BYTE( TE_EXPLFLAG_NONE );
MESSAGE_END();

CEnvExplosion::Use

Before:
// draw fireball
if ( !(pev->spawnflags & SF_ENVEXPLOSION_NOFIREBALL) )
{
    MESSAGE_BEGIN( MSG_PAS, SVC_TEMPENTITY, pev->origin );
    WRITE_BYTE( TE_EXPLOSION );
    WRITE_COORD( pev->origin.x );
    WRITE_COORD( pev->origin.y );
    WRITE_COORD( pev->origin.z );
    WRITE_SHORT( g_sModelIndexFireball );
    WRITE_BYTE( (BYTE)m_spriteScale ); // scale * 10
    WRITE_BYTE( 15 ); // framerate
    WRITE_BYTE( TE_EXPLFLAG_NONE );
    MESSAGE_END();
}
else
{
    MESSAGE_BEGIN( MSG_PAS, SVC_TEMPENTITY, pev->origin );
    WRITE_BYTE( TE_EXPLOSION );
    WRITE_COORD( pev->origin.x );
    WRITE_COORD( pev->origin.y );
    WRITE_COORD( pev->origin.z );
    WRITE_SHORT( g_sModelIndexFireball );
    WRITE_BYTE( 0 ); // no sprite
    WRITE_BYTE( 15 ); // framerate
    WRITE_BYTE( TE_EXPLFLAG_NONE );
    MESSAGE_END();
}
After:
// draw fireball
if ( !(pev->spawnflags & SF_ENVEXPLOSION_NOFIREBALL) )
{
    MESSAGE_BEGIN( MSG_PAS, gmsgTempEntity, pev->origin );
    WRITE_BYTE( TE_EXPLOSION );
    WRITE_LONG( pev->origin.x );
    WRITE_LONG( pev->origin.y );
    WRITE_LONG( pev->origin.z );
    WRITE_SHORT( g_sModelIndexFireball );
    WRITE_BYTE( (BYTE)m_spriteScale ); // scale * 10
    WRITE_BYTE( 15 ); // framerate
    WRITE_BYTE( TE_EXPLFLAG_NONE );
    MESSAGE_END();
}
else
{
    MESSAGE_BEGIN( MSG_PAS, gmsgTempEntity, pev->origin );
    WRITE_BYTE( TE_EXPLOSION );
    WRITE_LONG( pev->origin.x );
    WRITE_LONG( pev->origin.y );
    WRITE_LONG( pev->origin.z );
    WRITE_SHORT( g_sModelIndexFireball );
    WRITE_BYTE( 0 ); // no sprite
    WRITE_BYTE( 15 ); // framerate
    WRITE_BYTE( TE_EXPLFLAG_NONE );
    MESSAGE_END();
}

Other temporary entities

TODO: implement other temporary entities!

The end

You now know how to fully enable large maps in Half-Life. Enjoy making your dream open-world mod come true! (other engine limits still apply)

Comments

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