Fixing VGUI Screens in Source 2007

From Valve Developer Community
Jump to navigation Jump to search

About

This is a comprehensive guide to fixing the Source 2007 mod in-game VGUI screens. While there are two existing tutorials on this, neither provides interactivity functionality when implemented in the Source 2007 code. HOWEVER, both tutorials provide essential parts of the solution, and this fix is a combination of the two with some additions.

This is a rather long process, and is broken down by component. Working versions of this code will be available at the end of the tutorial, so if you get stuck or something doesn't work as described, go there and compare what you have with what's posted.

Step 1: Crash Fix

Note.pngNote:This section is a slight modification of the one in VGUI_Screen_Creation. It has been edited for accuracy and clarity, but the concept and method are the same.)

First, in src\game\shared\in_buttons.h, add the following:

   #define IN_VALIDVGUIINPUT            (1 << 23) //bitflag for vgui fix

Next, in src\game\client\in_main.cpp, inside method CInput::CreateMove ( ... ), add:

   cmd->buttons |= IN_VALIDVGUIINPUT;

right above:

   g_pClientMode->CreateMove( input_sample_frametime, cmd );

After that, in src\game\client\c_baseplayer.cpp, inside method C_BasePlayer::CreateMove( ... ), add:

   if(pCmd->buttons & IN_VALIDVGUIINPUT)

right above:

   DetermineVguiInputMode( pCmd );

(This is so it only calls DetermineVguiInputMode if the buttons include our flag.)

Then, inside method C_BasePlayer::DetermineVguiInputMode( ... ), change both instances of:

   pCmd->buttons &= ~(IN_ATTACK | IN_ATTACK2);

to:

   pCmd->buttons &= ~(IN_ATTACK | IN_ATTACK2 | IN_VALIDVGUIINPUT);

Finally, in \src\game\client\c_vguiscreen.cpp, in the function C_VGuiScreen::ClientThink( void ), find if ( m_bLoseThinkNextFrame == true ) and add above it:

    for (int i = 0; i < pPanel->GetChildCount(); i++)
    {
        vgui::Button *child = dynamic_cast<vgui::Button*>(pPanel->GetChild(i));
        if ( child )
        {
            int x1, x2, y1, y2;
            child->GetBounds( x1, y1, x2, y2 );
            
            // Generate mouse input commands
            if ((m_nButtonState & IN_ATTACK) && !m_bWasTriggered)
            {
                if ( px >= x1 && px <= x1 + x2 && py >= y1 && py <= y1 + y2 )
                {
                    child->FireActionSignal();
                }
            }
        }
    }

Step 2: Interactivity

So now your mod will compile and run, and it won't crash when you look at a screen. Good job! But there's quite a ways to go, still. Next, interactivity has to be fixed.

Note.pngNote:Like the above section, this is based heavily on another fix, Interactive_Ingame_VGUI_Panels. Also like above, it has been edited for accuracy and clarity.)

First, in src\game\client\in_main.cpp, inside the function, void CInput::ExtraMouseSample( float frametime, bool active ), replace:

   if ( g_pClientMode->CreateMove( frametime, cmd ) )

with

   if ( g_pClientMode->CreateMove( frametime, cmd, true ) )

Next, in src\game\client\iclientmode.h, add:

   virtual bool CreateMove( float flInputSampleTime, CUserCmd *cmd, bool bVguiUpdate ) = 0;

above

   virtual bool CreateMove( float flInputSampleTime, CUserCmd *cmd ) = 0;

After that, in both files src\game\client\clientmode_shared.h and src\game\client\hl2\c_basehlplayer.h, add:

   virtual bool CreateMove( float flInputSampleTime, CUserCmd *cmd, bool bVguiUpdate );

above

   virtual bool CreateMove( float flInputSampleTime, CUserCmd *cmd );
Note.pngNote:In the original version of the tutorial, it says to do the top of the previous two changes to all three files. This does not work, as it makes the classes in the latter two files abstract.)

Then, in src\game\client\clientmode_shared.cpp, change:

bool ClientModeShared::CreateMove( float flInputSampleTime, CUserCmd *cmd )
{
    // Let the player override the view.
    C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
    if(!pPlayer)
        return true;

    // Let the player at it
    return pPlayer->CreateMove( flInputSampleTime, cmd );
}

to

bool ClientModeShared::CreateMove( float flInputSampleTime, CUserCmd *cmd, bool bVguiUpdate )
{
    // Let the player override the view.
    C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
    if(!pPlayer)
        return true;
 
    // Let the player at it
    return pPlayer->CreateMove( flInputSampleTime, cmd, bVguiUpdate );
}

bool ClientModeShared::CreateMove( float flInputSampleTime, CUserCmd *cmd )
{
    return CreateMove( flInputSampleTime, cmd, false );
}

In src\game\client\hl2\c_basehlplayer.cpp, replace:

bool C_BaseHLPlayer::CreateMove( float flInputSampleTime, CUserCmd *pCmd )
{
    bool bResult = BaseClass::CreateMove( flInputSampleTime, pCmd );

with

bool C_BaseHLPlayer::CreateMove( float flInputSampleTime, CUserCmd *pCmd , bool bVguiUpdate )
{
    bool bResult = BaseClass::CreateMove( flInputSampleTime, pCmd, bVguiUpdate );

    if ( !IsInAVehicle() )
    {
        PerformClientSideObstacleAvoidance( TICK_INTERVAL, pCmd );
        PerformClientSideNPCSpeedModifiers( TICK_INTERVAL, pCmd );
    }

    return bResult;
}

bool C_BaseHLPlayer::CreateMove( float flInputSampleTime, CUserCmd *pCmd )
{
    return CreateMove(flInputSampleTime, pCmd, false);
}

In src\game\client\c_baseplayer.cpp, replace:

bool C_BasePlayer::CreateMove( float flInputSampleTime, CUserCmd *pCmd )

with

bool C_BasePlayer::CreateMove( float flInputSampleTime, CUserCmd *pCmd, bool bVguiUpdate )

Above that, add:

bool C_BasePlayer::CreateMove( float flInputSampleTime, CUserCmd *pCmd )
{
    return CreateMove(flInputSampleTime, pCmd, false);
}

Then, inside that function, find:

DetermineVguiInputMode( pCmd );

and replace it with

    if (bVguiUpdate)
    {
        bool tempvguimode = IsInVGuiInputMode();
        // Check to see if we're in vgui input mode...
        DetermineVguiInputMode( pCmd );
        
        if (tempvguimode == !IsInVGuiInputMode())
        {
            if (IsInVGuiInputMode())
            {
                engine->ClientCmd( "vguimode_true" );
            }
            else
            {
                engine->ClientCmd( "vguimode_false" );
            }
        }
    }

As described in the original document, this will not only check if the player is interacting with a VGUI screen (which includes looking at it while in range of interaction), but will also tell the server that the player is interacting with a VGUI screen.

Next, in src\game\client\c_vguiscreen.cpp, add:

CUtlVector<C_VGuiScreen*> g_pVGUIScreens;

above

CUtlDict<KeyValues*, int> g_KeyValuesCache;

Inside C_VGuiScreen::C_VGuiScreen(), add at the end:

g_pVGUIScreens.AddToTail( this );

Inside C_VGuiScreen::~C_VGuiScreen(), add at the beginning:

g_pVGUIScreens.FindAndRemove( this );

After that, find C_BaseEntity *FindNearbyVguiScreen( const Vector &viewPosition, const QAngle &viewAngle, int nTeam ), and replace the entire function with:

C_BaseEntity *FindNearbyVguiScreen( const Vector &viewPosition, const QAngle &viewAngle, int nTeam )
{
    if ( IsX360() )
    {
        // X360TBD: Turn this on if feature actually used
        return NULL;
    }
 
    C_BasePlayer *pLocalPlayer = C_BasePlayer::GetLocalPlayer();
 
    Assert( pLocalPlayer );
 
    if ( !pLocalPlayer )
        return NULL;
 
    // Get the view direction...
    Vector lookDir;
    AngleVectors( viewAngle, &lookDir );
 
    // Create a ray used for raytracing 
    Vector lookEnd;
    VectorMA( viewPosition, 2.0f * VGUI_SCREEN_MODE_RADIUS, lookDir, lookEnd );
 
    Ray_t lookRay;
    lookRay.Init( viewPosition, lookEnd );
 
 
    Vector vecOut, vecViewDelta;
 
 
    float flBestDist = 2.0f;
    C_VGuiScreen *pBestScreen = NULL;
 
 
    for (int i = 0; i < g_pVGUIScreens.Count(); i++)
    {
        if (g_pVGUIScreens.IsValidIndex(i))
        {
            C_VGuiScreen *pScreen = g_pVGUIScreens[i];
 
            if ( pScreen->IsAttachedToViewModel() )
                continue;
 
            // Don't bother with screens I'm behind...
            // Hax - don't cancel backfacing with viewmodel attached screens.
            // we can get prediction bugs that make us backfacing for one frame and
            // it resets the mouse position if we lose focus.
            if ( pScreen->IsBackfacing(viewPosition) )
                continue;
 
            // Don't bother with screens that are turned off
            if (!pScreen->IsActive())
                continue;
 
            // FIXME: Should this maybe go into a derived class of some sort?
            // Don't bother with screens on the wrong team
            if (!pScreen->IsVisibleToTeam(nTeam))
                continue;
 
            if ( !pScreen->AcceptsInput() )
                continue;
 
            if ( pScreen->IsInputOnlyToOwner() && pScreen->GetPlayerOwner() != pLocalPlayer )
                continue;
 
            // Test perpendicular distance from the screen...
            pScreen->GetVectors( NULL, NULL, &vecOut );
            VectorSubtract( viewPosition, pScreen->GetAbsOrigin(), vecViewDelta );
            float flPerpDist = DotProduct(vecViewDelta, vecOut);
            if ( (flPerpDist < 0) || (flPerpDist > VGUI_SCREEN_MODE_RADIUS) )
                continue;
 
            // Perform a raycast to see where in barycentric coordinates the ray hits
            // the viewscreen; if it doesn't hit it, you're not in the mode
            float u, v, t;
            if (!pScreen->IntersectWithRay( lookRay, &u, &v, &t ))
                continue;
 
            // Barycentric test
            if ((u < 0) || (v < 0) || (u > 1) || (v > 1))
                continue;
 
            if ( t < flBestDist )
            {
                flBestDist = t;
                pBestScreen = pScreen;
            }
        }
    }
 
    return pBestScreen;
}

Next, in src\game\server\player.h, at the end of the CBasePlayer class declaration, add:

bool m_pVGUImode;
bool GetVGUIMode(void) {return m_pVGUImode;}
void SetVGUIMode(bool newmode) { m_pVGUImode = newmode;}

In src\game\server\player.cpp, at the end of the function void CBasePlayer::Spawn( void ), add:

SetVGUIMode(false);

In the function bool CBasePlayer::ClientCommand( const CCommand &args ), after the existing else if statements, add:

    else if ( stricmp( cmd, "vguimode_true" ) == 0 )
    {
        SetVGUIMode( true );
        return true;
    }
    else if ( stricmp( cmd, "vguimode_false" ) == 0 )
    {
        SetVGUIMode( false );
        return true;
    }

Finally, in src\game\server\hl2\hl2_player.cpp, find UpdateWeaponPosture(); and add below it:

// If we're in VGUI mode we should avoid shooting
if ( GetVGUIMode() )
{
	m_nButtons &= ~(IN_ATTACK|IN_ATTACK2);
}

And in void CHL2_Player::UpdateWeaponPosture( void ), below CBaseEntity *aimTarget = tr.m_pEnt;, add:

    if ( GetVGUIMode() )
    {
    //We're over a friendly, drop our weapon
    if ( Weapon_Lower() == false )
    {
        //FIXME: We couldn't lower our weapon!
    }

    return;
}

Step 3: Client-Server Interface

All of this is good and dandy, except that the screen doesn't actually communicate with the server. This is a problem, as it means that things are basically non-functional. There is, however, a fix for this.

Note.pngNote:This is a mostly-original implementation of the concept, and as such will be more heavily documented.

First, we need some supporting framework. In src\game\server\vguiscreen.h, at the end of the first public section, add:

static CUtlLinkedList<const char *, byte> m_UserData;

This will allow functions outside the class to add data to the list of things to be processed.

Next, create a new CPP file in the server library, referred to in this tutorial as 'dst_concommand' (my online handle being Henry Dorsett, I preface my work with 'Dst'). Then, add the following code to it:

#include "cbase.h"

void Dst_VGUI_RelayTrigger(const CCommand &args)
{
	char* verify = (char*)args.Arg(0);
	if (!Q_strncmp(verify, "DST_VGUI_RelayTrigger", 21)) // Only allow proper calls
	{
		for (int i = 1; i < args.ArgC(); i++)
		{
			CVGuiScreen::m_UserData.AddToTail(args.Arg(i));
		}
	}
}

static ConCommand dst_RelayTrigger("DST_VGUI_RelayTrigger", Dst_VGUI_RelayTrigger, "PRIVATE: Interface for VGUI triggers.", FCVAR_CHEAT);

What this does is it creates a ConCommand ("Console Command"), allowing the client to send information to the server. The function takes the first argument passed, which is the name of the ConCommand, and makes sure that it's actually there, just for safety's sake. If it is (and it had better be, otherwise there are bigger problems), it iterates through the list of args and adds them to the list we created above, except for the first one, as it's already been dealt with.

Next, we need a way of making sure the right data is getting sent. More to the point, we need to tell it exactly what data to send. We do this in the function void CVGuiScreenPanel::OnCommand(const char *command), in the file src\game\client\c_vguiscreen.cpp. Find the function, and replace it with this:

void CVGuiScreenPanel::OnCommand(const char *command)
{
    C_VGuiScreen *screen = dynamic_cast<C_VGuiScreen*>(m_hEntity.Get());
    CBasePlayer *pLocalPlayer = CBasePlayer::GetLocalPlayer();
    char* entity = new char[20];
    ultoa((unsigned long)pLocalPlayer, entity, 10);

    char* newcommand = new char[256];
    strcpy(newcommand, "DST_VGUI_RelayTrigger");
    strcat(newcommand, " ");
    strcat(newcommand, screen->PanelName());
    strcat(newcommand, " ");
    strcat(newcommand, entity);
    strcat(newcommand, " ");
    strcat(newcommand, command);
    strcat(newcommand, " ");

    if ( Q_stricmp( command, "vguicancel" ) )
    {
        engine->ClientCmd( const_cast<char *>( newcommand ) );
    }

     BaseClass::OnCommand(newcommand);
}

Let's break this down. First, we get the screen to which this panel belongs. Then, we get the local player (which may or may not work in multiplayer mods). After that, we convert the player's memory address into a string, and store it in the entity variable.

With the data processed, we just need to make a new command to send to the server, which matches what's expected (or rather, what will be expected, when we get to it) by the server. The first thing we toss in is the name of the command we want to send to the server, followed by a space (you could put the space at the end of the command, but I gave it its own line for my own personal clarity). After that, we toss in the name of the screen panel, followed by another space (the spaces being used for tokenization of the command). Then goes the string containing the memory address of the local player, with the requisite space. Lastly, we add in the command that the panel received, followed by another space, which may or may not be required, but better to have it, anyway.

When that's done, we check to make sure that the original command isn't "vguicancel". If not, we send our entire package off to the server.

Before we're done with the file, something's missing from the work we did on it earlier. To actually get the screen to think, to process data without making function calls all the time, we need to tell the engine that it has a think function, and how often we want it to be thinking. Inside void C_VGuiScreen::CreateVguiScreen( const char *pTypeName ), at the end, add:

ThinkSet(static_cast <void (CBaseEntity::*)(void)>(&C_VGuiScreen::ClientThink), 1, "Let's see if this works...");
SetNextClientThink(CLIENT_THINK_ALWAYS);

Now we get to handle the information we've successfully sent. First thing, we have to tell the server-side screen to think. Inside the file src\game\server\vguiscreen.cpp, find the function CVGuiScreen::CVGuiScreen(), and add at the end:

SetNextThink(gpGlobals->curtime);

Next, find the function void CVGuiScreen::Think(void), and replace it with this:

void CVGuiScreen::Think(void)
{
    for (byte i = 0; i < m_UserData.Count(); i++)
    {
        char* e = (char*)m_UserData.Element(i), *p = (char*)GetPanelName();
        if (!Q_strncmp(e, p, sizeof(GetPanelName())))
        {
            const char* handle = m_UserData.Element(++i);
            EHANDLE hEntity = EHANDLE::FromIndex(atoi(handle));
            const char* cmd = m_UserData.Element(++i);
            CBaseEntity *entity = (CBaseEntity*)atoi(handle);

            if (!Q_strncmp(cmd, "Output1", 7))
                Output[0].FireOutput(entity, this);
            else if (!Q_strncmp(cmd, "Output2", 7))
                Output[1].FireOutput(entity, this);
            else if (!Q_strncmp(cmd, "Output3", 7))
                Output[2].FireOutput(entity, this);
            else if (!Q_strncmp(cmd, "Output4", 7))
                Output[3].FireOutput(entity, this);
            else if (!Q_strncmp(cmd, "Output5", 7))
                Output[4].FireOutput(entity, this);
            else if (!Q_strncmp(cmd, "Output6", 7))
                Output[5].FireOutput(entity, this);
            else if (!Q_strncmp(cmd, "Output7", 7))
                Output[6].FireOutput(entity, this);
            else if (!Q_strncmp(cmd, "Output8", 7))
                Output[7].FireOutput(entity, this);
            else if (!Q_strncmp(cmd, "Output9", 7))
                Output[8].FireOutput(entity, this);
            else if (!Q_strncmp(cmd, "Output10", 8))
                Output[9].FireOutput(entity, this);
            else if (!Q_strncmp(cmd, "Output11", 8))
                Output[10].FireOutput(entity, this);
            else if (!Q_strncmp(cmd, "Output12", 8))
                Output[11].FireOutput(entity, this);
            else if (!Q_strncmp(cmd, "Output13", 8))
                Output[12].FireOutput(entity, this);
            else if (!Q_strncmp(cmd, "Output14", 8))
                Output[13].FireOutput(entity, this);
            else if (!Q_strncmp(cmd, "Output15", 8))
                Output[14].FireOutput(entity, this);
            else if (!Q_strncmp(cmd, "Output16", 8))
                Output[15].FireOutput(entity, this);

            m_UserData.PurgeAndDeleteElements();
        }
    }

    SetNextThink(gpGlobals->curtime + 0.1f);
}

Breakdown time. Top of the function, we have a for loop that iterates through each section of the command in m_UserData. First, we get char pointers to the command data at the iterator's current index, as well as one to the name of the panel currently owned by the screen. If they're equal, that is, if we're talking about the same panel, we move on.

After that, we increment the index, get the 'handle' (or pointer) of the entity which set this series of events into motion, store that in handle, and increment the index again. Then we pull the actual command to be evaluated, and store it in cmd, and increment the index again. Lastly in this stage, we create a new entity, and give it the address held in the handle string.

After that, we use a set of if-else statements to see if the command matches one of the outputs (or, rather, which of the outputs it matches). When the match is found, we fire the output, giving it the entity held by the handle as one parameter, and this, the screen doing the processing, as the other.

When all that's done, we just do a bit of clean-up, purging and deleting the contents of the data we just processed (so we don't process it again), and telling the entity to think again in another tenth of a second.

Summary

There you have it, functioning, interactive, in-game VGUI screens.

Credits

VGUI_Screen_Creation

Interactive_Ingame_VGUI_Panels

The Source developer Skype channel