diff --git a/natives.cpp b/natives.cpp index 92d48a1..e3ab27b 100644 --- a/natives.cpp +++ b/natives.cpp @@ -38,11 +38,14 @@ extern const sp_nativeinfo_t sourcetv_natives[]; +// Print to client consoles SH_DECL_MANUALHOOK0_void_vafmt(CBaseServer_BroadcastPrintf, 0, 0, 0); bool g_bHasClientPrintfOffset = false; +SH_DECL_MANUALHOOK1_void(CBaseClient_FireGameEvent, 0, 0, 0, IGameEvent *); void SetupNativeCalls() +bool g_bHasClientFireGameEventOffset = false; { int offset = -1; if (!g_pGameConf->GetOffset("CBaseServer::BroadcastPrintf", &offset) || offset == -1) @@ -54,6 +57,17 @@ void SetupNativeCalls() SH_MANUALHOOK_RECONFIGURE(CBaseServer_BroadcastPrintf, offset, 0, 0); g_bHasClientPrintfOffset = true; } + + offset = -1; + if (!g_pGameConf->GetOffset("CBaseClient::FireGameEvent", &offset) || offset == -1) + { + smutils->LogError(myself, "Failed to get CBaseClient::FireGameEvent offset."); + } + else + { + SH_MANUALHOOK_RECONFIGURE(CBaseClient_FireGameEvent, offset, 0, 0); + g_bHasClientFireGameEventOffset = true; + } } // native SourceTV_GetServerInstanceCount(); @@ -786,6 +800,60 @@ static cell_t Native_KickClient(IPluginContext *pContext, const cell_t *params) return 0; } +// native SourceTV_PrintToChat(client, const String:format[], any:...); +static cell_t Native_PrintToChat(IPluginContext *pContext, const cell_t *params) +{ + if (hltvserver == nullptr) + return 0; + + cell_t client = params[1]; + if (client < 1 || client > hltvserver->GetBaseServer()->GetClientCount()) + { + pContext->ReportError("Invalid spectator client index %d.", client); + return 0; + } + + HLTVClientWrapper *pClient = hltvserver->GetClient(client); + if (!pClient->IsConnected()) + { + pContext->ReportError("Client %d is not connected.", client); + return 0; + } + + char buffer[1024]; + size_t len; + { + DetectExceptions eh(pContext); + len = smutils->FormatString(buffer, sizeof(buffer), pContext, params, 2); + if (eh.HasException()) + return 0; + } + + IGameEvent *msg = gameevents->CreateEvent("hltv_chat", true); + if (!msg) + return 0; + +#if SOURCE_ENGINE == SE_CSGO + wchar_t wBuffer[1024]; + V_UTF8ToUnicode(buffer, wBuffer, sizeof(wBuffer)); + msg->SetWString("text", wBuffer); +#else + msg->SetString("text", buffer); +#endif + + if (g_bHasClientFireGameEventOffset) + { + void *pGameClient = pClient->BaseClient(); + // The IClient vtable is +4 from the CBaseClient vtable due to multiple inheritance. + pGameClient = (void *)((intptr_t)pGameClient - 4); + SH_MCALL(pGameClient, CBaseClient_FireGameEvent)(msg); + } + + gameevents->FreeEvent(msg); + + return 0; +} + const sp_nativeinfo_t sourcetv_natives[] = { { "SourceTV_GetServerInstanceCount", Native_GetServerInstanceCount }, @@ -822,5 +890,6 @@ const sp_nativeinfo_t sourcetv_natives[] = { "SourceTV_GetClientIP", Native_GetClientIP }, { "SourceTV_GetClientPassword", Native_GetClientPassword }, { "SourceTV_KickClient", Native_KickClient }, + { "SourceTV_PrintToChat", Native_PrintToChat }, { NULL, NULL }, }; diff --git a/sourcetv_test.sp b/sourcetv_test.sp index a5bb954..defe03a 100644 --- a/sourcetv_test.sp +++ b/sourcetv_test.sp @@ -34,6 +34,7 @@ public OnPluginStart() RegConsoleCmd("sm_democonsole", Cmd_PrintDemoConsole); RegConsoleCmd("sm_botcmd", Cmd_ExecuteStringCommand); RegConsoleCmd("sm_speckick", Cmd_KickClient); + RegConsoleCmd("sm_specchatone", Cmd_PrintToChat); } public SourceTV_OnStartRecording(instance, const String:filename[]) @@ -413,7 +414,7 @@ public Action:Cmd_ExecuteStringCommand(client, args) public Action:Cmd_KickClient(client, args) { - if (args < 1) + if (args < 2) { ReplyToCommand(client, "Usage: sm_speckick "); return Plugin_Handled; @@ -429,4 +430,24 @@ public Action:Cmd_KickClient(client, args) SourceTV_KickClient(iTarget, sMsg); ReplyToCommand(client, "SourceTV kicking spectator %d with reason %s", iTarget, sMsg); return Plugin_Handled; +} + +public Action:Cmd_PrintToChat(client, args) +{ + if (args < 2) + { + ReplyToCommand(client, "Usage: sm_specchatone "); + return Plugin_Handled; + } + + new String:sIndex[16], String:sMsg[1024]; + GetCmdArg(1, sIndex, sizeof(sIndex)); + StripQuotes(sIndex); + GetCmdArg(2, sMsg, sizeof(sMsg)); + StripQuotes(sMsg); + + new iTarget = StringToInt(sIndex); + SourceTV_PrintToChat(iTarget, "%s", sMsg); + ReplyToCommand(client, "SourceTV sending chat message to spectator %d: %s", iTarget, sMsg); + return Plugin_Handled; } \ No newline at end of file diff --git a/sourcetvmanager.games.txt b/sourcetvmanager.games.txt index cdc8987..ebeeb16 100644 --- a/sourcetvmanager.games.txt +++ b/sourcetvmanager.games.txt @@ -63,6 +63,12 @@ "linux" "65" } + "CBaseClient::FireGameEvent" + { + "windows" "1" + "linux" "2" + } + "CBaseClient::Disconnect" { "linux" "16" @@ -230,6 +236,12 @@ "linux" "56" } + "CBaseClient::FireGameEvent" + { + "windows" "1" + "linux" "2" + } + "CBaseClient::Disconnect" { "linux" "14" diff --git a/sourcetvmanager.inc b/sourcetvmanager.inc index 88e3783..a5c099a 100644 --- a/sourcetvmanager.inc +++ b/sourcetvmanager.inc @@ -393,6 +393,16 @@ native SourceTV_GetClientPassword(client, String:password[], maxlen); */ native SourceTV_KickClient(client, const String:sReason[]); +/** + * Print a message to a single client's chat. + * + * @param client The spectator client index. + * @param format The format string. + * @param ... Variable number of format string arguments. + * @noreturn + * @error Invalid client index or not connected. + */ +native SourceTV_PrintToChat(client, const String:format[], any:...); /** * Called when a spectator wants to connect to the SourceTV server. @@ -520,5 +530,6 @@ public __ext_stvmngr_SetNTVOptional() MarkNativeAsOptional("SourceTV_GetClientIP"); MarkNativeAsOptional("SourceTV_GetClientPassword"); MarkNativeAsOptional("SourceTV_KickClient"); + MarkNativeAsOptional("SourceTV_PrintToChat"); } #endif