[SDK Programming Newbie] How to call a client-side (hl_cdll) method on the server-side (hldll)? Created 2 months ago2024-09-30 15:39:10 UTC by Spectred140 Spectred140

Created 2 months ago2024-09-30 15:39:10 UTC by Spectred140 Spectred140

Posted 2 months ago2024-09-30 15:39:10 UTC Post #349191
First of all, I'm not a C++ programmer. Despite this, I have a lot of experience with C#, so after two days of working with the Half-Life SDK, I learned the main differences. However, for more experienced developers, some of the things I’ve done may seem odd or even wrong. I don’t mind if someone points them out. :3

What I want to achieve is to add the ability to change the HUD color by firing a map entity, because sometimes the player will be playing as different characters. After reading this amazing tutorial, I created a static class called HUDManager with static fields defining the RGB values, as well as a static method to change these values. I modified the UnpackRGB() method to use the values from this class, and so far, it has worked with the default values (though I haven’t yet tried to use this method at runtime).

After a few more hours spent reading the SDK code and tutorials, I created an entity class, linked it, and... suddenly realized that entities are defined and declared on the server side, while my HUDManager is on the client side. So now I need a way to call a client-side method from the server.

I found this tutorial on temporary entity messages, which seems like what I need (or at least something very similar). However, it doesn’t fully solve my problem - none of the constants defined in const.h (from hl_cdll) seem to help.

I also found a file called UserMessages.cpp in hldll, which seems to use the REG_USER_MSG method. This is a macro for g_engfuncs.pfnRegUserMsg, which I presume, based on the file name and the comment above enginefuncs_s, is an interface to an engine function of the same name. Since the engine’s code is not open-source, I can’t dig deeper to verify if this is the function I need. That’s why I’m asking for help.

Some possibly useful information: I’m using the Half-Life Unified SDK, and my mod is intended to be entirely single-player.

I feel like this might be a very basic question that's already been answered somewhere, or there may be a tutorial about it that I just haven't found yet. Forgive my dumb C# brain xd

Oh, and if I’ve already posted this… I assume that after changing static variables of HUDManager at runtime (which I haven’t yet tested), I’ll need to manually redraw the HUD to apply the new colors. My second question is: how can I do this? I found the gHUD variable in cdll_int.h, which I assume is the main HUD variable on the client side? The CHud class (to which gHUD belongs) has a Redraw method with two arguments: flTime and intermission. What exactly does flTime refer to (specifically the m_flTime variable, which is assigned in the Redraw method)? The comment says it's "the current client time," but the current time in what context and in what format? m_flTime is used in so many places that I can’t quite grasp its meaning.
Posted 2 months ago2024-09-30 18:22:14 UTC Post #349193
Yeah, HL already does this in plenty of places, e.g. to send damage information to clients. What you're describing is a Remote Procedure Call / RPC, and Half-Life implements it exactly via "user messages".

It's a manual, tedious process, but fairly simple to follow:
Serverside: declare a global message ID (inline int gmsgMyMessage) and register it, i.e. link it to a name: gmsgMyMessage = REGISTER_USER_MSG( "MyMessage", -1 ), the last number saying how many bytes are sent in this message, -1 if unknown
Clientside: declare a HUD function and glue it to said message name with some macros, like DECLARE_MESSAGE and HOOK_MESSAGE

Here's a guide for that.
Admer456 Admer456If it ain't broken, don't fox it!
Posted 2 months ago2024-09-30 18:25:52 UTC Post #349194
As for flTime and others, that is the absolute time (in seconds) since the client started... something, I am not 100% sure if it's since joining the server or starting up the game altogether, but it's likely the latter.

And yes, gHUD is the HUD singleton. It represents the clientside as a whole, almost a bit of a god object I'm afraid..

Also, the HUD is redrawn every frame no matter what, it's an immediate-mode GUI as opposed to a retained one. So if you changed some colour variables, and they are being referenced in the HUD code, no worries, it'll auto-apply.
Admer456 Admer456If it ain't broken, don't fox it!
Posted 2 months ago2024-10-02 14:27:20 UTC Post #349202
Thanks! All worked. Thanks for the link too, I found a lot of interesting articles about working with HUD/VGUI there.

Also, for those who will find this thread when searching for the same question and encounter the same problem - Tried to create a message with a bogus message type ( 0 ) in my case popped up because of a message name longer than 12 characters (I somehow skipped it in tutorial 😅).
@Admer456 said:
And yes, gHUD is the HUD singleton. It represents the clientside as a whole, almost a bit of a god object I'm afraid..
Yeah, after a few days working with SDK, I'm starting to think that guys from Valve didn't know about any good coding practices or principles back in 1998. Otherwise, they could at least use folders to group files and make working with code a little less hell. Or, maybe, it wasn't possible in C++ back then... I don't know for sure, so I won't judge.
Posted 2 months ago2024-10-02 14:41:31 UTC Post #349203
Oh, and... I don't know, maybe I should create a new thread for a new question (sorry if I do :3), but I also want to ask how to get CBasePlayer from CBaseEntity.
I tried both static_cast and dynamic_cast, but they failed with different errors. I also tried to cast step-by-step through the inheritance chain (CBaseEntity -> CBaseDelay -> CBaseAnimating -> CBaseToggle -> CBaseMonster -> CBasePlayer, if I'm not mistaken), and it still gives an error when trying to cast CBasePlayer from CBaseMonster. I (again) have no idea what causes these errors, so I am asking here.
Posted 2 months ago2024-10-02 18:31:38 UTC Post #349204
Yeah, after a few days working with SDK, I'm starting to think that guys from Valve didn't know about any good coding practices or principles back in 1998.
They knew about good principles, they just didn't really have the time to apply em. Between 1996 and 1998, the game spec changed so rapidly and so much, nobody could really keep up. Game code is very patchy, very volatile, and rapidly evolves. It's simply like that by nature.
but I also want to ask how to get CBasePlayer from CBaseEntity
if ( pEntity->IsPlayer() )
{
   CBasePlayer* player = static_cast<CBasePlayer*>( pEntity );
   ...
}
It is possible you were getting errors because you were casting to CBasePlayer and not CBasePlayer*. This is just C++'s value and reference semantics. In C#, a value type is a struct. In C++, a value type is anything that isn't a pointer. So you were trying to, essentially, convert a reference type into a value type there. Possibly, I'm just guessing.

Rule of thumb: if you know an object is of a certain class (e.g. via IsPlayer or checking pev->classname), you use static_cast. static_cast will throw if the cast fails.
If you don't know what object a class is, and you wish to "ask" it, you use dynamic_cast (a bit like the is keyword in C# but simultaneously it's an as)

You may then write a few utility methods into CBaseEntity too, as an example:
template<class T>
T* As()
{
    return static_cast<T*>( this );
}

template<class T>
T* TryAs()
{
    if ( Is<T>() )
    {
       return As<T>();
    }

   return nullptr;
}

template<class T>
bool Is()
{
    return dynamic_cast<T*>( this ) != nullptr;
}
Usage:
if ( pEntity->Is<CBasePlayer>() )
{
   pEntity->As<CBasePlayer>()->DoPlayerSpecificStuff();
   // or
   CBasePlayer* player = pEntity->As<CBasePlayer>();
}
Admer456 Admer456If it ain't broken, don't fox it!
Posted 2 months ago2024-10-02 20:25:15 UTC Post #349206
@Admer456 said:
They knew about good principles, they just didn't really have the time to apply em. Between 1996 and 1998, the game spec changed so rapidly and so much, nobody could really keep up. Game code is very patchy, very volatile, and rapidly evolves. It's simply like that by nature.
Oh, it explains.
@Admer456 said:
It is possible you were getting errors because you were casting to CBasePlayer and not CBasePlayer*. This is just C++'s value and reference semantics. In C#, a value type is a struct. In C++, a value type is anything that isn't a pointer. So you were trying to, essentially, convert a reference type into a value type there. Possibly, I'm just guessing.
Sorry for asking without clarification.
I used a static cast like this:
if (pOther->IsPlayer())
    player = static_cast<CBasePlayer*>(pOther); // invalid type conversion
Have no idea why it gives an error. I read that this may also occur when inheritance is private, but it's public in SDK code.

And dynamic cast like this:
player = dynamic_cast<CBasePlayer*>(pOther); // the type in a dynamic_cast must be a pointer or reference to a complete class type, or void *
I believe I've already indicated that I need a pointer by using an asterisk. At least, when I used dynamic_cast multiple times trying to cast from CBaseEntity to CBasePlayer through the inheritance chain, it worked fine with all classes but CBasePlayer.

I also know the differences between these two methods, I tried both just in case, but thanks for the explanation anyway!

By the way, your code:
@Admer456 said:
if ( pEntity->IsPlayer() )
{
   CBasePlayer* player = static_cast<CBasePlayer*>( pEntity );
   ...
}
Is just like the one I used and caused the same error (invalid type conversion). The only difference is that I changed pEntity to pOther (since I used your snippet in the entity code), however, it does not look like the reason.
Posted 2 months ago2024-10-03 07:24:53 UTC Post #349209
Check the datatype of pOther. It's probably an entvars_t* which you first must convert into a CBaseEntity* via some utility.
Admer456 Admer456If it ain't broken, don't fox it!
Posted 2 months ago2024-10-03 08:16:07 UTC Post #349210
@Admer456 said:
It's probably an entvars_t*
Nope, it's exactly CBaseEntity*; at least, the method signature says so. All attempts were in the CBaseEntity::Use() method override. Maybe, some Unified SDK bug? I double-checked everything to be correct. Otherwise, I wouldn't be asking - I prefer to solve by myself mostly, without asking someone to do this instead of me, unless I don't have another option and Google can't help me.
Posted 2 months ago2024-10-03 16:04:34 UTC Post #349212
Post your code (preferably a good chunk of the .cpp) please. Something doesn't sound right.
Admer456 Admer456If it ain't broken, don't fox it!
Posted 2 months ago2024-10-03 17:35:10 UTC Post #349215
I found that this error doesn't appear in the new entity's file. However, it seems to appear when I'm trying to write such code in the same method override in my old entity's class. After finding this, I also tried to copy-paste includes (ones from items.cpp), and it solved the problem, so the reason is one of these includes. How...?
Okay, I'll post all the code anyway, I'm curious about the real reason, and I don't think my pathetic attempts in C++ are worth something xd
#include "extdll.h"
#include "util.h"
#include "cbase.h"
#include "UserMessages.h"
#include "cstring"

class CTargetChangeHudColor : public CBaseEntity
{
public:
    // make these variables private, probably?
    // nothing should change them directly
    int m_iRed;
    int m_iGreen;
    int m_iBlue;
    void Use(CBaseEntity* pActivator, CBaseEntity* pOther, USE_TYPE useType, float value) override;
    bool KeyValue(KeyValueData* pkvd) override;
};

LINK_ENTITY_TO_CLASS(target_change_hud_color, CTargetChangeHudColor);

bool CTargetChangeHudColor::KeyValue(KeyValueData* pkvd)
{
    if (FStrEq(pkvd->szKeyName, "red"))
    {
        m_iRed = atof(pkvd->szValue);
        return true;
    }
    else if (FStrEq(pkvd->szKeyName, "green"))
    {
        m_iGreen = atof(pkvd->szValue);
        return true;
    }
    else if (FStrEq(pkvd->szValue, "blue"))
    {
        m_iBlue = atof(pkvd->szValue);
        return true;
    }

    return CBaseEntity::KeyValue(pkvd);
}

void CTargetChangeHudColor::Use(CBaseEntity* pActivator, CBaseEntity* pOther, USE_TYPE useType, float value)
{
    CBasePlayer* player;

     // errors here
    if (pActivator->IsPlayer())
        player = static_cast<CBasePlayer*>(pActivator);
    else
        player = static_cast<CBasePlayer*>(pOther);

    // MESSAGE_BEGIN(MSG_ONE, gmsgHudColor, NULL, player->pev);
    MESSAGE_BEGIN(MSG_ALL, gmsgHudColor);
    WRITE_SHORT(m_iRed);
    WRITE_SHORT(m_iGreen);
    WRITE_SHORT(m_iBlue);
    MESSAGE_END();
}
ScreenshotScreenshot
I don't know which entity is calling player (who pressed the button), so I checked both of them. This article says something about pCaller, however, I don't see such variable in the method signature.
Posted 2 months ago2024-10-03 19:42:34 UTC Post #349216
Okay so generally speaking, avoid writing implementation code like this in headers.
class Class
{
public:
    void Method()
    {
       // Avoid doing this if possible
    }
};
// Class.h
class Class
{
public:
   void Method();
}

// Class.cpp
void Class::Method()
{
   // Do prefer doing this in .cpp files - sometimes it may not be possible
}
This is because of how declarations work in C and C++. When a header is #include -ed into a .cpp file, it is literally copy-pasted there, together with other headers. If a datatype is declared above some code, then that code is quite simply "aware" that it exists.

In other words, when writing code in header files, be aware of other headers that may be included, in the .cpp files that include that header.

In this case, chances are CBasePlayer wasn't declared in one of these situations. So you gotta move that code into a .cpp and include player.h to get the CBasePlayer declaration. Or, if you absolutely have to do it in a header, you can use forward-declaration:
class CBasePlayer; // this class is more fully declared elsewhere
Admer456 Admer456If it ain't broken, don't fox it!
Posted 2 months ago2024-10-04 16:02:30 UTC Post #349219
Looks like I have a lot to learn in C++ besides the main syntax 😅

Anyway, I finally have nothing to ask. Thanks for your patience when answering my dumb questions :3
Posted 2 months ago2024-10-04 20:19:37 UTC Post #349220
Yeah, C++ is quite the beast. I'd be willing to answer any other questions you'd have, whether here or on TWHL Discord, so feel free to ask.
Admer456 Admer456If it ain't broken, don't fox it!
You must be logged in to post a response.