|
@@ -0,0 +1,511 @@
|
|
|
|
+/**
|
|
|
|
+ * [CSRD] Custom Achievements
|
|
|
|
+ *
|
|
|
|
+ * The custom achievement implementation for Pikachu's Canadian Server of Romance and Drama.
|
|
|
|
+ */
|
|
|
|
+#pragma semicolon 1
|
|
|
|
+#include <sourcemod>
|
|
|
|
+
|
|
|
|
+#include <tf2_stocks>
|
|
|
|
+#include <sdktools_sound>
|
|
|
|
+
|
|
|
|
+#pragma newdecls required
|
|
|
|
+#include <stocksoup/log_server>
|
|
|
|
+#include <stocksoup/tf/tempents_stocks>
|
|
|
|
+
|
|
|
|
+#define PLUGIN_VERSION "0.1.0"
|
|
|
|
+public Plugin myinfo = {
|
|
|
|
+ name = "[CSRD] Custom Achievements",
|
|
|
|
+ author = "nosoop",
|
|
|
|
+ description = "Provides a lightweight API for custom achievements.",
|
|
|
|
+ version = PLUGIN_VERSION,
|
|
|
|
+ url = "https://git.csrd.science/"
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Maximum language shortcode length (2-3 characters usually, though 'pt_p' is a thing).
|
|
|
|
+#define MAX_LANG_SC_LENGTH 8
|
|
|
|
+
|
|
|
|
+// Head attachment point index for player.
|
|
|
|
+#define ATTACHMENT_HEAD 1
|
|
|
|
+
|
|
|
|
+public APLRes AskPluginLoad2(Handle hPlugin, bool late, char[] error, int err_max) {
|
|
|
|
+ RegPluginLibrary("custom-achievements");
|
|
|
|
+
|
|
|
|
+ CreateNative("CustomAchievement.CustomAchievement", Native_RegisterAchievement);
|
|
|
|
+ CreateNative("CustomAchievement.AwardToAccountID", Native_AwardAchievement);
|
|
|
|
+ CreateNative("CustomAchievement.FetchMetadataByAccountID", Native_FetchAchievementMetadata);
|
|
|
|
+ CreateNative("CustomAchievement.StoreMetadataByAccountID", Native_StoreAchievementMetadata);
|
|
|
|
+ CreateNative("CustomAchievement.ResetByAccountID", Native_ResetAchievement);
|
|
|
|
+
|
|
|
|
+ return APLRes_Success;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+ConVar g_ConVarDebug;
|
|
|
|
+
|
|
|
|
+Database g_Database;
|
|
|
|
+DBStatement g_QueryRegisterAchievement;
|
|
|
|
+DBStatement g_QueryGetAchievementIdentifier;
|
|
|
|
+
|
|
|
|
+Handle g_ForwardOnCustomAchievementAwarded;
|
|
|
|
+
|
|
|
|
+float g_flLastEarnedAchievementTime[MAXPLAYERS+1];
|
|
|
|
+
|
|
|
|
+public void OnPluginStart() {
|
|
|
|
+ g_ConVarDebug = CreateConVar("custom_achievement_debug", "0", "Generate debug spew?");
|
|
|
|
+
|
|
|
|
+ char error[64];
|
|
|
|
+
|
|
|
|
+ if ((g_Database = SQLite_UseDatabase("custom-achievements", error, sizeof(error)))
|
|
|
|
+ == INVALID_HANDLE) {
|
|
|
|
+ SetFailState("Could not use database custom-achievements for custom achievements: %s",
|
|
|
|
+ error);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ g_QueryRegisterAchievement = SQL_PrepareQuery(g_Database,
|
|
|
|
+ "INSERT OR IGNORE INTO achievements (name, displaytype) VALUES (?, ?)",
|
|
|
|
+ error, sizeof(error));
|
|
|
|
+
|
|
|
|
+ if (!g_QueryRegisterAchievement) {
|
|
|
|
+ SetFailState("Could not create achievement instantiation query: %s", error);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ g_QueryGetAchievementIdentifier = SQL_PrepareQuery(g_Database,
|
|
|
|
+ "SELECT achievement_id FROM achievements WHERE name = ?",
|
|
|
|
+ error, sizeof(error));
|
|
|
|
+
|
|
|
|
+ if (!g_QueryGetAchievementIdentifier) {
|
|
|
|
+ SetFailState("Could not create achievement identifier query: %s", error);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ g_ForwardOnCustomAchievementAwarded = CreateGlobalForward("OnCustomAchievementAwarded",
|
|
|
|
+ ET_Ignore, Param_Cell, Param_Cell);
|
|
|
|
+
|
|
|
|
+ HookEvent("achievement_earned", OnAchievementEarned);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+public void OnClientConnected(int client) {
|
|
|
|
+ g_flLastEarnedAchievementTime[client] = 0.0;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+public void OnAchievementEarned(Event event, const char[] name, bool dontBroadcast) {
|
|
|
|
+ int recipient = event.GetInt("player");
|
|
|
|
+
|
|
|
|
+ if (recipient && recipient <= MaxClients) {
|
|
|
|
+ g_flLastEarnedAchievementTime[recipient] = GetGameTime();
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+public int Native_RegisterAchievement(Handle hPlugin, int argc) {
|
|
|
|
+ int internalNameLength;
|
|
|
|
+ GetNativeStringLength(1, internalNameLength);
|
|
|
|
+ internalNameLength++;
|
|
|
|
+
|
|
|
|
+ char[] internalName = new char[internalNameLength];
|
|
|
|
+ GetNativeString(1, internalName, internalNameLength);
|
|
|
|
+
|
|
|
|
+ int achievementStyle = GetNativeCell(2);
|
|
|
|
+
|
|
|
|
+ g_QueryRegisterAchievement.BindString(0, internalName, false);
|
|
|
|
+ g_QueryRegisterAchievement.BindInt(1, achievementStyle, false);
|
|
|
|
+
|
|
|
|
+ SQL_Execute(g_QueryRegisterAchievement);
|
|
|
|
+
|
|
|
|
+ g_QueryGetAchievementIdentifier.BindString(0, internalName, false);
|
|
|
|
+ SQL_Execute(g_QueryGetAchievementIdentifier);
|
|
|
|
+
|
|
|
|
+ int iAchievement;
|
|
|
|
+ if (SQL_FetchRow(g_QueryGetAchievementIdentifier)) {
|
|
|
|
+ iAchievement = SQL_FetchInt(g_QueryGetAchievementIdentifier, 0);
|
|
|
|
+ }
|
|
|
|
+ LogDebug("Achievement '%s' registered under identifier %d", internalName, iAchievement);
|
|
|
|
+ return iAchievement;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+public int Native_AwardAchievement(Handle hPlugin, int argc) {
|
|
|
|
+ int iAchievement = GetNativeCell(1);
|
|
|
|
+ int steamid3 = GetNativeCell(2);
|
|
|
|
+ bool notify = GetNativeCell(3) != 0;
|
|
|
|
+
|
|
|
|
+ char query[1024];
|
|
|
|
+
|
|
|
|
+ Format(query, sizeof(query), "SELECT achieved FROM achievement_status "
|
|
|
|
+ ... "WHERE steamid3 = '%d' AND achievement_id = '%d'",
|
|
|
|
+ steamid3, iAchievement);
|
|
|
|
+
|
|
|
|
+ DataPack recipientData = new DataPack();
|
|
|
|
+ recipientData.WriteCell(iAchievement);
|
|
|
|
+ recipientData.WriteCell(steamid3);
|
|
|
|
+ recipientData.WriteCell(notify);
|
|
|
|
+
|
|
|
|
+ LogDebug("Awarding achievement %d to steamid %d", iAchievement, steamid3);
|
|
|
|
+ SQL_TQuery(g_Database, SQLT_OnPreAchievementAwarded, query, recipientData);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+public void SQLT_OnPreAchievementAwarded(Handle hOwner, Handle hChild, const char[] error,
|
|
|
|
+ DataPack recipientData) {
|
|
|
|
+ recipientData.Reset();
|
|
|
|
+
|
|
|
|
+ int iAchievement = recipientData.ReadCell();
|
|
|
|
+ int steamid3 = recipientData.ReadCell();
|
|
|
|
+ bool notify = recipientData.ReadCell() != 0;
|
|
|
|
+ delete recipientData;
|
|
|
|
+
|
|
|
|
+ DBResultSet resultSet = view_as<DBResultSet>(hChild);
|
|
|
|
+
|
|
|
|
+ if (!resultSet || !resultSet.FetchRow() || resultSet.FetchInt(0) == 0) {
|
|
|
|
+ LogDebug("Account %d does not have achievement yet. Granting.", steamid3);
|
|
|
|
+
|
|
|
|
+ char query[1024];
|
|
|
|
+
|
|
|
|
+ Transaction transaction = new Transaction();
|
|
|
|
+
|
|
|
|
+ // update player row to indicate achievement
|
|
|
|
+ Format(query, sizeof(query), "UPDATE OR IGNORE achievement_status SET achieved=%d "
|
|
|
|
+ ... "WHERE steamid3 = '%d' AND achievement_id = '%d'",
|
|
|
|
+ GetTime(), steamid3, iAchievement);
|
|
|
|
+ transaction.AddQuery(query);
|
|
|
|
+
|
|
|
|
+ // create new row for player achievement if it doesn't exist
|
|
|
|
+ Format(query, sizeof(query), "INSERT OR IGNORE INTO achievement_status "
|
|
|
|
+ ..."(achievement_id, steamid3, achieved) VALUES (%d, %d, %d)",
|
|
|
|
+ iAchievement, steamid3, GetTime());
|
|
|
|
+ transaction.AddQuery(query);
|
|
|
|
+
|
|
|
|
+ // transaction is autoclosed
|
|
|
|
+ SQL_ExecuteTransaction(g_Database, transaction);
|
|
|
|
+
|
|
|
|
+ if (notify) {
|
|
|
|
+ // perform query to get all localized strings, then fetch each and send to clients as appropriate
|
|
|
|
+ CallOnCustomAchievementAwarded(steamid3, iAchievement);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+public int Native_FetchAchievementMetadata(Handle hPlugin, int argc) {
|
|
|
|
+ int iAchievement = GetNativeCell(1);
|
|
|
|
+ int steamid3 = GetNativeCell(2);
|
|
|
|
+
|
|
|
|
+ DataPack fetchPack = new DataPack();
|
|
|
|
+ fetchPack.WriteCell(iAchievement);
|
|
|
|
+ fetchPack.WriteCell(hPlugin);
|
|
|
|
+ fetchPack.WriteFunction(GetNativeFunction(3));
|
|
|
|
+ fetchPack.WriteCell(GetNativeCell(4));
|
|
|
|
+
|
|
|
|
+ char query[1024];
|
|
|
|
+ Format(query, sizeof(query), "SELECT metadata FROM achievement_status "
|
|
|
|
+ ... "WHERE achievement_id = %d AND steamid3 = %d", iAchievement, steamid3);
|
|
|
|
+
|
|
|
|
+ SQL_TQuery(g_Database, SQLT_OnMetadataReceived, query, fetchPack);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+public void SQLT_OnMetadataReceived(Handle owner, Handle child, const char[] error,
|
|
|
|
+ DataPack fetchPack) {
|
|
|
|
+ fetchPack.Reset();
|
|
|
|
+
|
|
|
|
+ int iAchievement = fetchPack.ReadCell();
|
|
|
|
+ Handle hPlugin = fetchPack.ReadCell();
|
|
|
|
+ Function callback = fetchPack.ReadFunction();
|
|
|
|
+ any data = fetchPack.ReadCell();
|
|
|
|
+ delete fetchPack;
|
|
|
|
+
|
|
|
|
+ if (GetPluginStatus(hPlugin) != Plugin_Running) {
|
|
|
|
+ ThrowError("Calling plugin was unloaded before a query callback");
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ Call_StartFunction(hPlugin, callback);
|
|
|
|
+ Call_PushCell(iAchievement);
|
|
|
|
+
|
|
|
|
+ DBResultSet resultSet = view_as<DBResultSet>(child);
|
|
|
|
+ if (resultSet && resultSet.FetchRow()) {
|
|
|
|
+ int metadataLength = resultSet.FetchSize(0) + 1;
|
|
|
|
+ char[] metadata = new char[metadataLength];
|
|
|
|
+ resultSet.FetchString(0, metadata, metadataLength);
|
|
|
|
+
|
|
|
|
+ Call_PushString(metadata);
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * quirk: we have to finish the call before metadata goes out of scope, otherwise the
|
|
|
|
+ * callback gets an empty string???
|
|
|
|
+ */
|
|
|
|
+ Call_PushCell(data);
|
|
|
|
+ Call_Finish();
|
|
|
|
+ } else {
|
|
|
|
+ Call_PushString("");
|
|
|
|
+ Call_PushCell(data);
|
|
|
|
+ Call_Finish();
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+public int Native_StoreAchievementMetadata(Handle hPlugin, int argc) {
|
|
|
|
+ int iAchievement = GetNativeCell(1);
|
|
|
|
+ int steamid3 = GetNativeCell(2);
|
|
|
|
+ int metadataLength;
|
|
|
|
+
|
|
|
|
+ GetNativeStringLength(3, metadataLength);
|
|
|
|
+ metadataLength++;
|
|
|
|
+ metadataLength *= 2; // buffer must be at least 2*strlen(string)+1 [when SQL escaping]
|
|
|
|
+
|
|
|
|
+ char[] metadata = new char[metadataLength];
|
|
|
|
+ GetNativeString(3, metadata, metadataLength);
|
|
|
|
+
|
|
|
|
+ SQL_EscapeString(g_Database, metadata, metadata, metadataLength);
|
|
|
|
+
|
|
|
|
+ int queryLength = metadataLength + 1024;
|
|
|
|
+ char[] query = new char[queryLength];
|
|
|
|
+
|
|
|
|
+ // attempt to update an existing row
|
|
|
|
+ Transaction transaction = new Transaction();
|
|
|
|
+ Format(query, queryLength, "UPDATE OR IGNORE achievement_status SET metadata='%s' "
|
|
|
|
+ ... "WHERE steamid3 = '%d' AND achievement_id = '%d'",
|
|
|
|
+ metadata, steamid3, iAchievement);
|
|
|
|
+ transaction.AddQuery(query);
|
|
|
|
+
|
|
|
|
+ // create row if it doesn't
|
|
|
|
+ Format(query, queryLength, "INSERT OR IGNORE INTO achievement_status "
|
|
|
|
+ ..."(achievement_id, steamid3, metadata) VALUES (%d, %d, '%s')",
|
|
|
|
+ iAchievement, steamid3, metadata);
|
|
|
|
+ transaction.AddQuery(query);
|
|
|
|
+
|
|
|
|
+ // transaction is autoclosed
|
|
|
|
+ SQL_ExecuteTransaction(g_Database, transaction);
|
|
|
|
+
|
|
|
|
+ LogDebug("Wrote metadata for achievement %d under steamid3 %d (%s)", iAchievement, steamid3,
|
|
|
|
+ metadata);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+public int Native_ResetAchievement(Handle hPlugin, int argc) {
|
|
|
|
+ int iAchievement = GetNativeCell(1);
|
|
|
|
+ int steamid3 = GetNativeCell(2);
|
|
|
|
+
|
|
|
|
+ char query[256];
|
|
|
|
+
|
|
|
|
+ // We update the entries instead of deleting the row in case the row is regenerated anyways
|
|
|
|
+ Transaction transaction = new Transaction();
|
|
|
|
+ Format(query, sizeof(query), "UPDATE OR IGNORE achievement_status "
|
|
|
|
+ ... "SET metadata='', achieved=0 "
|
|
|
|
+ ... "WHERE steamid3 = '%d' AND achievement_id = '%d'",
|
|
|
|
+ steamid3, iAchievement);
|
|
|
|
+ transaction.AddQuery(query);
|
|
|
|
+
|
|
|
|
+ SQL_ExecuteTransaction(g_Database, transaction);
|
|
|
|
+
|
|
|
|
+ LogDebug("Reset achievement %d for steamid3 %d", iAchievement, steamid3);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+void CallOnCustomAchievementAwarded(int steamid3, int iAchievement) {
|
|
|
|
+ int recipient = FindClientByAccountID(steamid3);
|
|
|
|
+
|
|
|
|
+ if (recipient) {
|
|
|
|
+ char recipientName[MAX_NAME_LENGTH];
|
|
|
|
+ GetClientName(recipient, recipientName, sizeof(recipientName));
|
|
|
|
+
|
|
|
|
+ // int nClients;
|
|
|
|
+ // int clients[MAXPLAYERS+1];
|
|
|
|
+
|
|
|
|
+ TransmitAchievementEvent(recipient, iAchievement);
|
|
|
|
+
|
|
|
|
+ Call_StartForward(g_ForwardOnCustomAchievementAwarded);
|
|
|
|
+ Call_PushCell(recipient);
|
|
|
|
+ Call_PushCell(iAchievement);
|
|
|
|
+ Call_Finish();
|
|
|
|
+
|
|
|
|
+ LogDebug("%N was awarded achievement %d", recipient, iAchievement);
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Prepares the achievement event by pulling localization strings.
|
|
|
|
+ * If no localization strings are available, the achievement effects (particles and sound) are
|
|
|
|
+ * not played.
|
|
|
|
+ */
|
|
|
|
+void TransmitAchievementEvent(int recipient, int iAchievement) {
|
|
|
|
+ char query[512];
|
|
|
|
+
|
|
|
|
+ Format(query, sizeof(query),
|
|
|
|
+ "SELECT language_shortcode, achievement_local_name, achievement_local_description "
|
|
|
|
+ ... "FROM achievement_languages JOIN achievements ON achievement_name = name "
|
|
|
|
+ ... "WHERE achievement_id = '%d'", iAchievement);
|
|
|
|
+
|
|
|
|
+ DataPack pack = new DataPack();
|
|
|
|
+ pack.WriteCell(GetClientUserId(recipient));
|
|
|
|
+ pack.WriteCell(iAchievement);
|
|
|
|
+
|
|
|
|
+ SQL_TQuery(g_Database, SQLT_OnLocalizedAchievementStringsReceived, query, pack);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+public void SQLT_OnLocalizedAchievementStringsReceived(Handle hOwner, Handle hChild,
|
|
|
|
+ const char[] error, DataPack pack) {
|
|
|
|
+ pack.Reset();
|
|
|
|
+ int recipient = GetClientOfUserId(pack.ReadCell());
|
|
|
|
+ int iAchievement = pack.ReadCell();
|
|
|
|
+
|
|
|
|
+ if (recipient) {
|
|
|
|
+ DBResultSet resultSet = view_as<DBResultSet>(hChild);
|
|
|
|
+
|
|
|
|
+ if (resultSet && resultSet.RowCount > 0) {
|
|
|
|
+ KeyValues localizedStrings = new KeyValues("Achievement");
|
|
|
|
+
|
|
|
|
+ // Put all name / descriptions into sections corresponding to language shortcodes.
|
|
|
|
+ while (resultSet.FetchRow()) {
|
|
|
|
+ int nNameLength, nDescriptionLength;
|
|
|
|
+
|
|
|
|
+ char languageShortCode[MAX_LANG_SC_LENGTH];
|
|
|
|
+ resultSet.FetchString(0, languageShortCode, sizeof(languageShortCode));
|
|
|
|
+
|
|
|
|
+ nNameLength = resultSet.FetchSize(1) + 1;
|
|
|
|
+ char[] achievementName = new char[nNameLength];
|
|
|
|
+ resultSet.FetchString(1, achievementName, nNameLength);
|
|
|
|
+
|
|
|
|
+ nDescriptionLength = resultSet.FetchSize(2) + 1;
|
|
|
|
+ char[] achievementDescription = new char[nDescriptionLength];
|
|
|
|
+ resultSet.FetchString(2, achievementDescription, nDescriptionLength);
|
|
|
|
+
|
|
|
|
+ if (localizedStrings.JumpToKey(languageShortCode, true)) {
|
|
|
|
+ localizedStrings.SetString("name", achievementName);
|
|
|
|
+ localizedStrings.SetString("description", achievementDescription);
|
|
|
|
+
|
|
|
|
+ localizedStrings.GoBack();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ localizedStrings.Rewind();
|
|
|
|
+
|
|
|
|
+ char serverLanguageShortCode[MAX_LANG_SC_LENGTH];
|
|
|
|
+ GetLanguageInfo(GetServerLanguage(), serverLanguageShortCode,
|
|
|
|
+ sizeof(serverLanguageShortCode));
|
|
|
|
+
|
|
|
|
+ // Ensure the server's language is localized as a fallback measure.
|
|
|
|
+ if (localizedStrings.JumpToKey(serverLanguageShortCode)) {
|
|
|
|
+ localizedStrings.GoBack();
|
|
|
|
+
|
|
|
|
+ // Identify and send localized translations to each client, if possible.
|
|
|
|
+ for (int i = 1; i <= MaxClients; i++) {
|
|
|
|
+ if (IsClientInGame(i) && !IsFakeClient(i)) {
|
|
|
|
+ char languageShortCode[MAX_LANG_SC_LENGTH];
|
|
|
|
+
|
|
|
|
+ GetLanguageInfo(GetClientLanguage(i), languageShortCode,
|
|
|
|
+ sizeof(languageShortCode));
|
|
|
|
+
|
|
|
|
+ // Use server (default) language if client language isn't available.
|
|
|
|
+ if (localizedStrings.JumpToKey(languageShortCode)
|
|
|
|
+ || localizedStrings.JumpToKey(serverLanguageShortCode)) {
|
|
|
|
+ char localName[64], localDescription[256];
|
|
|
|
+
|
|
|
|
+ localizedStrings.GetString("name", localName, sizeof(localName));
|
|
|
|
+ localizedStrings.GetString("description", localDescription,
|
|
|
|
+ sizeof(localDescription));
|
|
|
|
+
|
|
|
|
+ SendAchievementMessageToOne(recipient, i, localName,
|
|
|
|
+ localDescription);
|
|
|
|
+
|
|
|
|
+ localizedStrings.GoBack();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ TransmitAchievementEffects(recipient);
|
|
|
|
+
|
|
|
|
+ g_flLastEarnedAchievementTime[recipient] = GetGameTime();
|
|
|
|
+ } else {
|
|
|
|
+ LogMessage("Achievement %d is missing server / fallback localization entries "
|
|
|
|
+ ... "in server's language '%s'. Achievement will not be displayed.",
|
|
|
|
+ iAchievement, serverLanguageShortCode);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ delete localizedStrings;
|
|
|
|
+ } else {
|
|
|
|
+ LogMessage("Achievement %d does not have any localized strings.", iAchievement);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ delete pack;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Plays the achievement sound and displays the related particle on the recipient.
|
|
|
|
+ */
|
|
|
|
+void TransmitAchievementEffects(int recipient) {
|
|
|
|
+ /**
|
|
|
|
+ * TODO add temporal filter if event `achievement_earned` or this function was called within
|
|
|
|
+ * the last second so the effect is only used once.
|
|
|
|
+ */
|
|
|
|
+ if (GetGameTime() - g_flLastEarnedAchievementTime[recipient] > 1.0) {
|
|
|
|
+ TFTeam recipientTeam = TF2_GetClientTeam(recipient);
|
|
|
|
+ if (recipientTeam != TFTeam_Unassigned && recipientTeam != TFTeam_Spectator) {
|
|
|
|
+ EmitGameSoundToAll("Achievement.Earned", recipient);
|
|
|
|
+
|
|
|
|
+ if (IsPlayerAlive(recipient)) {
|
|
|
|
+ TE_SetupTFParticleEffect("achieved", NULL_VECTOR, NULL_VECTOR, NULL_VECTOR,
|
|
|
|
+ recipient, PATTACH_POINT_FOLLOW, ATTACHMENT_HEAD);
|
|
|
|
+ TE_SendToAll();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Returns a client with the specified Steam account ID, or 0 if none.
|
|
|
|
+ */
|
|
|
|
+stock int FindClientByAccountID(int steamid3) {
|
|
|
|
+ for (int i = 1; i <= MaxClients; i++) {
|
|
|
|
+ if (!IsFakeClient(i) && IsClientAuthorized(i)) {
|
|
|
|
+ if (steamid3 == GetSteamAccountID(i)) {
|
|
|
|
+ return i;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return 0;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Sends the "<recipient> has earned the achievement <achievement>" message to the specified
|
|
|
|
+ * client.
|
|
|
|
+ *
|
|
|
|
+ * If a description is specified, the description is enclosed in parentheses.
|
|
|
|
+ */
|
|
|
|
+void SendAchievementMessageToOne(int recipient, int client, const char[] achievementName,
|
|
|
|
+ const char[] achievementDescription = "") {
|
|
|
|
+ char recipientName[MAX_NAME_LENGTH];
|
|
|
|
+ GetClientName(recipient, recipientName, sizeof(recipientName));
|
|
|
|
+
|
|
|
|
+ int clients[1];
|
|
|
|
+ clients[0] = client;
|
|
|
|
+
|
|
|
|
+ if (!strlen(achievementDescription)) {
|
|
|
|
+ // no description provided
|
|
|
|
+ SendSayText2Message(recipient, clients, 1, "#Achievement_Earned", recipientName,
|
|
|
|
+ achievementName);
|
|
|
|
+ } else {
|
|
|
|
+ int bufferLength = strlen(achievementName) + strlen(achievementDescription) + 16;
|
|
|
|
+ char[] achievementBuffer = new char[bufferLength];
|
|
|
|
+ Format(achievementBuffer, bufferLength, "%s \x01(%s)", achievementName,
|
|
|
|
+ achievementDescription);
|
|
|
|
+
|
|
|
|
+ SendSayText2Message(recipient, clients, 1, "#Achievement_Earned", recipientName,
|
|
|
|
+ achievementBuffer);
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+void SendSayText2Message(int author, int[] clients, int nClients,
|
|
|
|
+ const char[] localizationToken, const char[] name, const char[] message) {
|
|
|
|
+ Handle buffer = StartMessage("SayText2", clients, nClients,
|
|
|
|
+ USERMSG_RELIABLE | USERMSG_BLOCKHOOKS);
|
|
|
|
+
|
|
|
|
+ BfWrite bitbuf = view_as<BfWrite>(buffer);
|
|
|
|
+
|
|
|
|
+ bitbuf.WriteByte(author);
|
|
|
|
+ bitbuf.WriteByte(true);
|
|
|
|
+ bitbuf.WriteString(localizationToken);
|
|
|
|
+ bitbuf.WriteString(name);
|
|
|
|
+ bitbuf.WriteString(message);
|
|
|
|
+
|
|
|
|
+ EndMessage();
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+void LogDebug(const char[] format, any ...) {
|
|
|
|
+ if (g_ConVarDebug.BoolValue) {
|
|
|
|
+ char message[256];
|
|
|
|
+ VFormat(message, sizeof(message), format, 2);
|
|
|
|
+ LogServer(message);
|
|
|
|
+ }
|
|
|
|
+}
|