Point message fix for multiplayer

From Valve Developer Community
Jump to navigation Jump to search
Broom icon.png
This article or section should be converted to third person to conform to wiki standards.

This guide will show you how to change the point_message entity in a Source 2013 Multiplayer Source 2013 Multiplayer mod, to work for multiple players.

Prerequisites

  • A decent understanding of C++.
  • A simple understanding of how network entities work in the Source engine.

With that out of the way, lets get started!

Modifying the base.fgd file

In order for some code to work later, we need to add an origin to the point_message entity. If we do not do this, the text will always be on the ground. If you are not using your own custom base.fgd file, you can find the default one in steamapps/common/Source SDK Base 2013 Multiplayer/bin.

  1. Open the base.fgd file with your text editor of choice, such as Notepad++ Notepad++
  2. Use the search utility of your text editor to find point_message.

It should look like this:

@PointClass base(Targetname, Parentname) size(-8 -8 -8, 8 8 8) = point_message : 
	"An entity that displays a text message in the world, at its origin."
[
	spawnflags(flags) =
	[
		1: "Start Disabled" : 0
	]

	message(string) : "Entity Message"
	radius(integer) : "Show message radius" : 128 : "Distance the player must be within to see this message."
	developeronly(choices) : "Developer Only?" : 0 : "If set, this message will only be visible when developer mode is on." =
	[
		0 : "No"
		1 : "Yes"
	]

	// Inputs
	input Enable(void) : "Start displaying the message text, if the player is within the message radius."
	input Disable(void) : "Stop displaying the message text."
]

Under the line

radius(integer) : "Show message radius" : 128 : "Distance the player must be within to see this message."

add the following.

messageorigin(origin) : "Origin (X,Y,Z)"

That is all you need to do the the base.fgd.

Optionally you can change the

message(string) : "Entity Message"

line to

message(string) : "Entity Message" : "" : "Max of 128 characters!"

This will come up in hammer and will warn users of the maximum character limit we will set later. You can change the limit however if you want more or less characters.

This is what the point_message entity in the base.fgd file should look like completed.

@PointClass base(Targetname, Parentname) size(-8 -8 -8, 8 8 8) = point_message : 
	"An entity that displays a text message in the world, at its origin."
[
	spawnflags(flags) =
	[
		1: "Start Disabled" : 0
	]

	message(string) : "Entity Message" : "" : "Max of 128 characters!"
	radius(integer) : "Show message radius" : 128 : "Distance the player must be within to see this message."
	messageorigin(origin) : "Origin (X,Y,Z)"
	developeronly(choices) : "Developer Only?" : 0 : "If set, this message will only be visible when developer mode is on." =
	[
		0 : "No"
		1 : "Yes"
	]

	// Inputs
	input Enable(void) : "Start displaying the message text, if the player is within the message radius."
	input Disable(void) : "Stop displaying the message text."
]

Modifying the server side message_entity code

We need to make some changes to the default message_entity code.

  1. Open up your games.sln solution, and search in the solution explorer for message_entity.cpp
  2. Open the file up and look for the line
     DECLARE_CLASS(CMessageEntity, CPointEntity);
    
  3. Under it add the following
     DECLARE_SERVERCLASS();
    

This will set the class up for networking.

Towards the top of the file under

#define SF_MESSAGE_DISABLED		1

add the following forward declaration

void DrawMessageEntities();

This will prevent a compilation error.

  1. Now find the line
     DECLARE_DATADESC();
    
  2. Under it add the following lines
    CNetworkString(m_szMessageText,128);
    CNetworkVar(bool, m_drawText);
    CNetworkVar(Vector, m_vecTextOrigin);
    CNetworkVar(int, m_radius);
    

These are the values that will be sent to and received by the client.

You will now need to comment out some old declarations for some of the variables like so

  
protected:
	//int			m_radius;
	string_t		m_messageText;
	//bool			m_drawText;
	bool			m_bDeveloperOnly;
	bool			m_bEnabled;

Now we need to set up the server side data-table.

Under the line

LINK_ENTITY_TO_CLASS(point_message, CMessageEntity);

Add the following

IMPLEMENT_SERVERCLASS_ST(CMessageEntity, DT_MessageEntity)

	SendPropString(SENDINFO(m_szMessageText)),
	SendPropBool(SENDINFO(m_drawText)),
	SendPropVector(SENDINFO(m_vecTextOrigin)),
	SendPropInt(SENDINFO(m_radius)),

END_SEND_TABLE()

Next we need to add a keyfield for the origin point we added.

Under the line

 DEFINE_KEYFIELD(m_bDeveloperOnly, FIELD_BOOLEAN, "developeronly"),

Add the following line

 DEFINE_KEYFIELD(m_vecTextOrigin, FIELD_VECTOR, "messageorigin"),

Moving along, we now need to modify the Think method of the entity.

Find

 void CMessageEntity::Think(void)

and replace the whole method so it looks like this

void CMessageEntity::Think(void)
{
	BaseClass::Think();
	SetNextThink(gpGlobals->curtime + 0.1f);

	Q_snprintf(m_szMessageText.GetForModify(), sizeof(m_szMessageText), "%s", STRING(m_messageText));
	DrawMessageEntities();
}

This will copy the text from the entity in the level into a char array which we can then transmit to the client.

Now find

 void CMessageEntity::DrawOverlays(void)

and replace the whole method to look like this

void CMessageEntity::DrawOverlays(void)
{
	if (m_bDeveloperOnly && !g_pDeveloper->GetInt())
	{
		m_drawText = false;
		return;
	}

	if (!m_bEnabled)
	{
		m_drawText = false;
		return;
	}

	m_drawText = true;
}

All we need to tell the client is whether or not the entity is enabled or not.
The client will handle when to draw the text when the player is close enough.

Lastly inside

void CMessageEntity::Spawn(void)

and under

m_bEnabled = !HasSpawnFlags(SF_MESSAGE_DISABLED);

Add the following line

SetTransmitState(FL_EDICT_ALWAYS);

This will make it so the entity is created on the client side.

That is all we need to do on the server side. Now it is time to create the client side entity.

Creating the client side entity

Now we need to create the client side entity.

  1. In the solution explorer, open the Client drop down.
  2. The open the Source Files drop down.
  3. Right click on the Source Files filter and click add -> New item
  4. Select C++ File and call it c_message_entity. c_ is to say that it is a client side entity.
  5. Click add and the file should automatically open.
  6. Copy and paste the following code into the file. Read the comments if you want to know what is going on.
    #include "cbase.h" 
    #include "c_baseentity.h" // Needed so we can derive from it.
    #include "engine/ivdebugoverlay.h" // Needed so there is not an undefined type error
    
    class C_MessageEntity : public C_BaseEntity
    {
    public:
    	DECLARE_CLASS(C_MessageEntity, C_BaseEntity);
    	DECLARE_CLIENTCLASS(); // Declare that we are a client class, and that there should be a corrispoding server class
    
    	void ClientThink() override; // Override ClientThink so we can modify it
    	void OnDataChanged(DataUpdateType_t type) override; // Same with on data changed
    
    public:
    	char m_szMessageText[128]; // Variables which will be allocated values which are being recieved from server.
    	bool m_drawText; // You can modify the size of m_szMessageText on both ends to increase/decrease character count
    	Vector m_vecTextOrigin;
    	int m_radius;
    };
    
    LINK_ENTITY_TO_CLASS(point_message, C_MessageEntity);
    
    IMPLEMENT_CLIENTCLASS_DT(C_MessageEntity, DT_MessageEntity, CMessageEntity) // Implement client class data table. It is important that the values are sent and recieved in the same order.
    
    	RecvPropString(RECVINFO(m_szMessageText)),
    	RecvPropBool(RECVINFO(m_drawText)),
    	RecvPropVector(RECVINFO(m_vecTextOrigin)),
    	RecvPropInt(RECVINFO(m_radius)),
    
    END_RECV_TABLE()
    
    extern IVDebugOverlay* debugoverlay; // Get and external instance of debugoverlay. Let the linker figure out where it is.
    
    void C_MessageEntity::OnDataChanged(DataUpdateType_t type) // This is important. It is called when data is changed, or in our case as soon as we load onto the server.
    {
    	BaseClass::OnDataChanged(type);
    	SetNextClientThink(CLIENT_THINK_ALWAYS); // We need this so we can start this entity thinking. This will make it think until the level is shutdown.
    }
    
    void C_MessageEntity::ClientThink()
    {
    	BaseClass::ClientThink();
    
    	if (m_drawText) // Only draw text if enabled
    	{
    		if (UTIL_PlayerByIndex(GetLocalPlayerIndex())) // Get the local player
    		{
    			C_BasePlayer* LocalPlayer = UTIL_PlayerByIndex(GetLocalPlayerIndex()); // Assign the local player to a variable so it is easier to work with.
    			Vector worldTargetPosition = LocalPlayer->EyePosition(); // Get the players position
    
    			if((worldTargetPosition - GetAbsOrigin()).Length() <= m_radius) // Check whether the player within range or not
    			{
    				debugoverlay->AddTextOverlayRGB(m_vecTextOrigin,0,0,255,255,255,255,m_szMessageText); // Draw text on screen
    			}
    		}
    	}
    }
    

You can create a custom filter if you want, if you want to better organise your source files.

That is everything set up. You will need to create a map with hammer with a point_message entity in it.
Build both the Client and Server projects and then launch the game. Loading a map with a point_message entity in it, you should see that it turns on and off at certain distances.
This has been tested to work with multiple players on a Source SDK 2013 multiplayer mod.

This is the completed server side code in message_entity.cpp

//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $NoKeywords: $
//=============================================================================//

#include "cbase.h"
#include "basecombatweapon.h"
#include "explode.h"
#include "eventqueue.h"
#include "gamerules.h"
#include "ammodef.h"
#include "in_buttons.h"
#include "soundent.h"
#include "ndebugoverlay.h"
#include "vstdlib/random.h"
#include "engine/IEngineSound.h"
#include "game.h"

#include "player.h"
#include "entitylist.h"

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

// Spawnflags
#define SF_MESSAGE_DISABLED		1

void DrawMessageEntities();

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
class CMessageEntity : public CPointEntity
{
	DECLARE_CLASS(CMessageEntity, CPointEntity);
	DECLARE_SERVERCLASS();

public:
	void	Spawn(void);
	void	Activate(void);
	void	Think(void);
	void	DrawOverlays(void);

	virtual void UpdateOnRemove();

	void	InputEnable(inputdata_t& inputdata);
	void	InputDisable(inputdata_t& inputdata);

	DECLARE_DATADESC();

	CNetworkString(m_szMessageText, 128);
	CNetworkVar(bool, m_drawText);
	CNetworkVar(Vector, m_vecTextOrigin);
	CNetworkVar(int, m_radius);

protected:
	//int			m_radius;
	string_t		m_messageText;
	//bool			m_drawText;
	bool			m_bDeveloperOnly;
	bool			m_bEnabled;
};

LINK_ENTITY_TO_CLASS(point_message, CMessageEntity);

IMPLEMENT_SERVERCLASS_ST(CMessageEntity, DT_MessageEntity)

	SendPropString(SENDINFO(m_szMessageText)),
	SendPropBool(SENDINFO(m_drawText)),
	SendPropVector(SENDINFO(m_vecTextOrigin)),
	SendPropInt(SENDINFO(m_radius)),

END_SEND_TABLE()

BEGIN_DATADESC(CMessageEntity)

	DEFINE_KEYFIELD(m_radius, FIELD_INTEGER, "radius"),
	DEFINE_KEYFIELD(m_messageText, FIELD_STRING, "message"),
	DEFINE_KEYFIELD(m_bDeveloperOnly, FIELD_BOOLEAN, "developeronly"),
	DEFINE_KEYFIELD(m_vecTextOrigin, FIELD_VECTOR, "messageorigin"),
	DEFINE_FIELD(m_drawText, FIELD_BOOLEAN),
	DEFINE_FIELD(m_bEnabled, FIELD_BOOLEAN),

	// Inputs
	DEFINE_INPUTFUNC(FIELD_VOID, "Enable", InputEnable),
	DEFINE_INPUTFUNC(FIELD_VOID, "Disable", InputDisable),

END_DATADESC()

static CUtlVector< CHandle< CMessageEntity > >	g_MessageEntities;

//-----------------------------------------
// Spawn
//-----------------------------------------
void CMessageEntity::Spawn(void)
{
	SetNextThink(gpGlobals->curtime + 0.1f);
	m_drawText = false;
	m_bDeveloperOnly = false;
	m_bEnabled = !HasSpawnFlags(SF_MESSAGE_DISABLED);
	SetTransmitState(FL_EDICT_ALWAYS);
	//m_debugOverlays |= OVERLAY_TEXT_BIT;		// make sure we always show the text
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CMessageEntity::Activate(void)
{
	BaseClass::Activate();

	CHandle< CMessageEntity > h;
	h = this;
	g_MessageEntities.AddToTail(h);
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CMessageEntity::UpdateOnRemove()
{
	BaseClass::UpdateOnRemove();

	CHandle< CMessageEntity > h;
	h = this;
	g_MessageEntities.FindAndRemove(h);

	BaseClass::UpdateOnRemove();
}

//-----------------------------------------
// Think
//-----------------------------------------
void CMessageEntity::Think(void)
{
	BaseClass::Think();
	SetNextThink(gpGlobals->curtime + 0.1f);

	Q_snprintf(m_szMessageText.GetForModify(), sizeof(m_szMessageText), "%s", STRING(m_messageText));
	DrawMessageEntities();
}

//-------------------------------------------
//-------------------------------------------
void CMessageEntity::DrawOverlays(void)
{
	if (m_bDeveloperOnly && !g_pDeveloper->GetInt())
	{
		m_drawText = false;
		return;
	}

	if (!m_bEnabled)
	{
		m_drawText = false;
		return;
	}

	m_drawText = true;
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CMessageEntity::InputEnable(inputdata_t& inputdata)
{
	m_bEnabled = true;
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CMessageEntity::InputDisable(inputdata_t& inputdata)
{
	m_bEnabled = false;
}

// This is a hack to make point_message stuff appear in developer 0 release builds
//  for now
void DrawMessageEntities()
{
	int c = g_MessageEntities.Count();
	for (int i = c - 1; i >= 0; i--)
	{
		CMessageEntity* me = g_MessageEntities[i];
		if (!me)
		{
			g_MessageEntities.Remove(i);
			continue;
		}

		me->DrawOverlays();
	}
}

Please let me know of any issues on the discussion page of this topic.