Author SHA1 Message Date
A1m` 07fc524d27 Fixed incorrect use of ReferenceToIndex, which caused some functions to be unstable during client connection. (#2)
While this function works well with the client indexes we pass in, as its name
suggests, we should essentially be passing player references, not client indexes.
The problem is that SourceMod looks for this player in the entity list, but they
might not be there when connecting, or they might be another client or a bot.

This makes the code unstable and confusing, and it works intermittently.
Steam doesn't care whether the player is in the entity list—it works with the
Steam ID. Using ReferenceToIndex here is fundamentally wrong and causes random
-1 errors, especially during OnClientConnect when the entity may not exist yet.

This commit replaces it with a direct client index lookup, which is correct and
reliable because params[1] is already a client index.
2026-07-13 21:39:04 +00:00
Nicholas Hastings 8fd7a1988c Update extension author and repository URL 2026-07-12 22:40:00 -04:00
Nicholas Hastings 65936e7d9c Fix size_t truncation warning in SetHTTPRequestRawPostBodyFromFile 2026-07-12 22:10:00 -04:00
Nicholas Hastings 645103735e Fix streaming HTTP response header and data callbacks 2026-07-12 21:20:00 -04:00
9 changed files with 153 additions and 55 deletions
+1
View File
@@ -72,6 +72,7 @@ void SteamWorks::SDK_OnUnload()
delete this->pSWHTTPNatives; delete this->pSWHTTPNatives;
delete this->pSWHTTP; delete this->pSWHTTP;
this->pSWHTTP = NULL; /* Requests freed via frame actions may outlive us; let their dtor detect this. */
delete this->pSWGameServer; delete this->pSWGameServer;
delete this->pSWGameData; delete this->pSWGameData;
} }
+28 -14
View File
@@ -219,11 +219,16 @@ static cell_t sm_UserHasLicenseForApp(IPluginContext *pContext, const cell_t *pa
return k_EUserHasLicenseResultNoAuth; return k_EUserHasLicenseResultNoAuth;
} }
int client = gamehelpers->ReferenceToIndex(params[1]); int client = params[1];
IGamePlayer *pPlayer = playerhelpers->GetGamePlayer(client); /* Man, including GameHelpers and PlayerHelpers for this native :(. */ if (client < 1 || client > playerhelpers->GetMaxClients())
if (pPlayer == NULL || pPlayer->IsConnected() == false)
{ {
return pContext->ThrowNativeError("Client index %d is invalid", params[1]); return pContext->ThrowNativeError("Client index %d is invalid", client);
}
IGamePlayer *pPlayer = playerhelpers->GetGamePlayer(client);
if (pPlayer == NULL || !pPlayer->IsConnected())
{
return pContext->ThrowNativeError("Client index %d is not connected", client);
} }
CSteamID checkid = CreateCommonCSteamID(pPlayer, params, 3, 4); CSteamID checkid = CreateCommonCSteamID(pPlayer, params, 3, 4);
@@ -245,12 +250,16 @@ static cell_t sm_UserHasLicenseForAppId(IPluginContext *pContext, const cell_t *
static cell_t sm_GetClientSteamID(IPluginContext *pContext, const cell_t *params) static cell_t sm_GetClientSteamID(IPluginContext *pContext, const cell_t *params)
{ {
int client = gamehelpers->ReferenceToIndex(params[1]); int client = params[1];
IGamePlayer *pPlayer = playerhelpers->GetGamePlayer(client); if (client < 1 || client > playerhelpers->GetMaxClients())
if (pPlayer == NULL || pPlayer->IsConnected() == false)
{ {
return pContext->ThrowNativeError("Client index %d is invalid", params[1]); return pContext->ThrowNativeError("Client index %d is invalid", client);
}
IGamePlayer *pPlayer = playerhelpers->GetGamePlayer(client);
if (pPlayer == NULL || !pPlayer->IsConnected())
{
return pContext->ThrowNativeError("Client index %d is not connected", client);
} }
CSteamID steamId = CreateCommonCSteamID(pPlayer, params, 4, 5); CSteamID steamId = CreateCommonCSteamID(pPlayer, params, 4, 5);
@@ -270,14 +279,19 @@ static cell_t sm_GetUserGroupStatus(IPluginContext *pContext, const cell_t *para
if (pServer == NULL) if (pServer == NULL)
{ {
return false; return 0;
} }
int client = gamehelpers->ReferenceToIndex(params[1]); int client = params[1];
IGamePlayer *pPlayer = playerhelpers->GetGamePlayer(client); /* Man, including GameHelpers and PlayerHelpers for this native :(. */ if (client < 1 || client > playerhelpers->GetMaxClients())
if (pPlayer == NULL || pPlayer->IsConnected() == false)
{ {
return pContext->ThrowNativeError("Client index %d is invalid", params[1]); return pContext->ThrowNativeError("Client index %d is invalid", client);
}
IGamePlayer *pPlayer = playerhelpers->GetGamePlayer(client);
if (pPlayer == NULL || !pPlayer->IsConnected())
{
return pContext->ThrowNativeError("Client index %d is not connected", client);
} }
CSteamID checkid = CreateCommonCSteamID(pPlayer, params, 3, 4); CSteamID checkid = CreateCommonCSteamID(pPlayer, params, 3, 4);
+2 -2
View File
@@ -41,8 +41,8 @@
#define SMEXT_CONF_NAME "SteamWorks Extension" #define SMEXT_CONF_NAME "SteamWorks Extension"
#define SMEXT_CONF_DESCRIPTION "Exposes SteamWorks functions to Developers" #define SMEXT_CONF_DESCRIPTION "Exposes SteamWorks functions to Developers"
#define SMEXT_CONF_VERSION "1.2.3" #define SMEXT_CONF_VERSION "1.2.3"
#define SMEXT_CONF_AUTHOR "Kyle Sanderson" #define SMEXT_CONF_AUTHOR "Kyle Sanderson, AlliedModders"
#define SMEXT_CONF_URL "http://AlliedMods.net" #define SMEXT_CONF_URL "https://github.com/alliedmodders/SM-SteamWorks"
#define SMEXT_CONF_LOGTAG "STEAMWORKS" #define SMEXT_CONF_LOGTAG "STEAMWORKS"
#define SMEXT_CONF_LICENSE "GPLv3" #define SMEXT_CONF_LICENSE "GPLv3"
#define SMEXT_CONF_DATESTRING __DATE__ #define SMEXT_CONF_DATESTRING __DATE__
+27 -12
View File
@@ -60,11 +60,16 @@ static cell_t sm_RequestUserStats(IPluginContext *pContext, const cell_t *params
return 0; return 0;
} }
int client = gamehelpers->ReferenceToIndex(params[1]); int client = params[1];
IGamePlayer *pPlayer = playerhelpers->GetGamePlayer(client); /* Man, including GameHelpers and PlayerHelpers for this native :(. */ if (client < 1 || client > playerhelpers->GetMaxClients())
if (pPlayer == NULL || pPlayer->IsConnected() == false)
{ {
return pContext->ThrowNativeError("Client index %d is invalid", params[1]); return pContext->ThrowNativeError("Client index %d is invalid", client);
}
IGamePlayer *pPlayer = playerhelpers->GetGamePlayer(client);
if (pPlayer == NULL || !pPlayer->IsConnected())
{
return pContext->ThrowNativeError("Client index %d is not connected", client);
} }
CSteamID checkid = CreateCommonCSteamID(pPlayer, params); CSteamID checkid = CreateCommonCSteamID(pPlayer, params);
@@ -80,11 +85,16 @@ static cell_t sm_GetStatCell(IPluginContext *pContext, const cell_t *params)
return 0; return 0;
} }
int client = gamehelpers->ReferenceToIndex(params[1]); int client = params[1];
IGamePlayer *pPlayer = playerhelpers->GetGamePlayer(client); /* Man, including GameHelpers and PlayerHelpers for this native :(. */ if (client < 1 || client > playerhelpers->GetMaxClients())
if (pPlayer == NULL || pPlayer->IsConnected() == false)
{ {
return pContext->ThrowNativeError("Client index %d is invalid", params[1]); return pContext->ThrowNativeError("Client index %d is invalid", client);
}
IGamePlayer *pPlayer = playerhelpers->GetGamePlayer(client);
if (pPlayer == NULL || !pPlayer->IsConnected())
{
return pContext->ThrowNativeError("Client index %d is not connected", client);
} }
char *pName; char *pName;
@@ -123,11 +133,16 @@ static cell_t sm_GetStatFloat(IPluginContext *pContext, const cell_t *params)
return 0; return 0;
} }
int client = gamehelpers->ReferenceToIndex(params[1]); int client = params[1];
IGamePlayer *pPlayer = playerhelpers->GetGamePlayer(client); /* Man, including GameHelpers and PlayerHelpers for this native :(. */ if (client < 1 || client > playerhelpers->GetMaxClients())
if (pPlayer == NULL || pPlayer->IsConnected() == false)
{ {
return pContext->ThrowNativeError("Client index %d is invalid", params[1]); return pContext->ThrowNativeError("Client index %d is invalid", client);
}
IGamePlayer *pPlayer = playerhelpers->GetGamePlayer(client);
if (pPlayer == NULL || !pPlayer->IsConnected())
{
return pContext->ThrowNativeError("Client index %d is not connected", client);
} }
char *pName; char *pName;
+43 -1
View File
@@ -23,7 +23,9 @@ static ISteamHTTP *GetHTTPPointer()
return g_SteamWorks.pSWGameServer->GetHTTP(); return g_SteamWorks.pSWGameServer->GetHTTP();
} }
SteamWorksHTTP::SteamWorksHTTP() SteamWorksHTTP::SteamWorksHTTP() :
m_CallbackHeadersReceived(this, &SteamWorksHTTP::OnHTTPHeadersReceived),
m_CallbackDataReceived(this, &SteamWorksHTTP::OnHTTPDataReceived)
{ {
this->typeHTTP = handlesys->CreateType("HTTPHandle", this, 0, NULL, NULL, myself->GetIdentity(), NULL); this->typeHTTP = handlesys->CreateType("HTTPHandle", this, 0, NULL, NULL, myself->GetIdentity(), NULL);
} }
@@ -38,6 +40,46 @@ HandleType_t SteamWorksHTTP::GetHTTPHandle(void)
return this->typeHTTP; return this->typeHTTP;
} }
void SteamWorksHTTP::RegisterRequest(SteamWorksHTTPRequest *pRequest)
{
if (pRequest->request != INVALID_HTTPREQUEST_HANDLE)
{
this->m_Requests[pRequest->request] = pRequest;
}
}
void SteamWorksHTTP::UnregisterRequest(SteamWorksHTTPRequest *pRequest)
{
if (pRequest->request != INVALID_HTTPREQUEST_HANDLE)
{
this->m_Requests.erase(pRequest->request);
}
}
SteamWorksHTTPRequest *SteamWorksHTTP::FindRequest(HTTPRequestHandle request)
{
auto it = this->m_Requests.find(request);
return (it == this->m_Requests.end()) ? NULL : it->second;
}
void SteamWorksHTTP::OnHTTPHeadersReceived(HTTPRequestHeadersReceived_t *pParam)
{
SteamWorksHTTPRequest *pRequest = this->FindRequest(pParam->m_hRequest);
if (pRequest != NULL)
{
pRequest->OnHTTPHeadersReceived(pParam);
}
}
void SteamWorksHTTP::OnHTTPDataReceived(HTTPRequestDataReceived_t *pParam)
{
SteamWorksHTTPRequest *pRequest = this->FindRequest(pParam->m_hRequest);
if (pRequest != NULL)
{
pRequest->OnHTTPDataReceived(pParam);
}
}
static void DelayedDeleteSteamWorksHTTPRequest(void *object) static void DelayedDeleteSteamWorksHTTPRequest(void *object)
{ {
SteamWorksHTTPRequest *pRequest = reinterpret_cast<SteamWorksHTTPRequest *>(object); SteamWorksHTTPRequest *pRequest = reinterpret_cast<SteamWorksHTTPRequest *>(object);
+19
View File
@@ -21,6 +21,10 @@
#include "steam_gameserver.h" #include "steam_gameserver.h"
#include "smsdk_ext.h" #include "smsdk_ext.h"
#include <unordered_map>
class SteamWorksHTTPRequest;
class SteamWorksHTTP : class SteamWorksHTTP :
public IHandleTypeDispatch public IHandleTypeDispatch
{ {
@@ -35,8 +39,23 @@ class SteamWorksHTTP :
public: public:
HandleType_t GetHTTPHandle(void); HandleType_t GetHTTPHandle(void);
/* Streaming responses report progress through HTTPRequestHeadersReceived_t /
HTTPRequestDataReceived_t. Steam delivers those as broadcast gameserver
callbacks (not as call results of the streaming API call), so a single
dispatcher here receives them and routes each one to the owning request by
its handle. Requests add/remove themselves as they are created/destroyed. */
void RegisterRequest(SteamWorksHTTPRequest *pRequest);
void UnregisterRequest(SteamWorksHTTPRequest *pRequest);
private:
SteamWorksHTTPRequest *FindRequest(HTTPRequestHandle request);
STEAM_GAMESERVER_CALLBACK(SteamWorksHTTP, OnHTTPHeadersReceived, HTTPRequestHeadersReceived_t, m_CallbackHeadersReceived);
STEAM_GAMESERVER_CALLBACK(SteamWorksHTTP, OnHTTPDataReceived, HTTPRequestDataReceived_t, m_CallbackDataReceived);
private: private:
HandleType_t typeHTTP; HandleType_t typeHTTP;
std::unordered_map<HTTPRequestHandle, SteamWorksHTTPRequest *> m_Requests;
}; };
#include "swhttprequest.h" #include "swhttprequest.h"
+21 -17
View File
@@ -67,6 +67,14 @@ SteamWorksHTTPRequest::SteamWorksHTTPRequest() : request(INVALID_HTTPREQUEST_HAN
SteamWorksHTTPRequest::~SteamWorksHTTPRequest() SteamWorksHTTPRequest::~SteamWorksHTTPRequest()
{ {
/* Requests are freed via a frame action, so on extension unload this can run
after the dispatcher itself has been torn down; pSWHTTP is nulled in that
case (see SDK_OnUnload), so guard against it. */
if (g_SteamWorks.pSWHTTP != NULL)
{
g_SteamWorks.pSWHTTP->UnregisterRequest(this);
}
ISteamHTTP *pHTTP = GetHTTPPointer(); ISteamHTTP *pHTTP = GetHTTPPointer();
if (pHTTP != NULL) if (pHTTP != NULL)
{ {
@@ -101,7 +109,10 @@ void SteamWorksHTTPRequest::OnHTTPRequestCompleted(HTTPRequestCompleted_t *pRequ
this->pCompletedForward->Execute(NULL); this->pCompletedForward->Execute(NULL);
} }
void SteamWorksHTTPRequest::OnHTTPHeadersReceived(HTTPRequestHeadersReceived_t *pRequest, bool bFailed) /* Streaming header/data notifications are success-only callbacks (unlike the
completion call result, they carry no IO-failure flag), so bFailure is always
false here. Failures still surface through the completion callback. */
void SteamWorksHTTPRequest::OnHTTPHeadersReceived(HTTPRequestHeadersReceived_t *pRequest)
{ {
if (this->pHeadersReceivedForward == NULL || this->pHeadersReceivedForward->GetFunctionCount() == 0) if (this->pHeadersReceivedForward == NULL || this->pHeadersReceivedForward->GetFunctionCount() == 0)
{ {
@@ -109,13 +120,13 @@ void SteamWorksHTTPRequest::OnHTTPHeadersReceived(HTTPRequestHeadersReceived_t *
} }
this->pHeadersReceivedForward->PushCell(this->handle); this->pHeadersReceivedForward->PushCell(this->handle);
this->pHeadersReceivedForward->PushCell(bFailed); this->pHeadersReceivedForward->PushCell(false);
this->pHeadersReceivedForward->PushCell(pRequest->m_ulContextValue >> 32); this->pHeadersReceivedForward->PushCell(pRequest->m_ulContextValue >> 32);
this->pHeadersReceivedForward->PushCell((pRequest->m_ulContextValue & 0x00000000FFFFFFFF)); this->pHeadersReceivedForward->PushCell((pRequest->m_ulContextValue & 0x00000000FFFFFFFF));
this->pHeadersReceivedForward->Execute(NULL); this->pHeadersReceivedForward->Execute(NULL);
} }
void SteamWorksHTTPRequest::OnHTTPDataReceived(HTTPRequestDataReceived_t *pRequest, bool bFailed) void SteamWorksHTTPRequest::OnHTTPDataReceived(HTTPRequestDataReceived_t *pRequest)
{ {
if (this->pDataReceivedForward == NULL || this->pDataReceivedForward->GetFunctionCount() == 0) if (this->pDataReceivedForward == NULL || this->pDataReceivedForward->GetFunctionCount() == 0)
{ {
@@ -123,7 +134,7 @@ void SteamWorksHTTPRequest::OnHTTPDataReceived(HTTPRequestDataReceived_t *pReque
} }
this->pDataReceivedForward->PushCell(this->handle); this->pDataReceivedForward->PushCell(this->handle);
this->pDataReceivedForward->PushCell(bFailed); this->pDataReceivedForward->PushCell(false);
this->pDataReceivedForward->PushCell(pRequest->m_cOffset); this->pDataReceivedForward->PushCell(pRequest->m_cOffset);
this->pDataReceivedForward->PushCell(pRequest->m_cBytesReceived); this->pDataReceivedForward->PushCell(pRequest->m_cBytesReceived);
this->pDataReceivedForward->PushCell(pRequest->m_ulContextValue >> 32); this->pDataReceivedForward->PushCell(pRequest->m_ulContextValue >> 32);
@@ -160,6 +171,8 @@ static cell_t sm_CreateHTTPRequest(IPluginContext *pContext, const cell_t *param
pRequest->request = request; pRequest->request = request;
pRequest->handle = handle; pRequest->handle = handle;
g_SteamWorks.pSWHTTP->RegisterRequest(pRequest);
return handle; return handle;
} }
@@ -296,23 +309,14 @@ static cell_t sm_SetCallbacks(IPluginContext *pContext, const cell_t *params)
static void SetCallbacks(SteamAPICall_t &hCall, SteamWorksHTTPRequest *pRequest) static void SetCallbacks(SteamAPICall_t &hCall, SteamWorksHTTPRequest *pRequest)
{ {
/* Only completion is a call result of this send. Header/data streaming
notifications are delivered as broadcast callbacks and routed to the request
by SteamWorksHTTP's dispatcher, so there is nothing to bind to hCall here. */
if (pRequest->pCompletedForward != NULL) if (pRequest->pCompletedForward != NULL)
{ {
pRequest->CompletedCallResult.SetGameserverFlag(); pRequest->CompletedCallResult.SetGameserverFlag();
pRequest->CompletedCallResult.Set(hCall, pRequest, &SteamWorksHTTPRequest::OnHTTPRequestCompleted); pRequest->CompletedCallResult.Set(hCall, pRequest, &SteamWorksHTTPRequest::OnHTTPRequestCompleted);
} }
if (pRequest->pHeadersReceivedForward != NULL)
{
pRequest->HeadersCallResult.SetGameserverFlag();
pRequest->HeadersCallResult.Set(hCall, pRequest, &SteamWorksHTTPRequest::OnHTTPHeadersReceived);
}
if (pRequest->pDataReceivedForward != NULL)
{
pRequest->DataCallResult.SetGameserverFlag();
pRequest->DataCallResult.Set(hCall, pRequest, &SteamWorksHTTPRequest::OnHTTPDataReceived);
}
} }
static cell_t sm_SendHTTPRequestAndStreamResponse(IPluginContext *pContext, const cell_t *params) static cell_t sm_SendHTTPRequestAndStreamResponse(IPluginContext *pContext, const cell_t *params)
@@ -500,7 +504,7 @@ static cell_t sm_SetHTTPRequestRawPostBodyFromFile(IPluginContext *pContext, con
} }
char *pBuffer = new char[size + 1]; char *pBuffer = new char[size + 1];
uint32_t itemsRead = fread(pBuffer, sizeof(char), size, pInputFile); size_t itemsRead = fread(pBuffer, sizeof(char), size, pInputFile);
fclose(pInputFile); fclose(pInputFile);
if (itemsRead != size) if (itemsRead != size)
+5 -4
View File
@@ -32,14 +32,15 @@ class SteamWorksHTTPRequest
Handle_t handle; Handle_t handle;
public: public:
/* Completion is a genuine call result of the send API call, so it stays a
CCallResult. Headers/data arrive as broadcast callbacks and are routed here
by SteamWorksHTTP's dispatcher, hence no per-request CCallResult for them. */
void OnHTTPRequestCompleted(HTTPRequestCompleted_t *pRequest, bool bFailed); void OnHTTPRequestCompleted(HTTPRequestCompleted_t *pRequest, bool bFailed);
void OnHTTPHeadersReceived(HTTPRequestHeadersReceived_t *pRequest, bool bFailed); void OnHTTPHeadersReceived(HTTPRequestHeadersReceived_t *pRequest);
void OnHTTPDataReceived(HTTPRequestDataReceived_t *pRequest, bool bFailed); void OnHTTPDataReceived(HTTPRequestDataReceived_t *pRequest);
public: public:
CCallResult<SteamWorksHTTPRequest, HTTPRequestCompleted_t> CompletedCallResult; CCallResult<SteamWorksHTTPRequest, HTTPRequestCompleted_t> CompletedCallResult;
CCallResult<SteamWorksHTTPRequest, HTTPRequestHeadersReceived_t> HeadersCallResult;
CCallResult<SteamWorksHTTPRequest, HTTPRequestDataReceived_t> DataCallResult;
public: public:
IChangeableForward *pCompletedForward; IChangeableForward *pCompletedForward;
+4 -2
View File
@@ -598,7 +598,8 @@ typeset SteamWorksHTTPRequestCompleted
* of trailing context parameters matches the context values set on the request. * of trailing context parameters matches the context values set on the request.
* *
* @param hRequest HTTP request handle. * @param hRequest HTTP request handle.
* @param bFailure True if the request failed due to an internal or network error. * @param bFailure Always false; headers-received is a success-only notification. A
* failed request is reported through SteamWorksHTTPRequestCompleted.
* @param data1 First context value, if one was set. * @param data1 First context value, if one was set.
* @param data2 Second context value, if one was set. * @param data2 Second context value, if one was set.
*/ */
@@ -615,7 +616,8 @@ typeset SteamWorksHTTPHeadersReceived
* number of trailing context parameters matches the context values set on the request. * number of trailing context parameters matches the context values set on the request.
* *
* @param hRequest HTTP request handle. * @param hRequest HTTP request handle.
* @param bFailure True if the request failed due to an internal or network error. * @param bFailure Always false; data-received is a success-only notification. A
* failed request is reported through SteamWorksHTTPRequestCompleted.
* @param offset Offset of this chunk within the response body. * @param offset Offset of this chunk within the response body.
* @param bytesreceived Number of bytes in this chunk. * @param bytesreceived Number of bytes in this chunk.
* @param data1 First context value, if one was set. * @param data1 First context value, if one was set.