Custom loading screen

From Valve Developer Community
Jump to: navigation, search
Showing the final results, in this case, loading a custom screen upon detecting a defined map. Note: The RES file for the loading pop-up was edited in order to remove borders so the background isn't "cut-off".

The following code allows us to have custom loading screens when loading maps, either unique ones depending on the map name, or a default one. The final aspect will make it look like in No More Room in Hell, or Obsidian Conflict.

This method overrides GameUI and is based on what it was hinted at this email from HLCoders.

Credit me (Maestra Fénix) if you want to use this, that's the sole condition.

Requirements

  • A mod running on Source SDK 2013
  • Knowledge of C++, RES, and VMT / VTF files.

Code

Place the following two files somewhere in client (if possible, in a subfolder in order to keep it tidy):

mapload_background.h

//======= Maestra Fenix, 2017 ==================================================//
//
// Purpose: Map load background panel
//
//==============================================================================//

#ifndef MAPLOAD_BACKGROUND_H
#define MAPLOAD_BACKGROUND_H

#ifdef _WIN32
#pragma once
#endif

#include "vgui\ISurface.h"
#include <vgui_controls/EditablePanel.h>
#include <vgui_controls\ImagePanel.h>
#include "ienginevgui.h"

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
class CMapLoadBG : public vgui::EditablePanel
{
	DECLARE_CLASS_SIMPLE( CMapLoadBG, vgui::EditablePanel);

public:
	// Construction
	CMapLoadBG( char const *panelName );
	~CMapLoadBG();

	void SetNewBackgroundImage( char const *imageName );

protected:
	void ApplySchemeSettings( vgui::IScheme *pScheme );

private:
	vgui::ImagePanel *m_pBackground;
};

#endif	// !MAPLOAD_BACKGROUND_H

mapload_background.cpp

//======= Maestra Fenix, 2017 ==================================================//
//
// Purpose: Map load background panel
//
//==============================================================================//

#include "cbase.h"
#include "mapload_background.h"

// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"

using namespace vgui;

//-----------------------------------------------------------------------------
// Purpose: Constructor
//-----------------------------------------------------------------------------
CMapLoadBG::CMapLoadBG( char const *panelName ) : EditablePanel( NULL, panelName )
{
	VPANEL toolParent = enginevgui->GetPanel( PANEL_GAMEUIDLL );
	SetParent( toolParent );

	// Fenix: We load a RES file rather than create the element here for taking advantage of the "F" parameter for wide and tall
	// Is the sole thing that makes fill the background to the entire screen regardless of the texture size
	// Congratulations to Valve for once again give options to only one side and not both
	LoadControlSettings( "resource/loadingdialogbackground.res" );

	m_pBackground = FindControl<ImagePanel>( "LoadingImage", true );
}

//-----------------------------------------------------------------------------
// Purpose: Destructor
//-----------------------------------------------------------------------------
CMapLoadBG::~CMapLoadBG()
{
	// None
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CMapLoadBG::ApplySchemeSettings( IScheme *pScheme )
{
	BaseClass::ApplySchemeSettings( pScheme );

	int iWide, iTall;
	surface()->GetScreenSize( iWide, iTall );
	SetSize( iWide, iTall );
}

//-----------------------------------------------------------------------------
// Purpose: Sets a new background on demand
//-----------------------------------------------------------------------------
void CMapLoadBG::SetNewBackgroundImage( char const *imageName )
{
	m_pBackground->SetImage( imageName );
}

clientmode_shared.cpp

Go to clientmode_shared.cpp and add the following between lines 42 and 44 (after #if defined( _X360 ) and before #if defined( REPLAY_ENABLED )):

// Fenix: Needed for the custom background loading screens
#include "GameUI/IGameUI.h"
#include "mapload_background.h"

Add this following snippet between lines 77 and 79 (after static vgui::HContext s_hVGuiContext = DEFAULT_VGUI_CONTEXT; and before ConVar cl_drawhud):

// Fenix: Needed for the custom background loading screens
// See interface.h/.cpp for specifics: basically this ensures that we actually Sys_UnloadModule the dll and that we don't call Sys_LoadModule 
// over and over again.
static CDllDemandLoader g_GameUI( "GameUI" );

Lines 87 and 89 (after extern bool IsInCommentaryMode( void ); and before #ifdef VOICE_VOX_ENABLE):

// Fenix: Needed for the custom background loading screens
CMapLoadBG *pPanelBg;
IMaterial *pMatMapBg;

In ClientModeShared(), add the following after line 278 (m_nRootSize[ 0 ] = m_nRootSize[ 1 ] = -1;):

// Fenix: Needed for the custom background loading screens
pPanelBg = NULL;
pMatMapBg = NULL;

At Init(), after line 368 (HOOK_MESSAGE( Rumble );):

	// Fenix: Custom background loading screens - Injects the custom panel at the loading screen
	CreateInterfaceFn gameUIFactory = g_GameUI.GetFactory();
	if ( gameUIFactory )
	{
		IGameUI *pGameUI = (IGameUI *)gameUIFactory( GAMEUI_INTERFACE_VERSION, NULL );
		if ( pGameUI )
		{
			// Insert custom loading panel for the loading dialog
			pPanelBg = new CMapLoadBG( "Background" );
			pPanelBg->InvalidateLayout( false, true );
			pPanelBg->SetVisible( false );
			pPanelBg->MakePopup( false );
			pGameUI->SetLoadingBackgroundDialog( pPanelBg->GetVPanel() );
		}
	}


To end this, add the following in LevelInit( const char *newmap ) at line 832 (after enginesound->SetPlayerDSP( filter, 0, true );):

// Fenix: Custom background loading screens - decides if use a loading screen for a map or a default one
#ifdef _WIN32
	char szMapBgName[MAX_PATH];
#else	// !_WIN32
	char szMapBgName[PATH_MAX];
#endif	// _WIN32
	
	Q_snprintf( szMapBgName, sizeof( szMapBgName ), "vgui/loading/maps/%s", newmap );

	pMatMapBg = materials->FindMaterial( szMapBgName, TEXTURE_GROUP_OTHER );

	if ( !pMatMapBg->IsErrorMaterial() )
	{
		Q_snprintf( szMapBgName, sizeof( szMapBgName ), "loading/maps/%s", newmap );
		pPanelBg->SetNewBackgroundImage( szMapBgName );
	}
	else
	{
		pPanelBg->SetNewBackgroundImage( "loading/default" );
	}

If you want to hide loading dialogue

The code for the GameUI is closed, but some hack can be made to hide the loading dialog.

In mapload_background.h add override for method OnMessage after void SetNewBackgroundImage(char const* imageName); line:

void OnMessage(const KeyValues* params, vgui::VPANEL fromPanel);

Next at the end of mapload_background.cpp add following code:

// URAKOLOUY5: Recursively find all elements with this name and unparent them
void SetVisibilityRecursive(vgui::VPANEL parentPanel, const char* targetPanelName, int depth = 0) {
    int numChildren = vgui::ipanel()->GetChildCount(parentPanel);
    for (int i = 0; i < numChildren; i++) {
        vgui::VPANEL childPanel = vgui::ipanel()->GetChild(parentPanel, i);
        const char* childPanelName = vgui::ipanel()->GetName(childPanel);

        if (V_strcmp(childPanelName, targetPanelName) == 0)
            vgui::ipanel()->SetParent(childPanel, NULL);

        SetVisibilityRecursive(childPanel, targetPanelName, depth + 1);
    }
}

// URAKOLOUY5: Override OnMessage to detect "Activate" message and call SetVisibilityRecursive
void CMapLoadBG::OnMessage(const KeyValues* params, vgui::VPANEL fromPanel) {
    if (V_strcmp(params->GetName(), "Activate") == 0) {
        vgui::VPANEL rootPanel = enginevgui->GetPanel(PANEL_GAMEUIDLL);
        SetVisibilityRecursive(rootPanel, "LoadingDialog");
    }

    // Call the base class OnMessage to handle messages we didn't process
    BaseClass::OnMessage(params, fromPanel);
}

This hack unparents LoadingDialogue on every loading (it finds LoadingDialog by name in PANEL_GAMEUIDLL.

Actually, you can use this code to "hide" other elements too.


Here ends all the work at the code. Time to create the RES file and the VMT and VTF files.

RES and VMT / VTF

loadingdialogbackground.res

Create this RES file at the resource folder present on the main mod folder:

"Resource/loadingdialogbackground.res"
{
	"LoadingImage"
	{
		"ControlName"		"ImagePanel"
		"fieldName"			"LoadingImage"
		"xpos"				"0"
		"ypos"				"0"
		"wide"				"f-1"
		"tall"				"f-1"
		"autoResize"		"0"
		"pinCorner"			"0"
		"visible"			"1"
		"enabled"			"1"
		"tabPosition"		"0"
		"border"			"1"
		"image"				"loading/default" //Leaving it empty will make appear a grey background the first time on map load
		"scaleImage"		"1"
	}
}

VMT & VTF

Head to materials/vgui/ and create a new folder called loading. In there, create a VMT called default with the following code:

"UnlitGeneric"
{
	"$basetexture"   "vgui/loading/default"
	"$vertexcolor"   "1"
	"$vertexalpha"   "1"
	"$ignorez"       "1"
	"$no_fullbright" "1"
	"$nolod"         "1"
}

You are free to create a VTF texture with the same name of any size, although I recommend being 2048x1024 big, with No Mipmap, No Level of Detail and No Minimum Mipmap flags checked.

This will be used as the default loading screen in case no defined loading screen for a map is found.

Create now a subfolder called maps/, in there you can place the VMTs and VTFs with the name of the maps desired (example: ep2_outland_03).

After this step, the feature is ready to be tested.

Notes

  • This code was made with having as a reference the emails at HLCoders present above and looking at how NMRIH loading screens appear. Aside from that, all was guesswork and trial & error. Likely there can be better methods out there, but couldn't find other ways. Those teams who did refuse to share their secrets.
  • As seen in NMRIH and OC, the checks about if a map has a present custom loading screen aren't made until late in the load. Other mods apparently were able to do it instantly, but IDK how they did that without engine access.
  • This is an obvious one, but this code was very old and can be improved, like having different default loading screens, or avoiding throwing an error in console (visible with developer 2) if it can't find one.